fix navbar freezing fix
This commit is contained in:
@@ -45,7 +45,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// 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.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mileograph_flutter/components/calculator/calculator.dart';
|
||||
import 'package:mileograph_flutter/components/pages/traction.dart';
|
||||
|
||||
@@ -10,6 +10,7 @@ class NewEntryPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NewEntryPageState extends State<NewEntryPage> {
|
||||
late final NavigationGuardCallback _exitGuard;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
TimeOfDay _selectedTime = TimeOfDay.now();
|
||||
@@ -40,7 +41,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
@override
|
||||
void 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
|
||||
Future.microtask(() {
|
||||
if (!mounted) return;
|
||||
@@ -58,7 +60,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
NavigationGuard.unregister(_handleExitIntent);
|
||||
NavigationGuard.unregister(_exitGuard);
|
||||
_startController.dispose();
|
||||
_endController.dispose();
|
||||
_headcodeController.dispose();
|
||||
@@ -586,7 +588,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
if (didPop) return;
|
||||
final allow = await _handleExitIntent();
|
||||
if (allow && context.mounted) {
|
||||
Navigator.of(context).maybePop();
|
||||
final router = GoRouter.of(context);
|
||||
if (router.canPop()) {
|
||||
context.pop();
|
||||
} else {
|
||||
context.go('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -597,7 +604,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
onPressed: () async {
|
||||
if (!await _handleExitIntent()) 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'),
|
||||
|
||||
@@ -209,22 +209,46 @@ class DataService extends ChangeNotifier {
|
||||
final target = date ?? DateTime.now();
|
||||
final formatted =
|
||||
"${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 {
|
||||
final json = await api.get('/legs/on-this-day?date=$formatted');
|
||||
if (json is List) {
|
||||
_onThisDay = json.map((e) => Leg.fromJson(e)).toList();
|
||||
} else {
|
||||
_onThisDay = [];
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to fetch on-this-day legs: $e');
|
||||
lastError ??= e;
|
||||
_onThisDay = [];
|
||||
} finally {
|
||||
if (lastError != null) {
|
||||
debugPrint('Failed to fetch on-this-day legs ($endpoint): $lastError');
|
||||
}
|
||||
_isOnThisDayLoading = false;
|
||||
_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 {
|
||||
if (_eventFields.isNotEmpty && !force) return;
|
||||
_isEventFieldsLoading = true;
|
||||
|
||||
@@ -7,9 +7,10 @@ class NavigationGuard {
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
static void unregister(NavigationGuardCallback callback) {
|
||||
if (_callback == callback) {
|
||||
static void unregister([NavigationGuardCallback? callback]) {
|
||||
if (callback == null || identical(_callback, callback)) {
|
||||
_callback = null;
|
||||
_promptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mileograph_flutter/components/login/login.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: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 {
|
||||
const MyApp({super.key});
|
||||
|
||||
@@ -52,6 +75,7 @@ class _MyAppState extends State<MyApp> {
|
||||
},
|
||||
routes: [
|
||||
ShellRoute(
|
||||
navigatorKey: _shellNavigatorKey,
|
||||
builder: (context, state, child) => MyHomePage(child: child),
|
||||
routes: [
|
||||
GoRoute(path: '/', builder: (context, state) => const Dashboard()),
|
||||
@@ -153,26 +177,7 @@ class MyHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
final List<String> 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;
|
||||
}
|
||||
List<String> get contentPages => _contentPages;
|
||||
|
||||
Future<void> _onItemTapped(int index, int currentIndex) async {
|
||||
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;
|
||||
|
||||
@override
|
||||
@@ -221,8 +230,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.toString();
|
||||
final pageIndex = _getIndexFromLocation(location);
|
||||
final uri = GoRouterState.of(context).uri;
|
||||
final pageIndex = tabIndexForPath(uri.path);
|
||||
_recordTabChange(pageIndex);
|
||||
if (pageIndex != _addTabIndex) {
|
||||
NavigationGuard.unregister();
|
||||
}
|
||||
final homepageReady = context.select<DataService, bool>(
|
||||
(data) => data.homepageStats != null || !data.isHomepageLoading,
|
||||
);
|
||||
@@ -232,7 +245,35 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
? widget.child
|
||||
: 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(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text.rich(
|
||||
@@ -267,7 +308,27 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
],
|
||||
),
|
||||
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
20
test/app_shell_test.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user