fix navbar freezing fix
Some checks failed
Release / meta (push) Failing after 9s
Release / android-build (push) Has been skipped
Release / linux-build (push) Has been skipped
Release / release-dev (push) Has been skipped
Release / release-master (push) Has been skipped

This commit is contained in:
2025-12-17 17:41:09 +00:00
parent a9e0cdbe1b
commit 587933fa50
7 changed files with 182 additions and 63 deletions

View File

@@ -45,7 +45,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.mileograph_flutter" applicationId = "com.petegregoryy.mileograph_flutter"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:mileograph_flutter/components/calculator/calculator.dart'; import 'package:mileograph_flutter/components/calculator/calculator.dart';
import 'package:mileograph_flutter/components/pages/traction.dart'; import 'package:mileograph_flutter/components/pages/traction.dart';

View File

@@ -10,6 +10,7 @@ class NewEntryPage extends StatefulWidget {
} }
class _NewEntryPageState extends State<NewEntryPage> { class _NewEntryPageState extends State<NewEntryPage> {
late final NavigationGuardCallback _exitGuard;
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
DateTime _selectedDate = DateTime.now(); DateTime _selectedDate = DateTime.now();
TimeOfDay _selectedTime = TimeOfDay.now(); TimeOfDay _selectedTime = TimeOfDay.now();
@@ -40,7 +41,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
NavigationGuard.register(_handleExitIntent); _exitGuard = _handleExitIntent;
NavigationGuard.register(_exitGuard);
// legacy single-draft auto-save listeners removed in favor of explicit multi-draft flow // legacy single-draft auto-save listeners removed in favor of explicit multi-draft flow
Future.microtask(() { Future.microtask(() {
if (!mounted) return; if (!mounted) return;
@@ -58,7 +60,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
@override @override
void dispose() { void dispose() {
NavigationGuard.unregister(_handleExitIntent); NavigationGuard.unregister(_exitGuard);
_startController.dispose(); _startController.dispose();
_endController.dispose(); _endController.dispose();
_headcodeController.dispose(); _headcodeController.dispose();
@@ -586,7 +588,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
if (didPop) return; if (didPop) return;
final allow = await _handleExitIntent(); final allow = await _handleExitIntent();
if (allow && context.mounted) { if (allow && context.mounted) {
Navigator.of(context).maybePop(); final router = GoRouter.of(context);
if (router.canPop()) {
context.pop();
} else {
context.go('/');
}
} }
}, },
child: Scaffold( child: Scaffold(
@@ -597,7 +604,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
onPressed: () async { onPressed: () async {
if (!await _handleExitIntent()) return; if (!await _handleExitIntent()) return;
if (!context.mounted) return; if (!context.mounted) return;
Navigator.of(context).maybePop(); final router = GoRouter.of(context);
if (router.canPop()) {
context.pop();
} else {
context.go('/');
}
}, },
), ),
title: const Text('Edit entry'), title: const Text('Edit entry'),

View File

@@ -209,22 +209,46 @@ class DataService extends ChangeNotifier {
final target = date ?? DateTime.now(); final target = date ?? DateTime.now();
final formatted = final formatted =
"${target.year.toString().padLeft(4, '0')}-${target.month.toString().padLeft(2, '0')}-${target.day.toString().padLeft(2, '0')}"; "${target.year.toString().padLeft(4, '0')}-${target.month.toString().padLeft(2, '0')}-${target.day.toString().padLeft(2, '0')}";
final endpoint = '/legs/on-this-day?date=$formatted';
dynamic json;
Object? lastError;
for (int attempt = 0; attempt < 2; attempt++) {
try {
json = await api.get(endpoint);
lastError = null;
break;
} catch (e) {
lastError = e;
if (!_looksLikeOnThisDayRoutingConflict(e)) break;
await Future<void>.delayed(const Duration(milliseconds: 250));
}
}
try { try {
final json = await api.get('/legs/on-this-day?date=$formatted');
if (json is List) { if (json is List) {
_onThisDay = json.map((e) => Leg.fromJson(e)).toList(); _onThisDay = json.map((e) => Leg.fromJson(e)).toList();
} else { } else {
_onThisDay = []; _onThisDay = [];
} }
} catch (e) { } catch (e) {
debugPrint('Failed to fetch on-this-day legs: $e'); lastError ??= e;
_onThisDay = []; _onThisDay = [];
} finally { } finally {
if (lastError != null) {
debugPrint('Failed to fetch on-this-day legs ($endpoint): $lastError');
}
_isOnThisDayLoading = false; _isOnThisDayLoading = false;
_notifyAsync(); _notifyAsync();
} }
} }
bool _looksLikeOnThisDayRoutingConflict(Object error) {
final msg = error.toString();
return msg.contains('API error 422') &&
msg.contains('[path, loco_id]') &&
msg.contains('input: on-this-day');
}
Future<void> fetchEventFields({bool force = false}) async { Future<void> fetchEventFields({bool force = false}) async {
if (_eventFields.isNotEmpty && !force) return; if (_eventFields.isNotEmpty && !force) return;
_isEventFieldsLoading = true; _isEventFieldsLoading = true;

View File

@@ -7,9 +7,10 @@ class NavigationGuard {
_callback = callback; _callback = callback;
} }
static void unregister(NavigationGuardCallback callback) { static void unregister([NavigationGuardCallback? callback]) {
if (_callback == callback) { if (callback == null || identical(_callback, callback)) {
_callback = null; _callback = null;
_promptActive = false;
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:mileograph_flutter/components/login/login.dart'; import 'package:mileograph_flutter/components/login/login.dart';
import 'package:mileograph_flutter/components/pages/calculator.dart'; import 'package:mileograph_flutter/components/pages/calculator.dart';
@@ -17,6 +18,28 @@ import 'package:mileograph_flutter/services/data_service.dart';
import 'package:mileograph_flutter/services/navigation_guard.dart'; import 'package:mileograph_flutter/services/navigation_guard.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>();
const List<String> _contentPages = [
"/",
"/calculator",
"/legs",
"/traction",
"/trips",
"/add",
];
const int _addTabIndex = 5;
int tabIndexForPath(String path) {
final newIndex = _contentPages.indexWhere((routePath) {
if (path == routePath) return true;
if (routePath == '/') return path == '/';
return path.startsWith('$routePath/');
});
return newIndex < 0 ? 0 : newIndex;
}
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
const MyApp({super.key}); const MyApp({super.key});
@@ -52,6 +75,7 @@ class _MyAppState extends State<MyApp> {
}, },
routes: [ routes: [
ShellRoute( ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) => MyHomePage(child: child), builder: (context, state, child) => MyHomePage(child: child),
routes: [ routes: [
GoRoute(path: '/', builder: (context, state) => const Dashboard()), GoRoute(path: '/', builder: (context, state) => const Dashboard()),
@@ -153,26 +177,7 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
final List<String> contentPages = [ List<String> get contentPages => _contentPages;
"/",
"/calculator",
"/legs",
"/traction",
"/trips",
"/add",
];
int _getIndexFromLocation(String location) {
int newIndex = contentPages.indexWhere((path) {
if (location == path) return true;
if (path == '/') return location == '/';
return location.startsWith('$path/');
});
if (newIndex < 0) {
return 0;
}
return newIndex;
}
Future<void> _onItemTapped(int index, int currentIndex) async { Future<void> _onItemTapped(int index, int currentIndex) async {
if (index < 0 || index >= contentPages.length || index == currentIndex) { if (index < 0 || index >= contentPages.length || index == currentIndex) {
@@ -184,6 +189,10 @@ class _MyHomePageState extends State<MyHomePage> {
}); });
} }
int? _lastTabIndex;
final List<int> _tabHistory = [];
bool _handlingBackNavigation = false;
bool _fetched = false; bool _fetched = false;
@override @override
@@ -221,8 +230,12 @@ class _MyHomePageState extends State<MyHomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final location = GoRouterState.of(context).uri.toString(); final uri = GoRouterState.of(context).uri;
final pageIndex = _getIndexFromLocation(location); final pageIndex = tabIndexForPath(uri.path);
_recordTabChange(pageIndex);
if (pageIndex != _addTabIndex) {
NavigationGuard.unregister();
}
final homepageReady = context.select<DataService, bool>( final homepageReady = context.select<DataService, bool>(
(data) => data.homepageStats != null || !data.isHomepageLoading, (data) => data.homepageStats != null || !data.isHomepageLoading,
); );
@@ -232,7 +245,35 @@ class _MyHomePageState extends State<MyHomePage> {
? widget.child ? widget.child
: const Center(child: CircularProgressIndicator()); : const Center(child: CircularProgressIndicator());
return Scaffold( return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) async {
if (didPop) return;
final shellNav = _shellNavigatorKey.currentState;
if (shellNav != null && shellNav.canPop()) {
shellNav.pop();
return;
}
if (_tabHistory.isNotEmpty) {
final previousTab = _tabHistory.removeLast();
if (!mounted) return;
_handlingBackNavigation = true;
context.go(contentPages[previousTab]);
return;
}
if (pageIndex != 0) {
if (!mounted) return;
_handlingBackNavigation = true;
context.go(contentPages[0]);
return;
}
SystemNavigator.pop();
},
child: Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text.rich( title: Text.rich(
@@ -267,7 +308,27 @@ class _MyHomePageState extends State<MyHomePage> {
], ],
), ),
body: currentPage, body: currentPage,
),
); );
} }
void _recordTabChange(int pageIndex) {
final last = _lastTabIndex;
if (last == null) {
_lastTabIndex = pageIndex;
return;
}
if (last == pageIndex) return;
if (_handlingBackNavigation) {
_handlingBackNavigation = false;
_lastTabIndex = pageIndex;
return;
} }
if (_tabHistory.isEmpty || _tabHistory.last != last) {
_tabHistory.add(last);
}
_lastTabIndex = pageIndex;
}
}

20
test/app_shell_test.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mileograph_flutter/ui/app_shell.dart';
void main() {
test('tabIndexForPath maps nested routes', () {
expect(tabIndexForPath('/'), 0);
expect(tabIndexForPath('/calculator'), 1);
expect(tabIndexForPath('/calculator/details'), 1);
expect(tabIndexForPath('/legs'), 2);
expect(tabIndexForPath('/traction/12/timeline'), 3);
expect(tabIndexForPath('/trips'), 4);
expect(tabIndexForPath('/add'), 5);
});
test('tabIndexForPath ignores query when parsing uri', () {
expect(tabIndexForPath(Uri.parse('/trips?sort=desc').path), 4);
expect(tabIndexForPath(Uri.parse('/calculator/details?x=1').path), 1);
});
}