import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'tracker_storage.dart'; part 'last_location.freezed.dart'; part 'last_location.g.dart'; /// The deepest route the user was viewing when the app was last active. /// /// Encoded via [toRouteName] into [Route.settings.name] so a single /// [NavigatorObserver] can track it without every screen needing to know /// about persistence. @freezed class LastLocation with _$LastLocation { const LastLocation._(); const factory LastLocation({required String trackerId, int? bookIndex}) = _LastLocation; factory LastLocation.fromJson(Map json) => _$LastLocationFromJson(json); static const String _trackerPrefix = '/tracker/'; String toRouteName() => bookIndex == null ? '$_trackerPrefix$trackerId' : '$_trackerPrefix$trackerId/book/$bookIndex'; /// Parses a route name such as `/tracker/` or /// `/tracker//book/` back into a [LastLocation]. /// /// Returns null for the root `/` route, unnamed routes (modals, dialogs), /// and malformed names. static LastLocation? tryParseRoute(String? name) { if (name == null) return null; final List parts = name .split('/') .where((String s) => s.isNotEmpty) .toList(growable: false); if (parts.isEmpty || parts.first != 'tracker') return null; if (parts.length == 2) return LastLocation(trackerId: parts[1]); if (parts.length == 4 && parts[2] == 'book') { final int? idx = int.tryParse(parts[3]); if (idx != null) { return LastLocation(trackerId: parts[1], bookIndex: idx); } } return null; } } /// Reads and writes the most recent [LastLocation] to [SharedPreferences]. class LastLocationService { LastLocationService(this._prefs); static const String _key = 'bible_tracker.last_location.v1'; final SharedPreferences _prefs; LastLocation? load() { final String? raw = _prefs.getString(_key); if (raw == null || raw.isEmpty) return null; try { return LastLocation.fromJson(jsonDecode(raw) as Map); } catch (_) { return null; } } Future save(LastLocation? location) async { if (location == null) { await _prefs.remove(_key); return; } await _prefs.setString(_key, jsonEncode(location.toJson())); } } final Provider lastLocationServiceProvider = Provider( (Ref ref) => LastLocationService(ref.read(sharedPreferencesProvider)), ); /// Populated at startup in `main.dart` with the location that was persisted /// during the previous session. Reading it once avoids a race where the root /// navigator's initial `didPush` clears the saved value before the UI has /// had a chance to restore it. final Provider pendingRestoreProvider = Provider( (Ref ref) => null, ); /// Writes the top-most named route to [_onChange] whenever navigation /// changes. Unnamed routes (bottom sheets, dialogs, the create-tracker page) /// are ignored so they don't clobber the underlying tracker/book location. class LastLocationObserver extends NavigatorObserver { LastLocationObserver(this._onChange); final void Function(LastLocation? location) _onChange; @override void didPush(Route route, Route? previousRoute) { _notify(route); } @override void didPop(Route route, Route? previousRoute) { _notify(previousRoute); } @override void didReplace({Route? newRoute, Route? oldRoute}) { _notify(newRoute); } @override void didRemove(Route route, Route? previousRoute) { _notify(previousRoute); } void _notify(Route? route) { final String? name = route?.settings.name; if (name == null) return; if (name == '/') { _onChange(null); return; } final LastLocation? parsed = LastLocation.tryParseRoute(name); if (parsed != null) _onChange(parsed); } }