diff --git a/lib/app_state.dart b/lib/app_state.dart index 4a5c4fd..c8d5cdb 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -14,7 +14,7 @@ import 'state/settings_state.dart'; import 'state/upload_queue_state.dart'; // Re-export types -export 'state/settings_state.dart' show UploadMode; +export 'state/settings_state.dart' show UploadMode, FollowMeMode; export 'state/session_state.dart' show AddCameraSession, EditCameraSession; // ------------------ AppState ------------------ @@ -68,6 +68,7 @@ class AppState extends ChangeNotifier { bool get offlineMode => _settingsState.offlineMode; int get maxCameras => _settingsState.maxCameras; UploadMode get uploadMode => _settingsState.uploadMode; + FollowMeMode get followMeMode => _settingsState.followMeMode; // Tile provider state List get tileProviders => _settingsState.tileProviders; @@ -230,7 +231,10 @@ class AppState extends ChangeNotifier { await _settingsState.deleteTileProvider(providerId); } - + /// Set follow-me mode + Future setFollowMeMode(FollowMeMode mode) async { + await _settingsState.setFollowMeMode(mode); + } // ---------- Queue Methods ---------- void clearQueue() { diff --git a/lib/dev_config.dart b/lib/dev_config.dart index b866df0..2f81a60 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -41,7 +41,6 @@ const double kMinSpeedForRotationMps = 1.0; // Minimum speed (m/s) to apply rota const String kLastMapLatKey = 'last_map_latitude'; const String kLastMapLngKey = 'last_map_longitude'; const String kLastMapZoomKey = 'last_map_zoom'; -const String kFollowMeModeKey = 'follow_me_mode'; // Tile/OSM fetch retry parameters (for tunable backoff) const int kTileFetchMaxAttempts = 3; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index bf1c674..d8d70ff 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -12,12 +12,6 @@ import '../widgets/edit_camera_sheet.dart'; import '../widgets/camera_provider_with_cache.dart'; import '../widgets/download_area_dialog.dart'; -enum FollowMeMode { - off, // No following - northUp, // Follow position, keep north up - rotating, // Follow position and rotation -} - class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -29,25 +23,12 @@ class _HomeScreenState extends State with TickerProviderStateMixin { final GlobalKey _scaffoldKey = GlobalKey(); final GlobalKey _mapViewKey = GlobalKey(); late final AnimatedMapController _mapController; - FollowMeMode _followMeMode = FollowMeMode.northUp; bool _editSheetShown = false; @override void initState() { super.initState(); _mapController = AnimatedMapController(vsync: this); - // Load saved follow-me mode - _loadFollowMeMode(); - } - - /// Load the saved follow-me mode - Future _loadFollowMeMode() async { - final savedMode = await MapViewState.loadFollowMeMode(); - if (mounted) { - setState(() { - _followMeMode = savedMode; - }); - } } @override @@ -56,8 +37,8 @@ class _HomeScreenState extends State with TickerProviderStateMixin { super.dispose(); } - String _getFollowMeTooltip() { - switch (_followMeMode) { + String _getFollowMeTooltip(FollowMeMode mode) { + switch (mode) { case FollowMeMode.off: return 'Enable follow-me (north up)'; case FollowMeMode.northUp: @@ -67,8 +48,8 @@ class _HomeScreenState extends State with TickerProviderStateMixin { } } - IconData _getFollowMeIcon() { - switch (_followMeMode) { + IconData _getFollowMeIcon(FollowMeMode mode) { + switch (mode) { case FollowMeMode.off: return Icons.gps_off; case FollowMeMode.northUp: @@ -78,8 +59,8 @@ class _HomeScreenState extends State with TickerProviderStateMixin { } } - FollowMeMode _getNextFollowMeMode() { - switch (_followMeMode) { + FollowMeMode _getNextFollowMeMode(FollowMeMode mode) { + switch (mode) { case FollowMeMode.off: return FollowMeMode.northUp; case FollowMeMode.northUp: @@ -90,12 +71,10 @@ class _HomeScreenState extends State with TickerProviderStateMixin { } void _openAddCameraSheet() { - // Disable follow-me when adding a camera so the map doesn't jump around - setState(() => _followMeMode = FollowMeMode.off); - // Save the disabled follow-me mode - MapViewState.saveFollowMeMode(_followMeMode); - final appState = context.read(); + // Disable follow-me when adding a camera so the map doesn't jump around + appState.setFollowMeMode(FollowMeMode.off); + appState.startAddSession(); final session = appState.session!; // guaranteed non‑null now @@ -105,12 +84,10 @@ class _HomeScreenState extends State with TickerProviderStateMixin { } void _openEditCameraSheet() { - // Disable follow-me when editing a camera so the map doesn't jump around - setState(() => _followMeMode = FollowMeMode.off); - // Save the disabled follow-me mode - MapViewState.saveFollowMeMode(_followMeMode); - final appState = context.read(); + // Disable follow-me when editing a camera so the map doesn't jump around + appState.setFollowMeMode(FollowMeMode.off); + final session = appState.editSession!; // should be non-null when this is called _scaffoldKey.currentState!.showBottomSheet( @@ -140,18 +117,15 @@ class _HomeScreenState extends State with TickerProviderStateMixin { title: const Text('Flock Map'), actions: [ IconButton( - tooltip: _getFollowMeTooltip(), - icon: Icon(_getFollowMeIcon()), + tooltip: _getFollowMeTooltip(appState.followMeMode), + icon: Icon(_getFollowMeIcon(appState.followMeMode)), onPressed: () { - setState(() { - final oldMode = _followMeMode; - _followMeMode = _getNextFollowMeMode(); - debugPrint('[HomeScreen] Follow mode changed: $oldMode → $_followMeMode'); - }); - // Save the new follow-me mode - MapViewState.saveFollowMeMode(_followMeMode); + final oldMode = appState.followMeMode; + final newMode = _getNextFollowMeMode(oldMode); + debugPrint('[HomeScreen] Follow mode changed: $oldMode → $newMode'); + appState.setFollowMeMode(newMode); // If enabling follow-me, retry location init in case permission was granted - if (_followMeMode != FollowMeMode.off) { + if (newMode != FollowMeMode.off) { _mapViewKey.currentState?.retryLocationInit(); } }, @@ -167,12 +141,10 @@ class _HomeScreenState extends State with TickerProviderStateMixin { MapView( key: _mapViewKey, controller: _mapController, - followMeMode: _followMeMode, + followMeMode: appState.followMeMode, onUserGesture: () { - if (_followMeMode != FollowMeMode.off) { - setState(() => _followMeMode = FollowMeMode.off); - // Save the disabled follow-me mode when user interacts with map - MapViewState.saveFollowMeMode(_followMeMode); + if (appState.followMeMode != FollowMeMode.off) { + appState.setFollowMeMode(FollowMeMode.off); } }, ), diff --git a/lib/state/settings_state.dart b/lib/state/settings_state.dart index 61e9d1f..08f786c 100644 --- a/lib/state/settings_state.dart +++ b/lib/state/settings_state.dart @@ -8,6 +8,13 @@ import '../models/tile_provider.dart'; // Enum for upload mode (Production, OSM Sandbox, Simulate) enum UploadMode { production, sandbox, simulate } +// Enum for follow-me mode (moved from HomeScreen to centralized state) +enum FollowMeMode { + off, // No following + northUp, // Follow position, keep north up + rotating, // Follow position and rotation +} + class SettingsState extends ChangeNotifier { static const String _offlineModePrefsKey = 'offline_mode'; static const String _maxCamerasPrefsKey = 'max_cameras'; @@ -15,10 +22,12 @@ class SettingsState extends ChangeNotifier { static const String _tileProvidersPrefsKey = 'tile_providers'; static const String _selectedTileTypePrefsKey = 'selected_tile_type'; static const String _legacyTestModePrefsKey = 'test_mode'; + static const String _followMeModePrefsKey = 'follow_me_mode'; bool _offlineMode = false; int _maxCameras = 250; UploadMode _uploadMode = UploadMode.simulate; + FollowMeMode _followMeMode = FollowMeMode.northUp; List _tileProviders = []; String _selectedTileTypeId = ''; @@ -26,6 +35,7 @@ class SettingsState extends ChangeNotifier { bool get offlineMode => _offlineMode; int get maxCameras => _maxCameras; UploadMode get uploadMode => _uploadMode; + FollowMeMode get followMeMode => _followMeMode; List get tileProviders => List.unmodifiable(_tileProviders); String get selectedTileTypeId => _selectedTileTypeId; @@ -91,6 +101,14 @@ class SettingsState extends ChangeNotifier { // Load tile providers (default to built-in providers if none saved) await _loadTileProviders(prefs); + // Load follow-me mode + if (prefs.containsKey(_followMeModePrefsKey)) { + final modeIndex = prefs.getInt(_followMeModePrefsKey) ?? 0; + if (modeIndex >= 0 && modeIndex < FollowMeMode.values.length) { + _followMeMode = FollowMeMode.values[modeIndex]; + } + } + // Load selected tile type (default to first available) _selectedTileTypeId = prefs.getString(_selectedTileTypePrefsKey) ?? ''; if (_selectedTileTypeId.isEmpty || selectedTileType == null) { @@ -211,5 +229,14 @@ class SettingsState extends ChangeNotifier { notifyListeners(); } + /// Set follow-me mode + Future setFollowMeMode(FollowMeMode mode) async { + if (_followMeMode != mode) { + _followMeMode = mode; + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_followMeModePrefsKey, mode.index); + notifyListeners(); + } + } } \ No newline at end of file diff --git a/lib/widgets/map/gps_controller.dart b/lib/widgets/map/gps_controller.dart index 82352cf..c8a3033 100644 --- a/lib/widgets/map/gps_controller.dart +++ b/lib/widgets/map/gps_controller.dart @@ -5,20 +5,16 @@ import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import '../../dev_config.dart'; -import '../../screens/home_screen.dart' show FollowMeMode; +import '../../app_state.dart' show FollowMeMode; /// Manages GPS location tracking, follow-me modes, and location-based map animations. /// Handles GPS permissions, position streams, and follow-me behavior. class GpsController { StreamSubscription? _positionSub; LatLng? _currentLatLng; - FollowMeMode _currentFollowMeMode = FollowMeMode.off; /// Get the current GPS location (if available) LatLng? get currentLocation => _currentLatLng; - - /// Get the current follow-me mode - FollowMeMode get currentFollowMeMode => _currentFollowMeMode; /// Initialize GPS location tracking Future initializeLocation() async { @@ -48,8 +44,6 @@ class GpsController { required FollowMeMode oldMode, required AnimatedMapController controller, }) { - // Update the stored follow-me mode - _currentFollowMeMode = newMode; debugPrint('[GpsController] Follow-me mode changed: $oldMode → $newMode'); // Only act when follow-me is first enabled and we have a current location @@ -84,6 +78,7 @@ class GpsController { /// Process GPS position updates and handle follow-me animations void processPositionUpdate({ required Position position, + required FollowMeMode followMeMode, required AnimatedMapController controller, required VoidCallback onLocationUpdated, }) { @@ -93,12 +88,12 @@ class GpsController { // Notify that location was updated (for setState, etc.) onLocationUpdated(); - // Handle follow-me animations if enabled - use current stored mode, not parameter - if (_currentFollowMeMode != FollowMeMode.off) { - debugPrint('[GpsController] GPS position update: ${latLng.latitude}, ${latLng.longitude}, follow-me: $_currentFollowMeMode'); + // Handle follow-me animations if enabled - use current mode from app state + if (followMeMode != FollowMeMode.off) { + debugPrint('[GpsController] GPS position update: ${latLng.latitude}, ${latLng.longitude}, follow-me: $followMeMode'); WidgetsBinding.instance.addPostFrameCallback((_) { try { - if (_currentFollowMeMode == FollowMeMode.northUp) { + if (followMeMode == FollowMeMode.northUp) { // Follow position only, keep current rotation controller.animateTo( dest: latLng, @@ -106,7 +101,7 @@ class GpsController { duration: kFollowMeAnimationDuration, curve: Curves.easeOut, ); - } else if (_currentFollowMeMode == FollowMeMode.rotating) { + } else if (followMeMode == FollowMeMode.rotating) { // Follow position and rotation based on heading final heading = position.heading; final speed = position.speed; // Speed in m/s @@ -135,10 +130,8 @@ class GpsController { required FollowMeMode followMeMode, required AnimatedMapController controller, required VoidCallback onLocationUpdated, + required FollowMeMode Function() getCurrentFollowMeMode, }) async { - // Store the initial follow-me mode - _currentFollowMeMode = followMeMode; - final perm = await Geolocator.requestPermission(); if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) { @@ -147,8 +140,11 @@ class GpsController { } _positionSub = Geolocator.getPositionStream().listen((Position position) { + // Get the current follow-me mode from the app state each time + final currentFollowMeMode = getCurrentFollowMeMode(); processPositionUpdate( position: position, + followMeMode: currentFollowMeMode, controller: controller, onLocationUpdated: onLocationUpdated, ); diff --git a/lib/widgets/map/map_position_manager.dart b/lib/widgets/map/map_position_manager.dart index 53f2b08..1ed5032 100644 --- a/lib/widgets/map/map_position_manager.dart +++ b/lib/widgets/map/map_position_manager.dart @@ -4,9 +4,9 @@ import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../dev_config.dart'; -import '../../screens/home_screen.dart' show FollowMeMode; -/// Manages map position persistence, initial positioning, and follow-me mode storage. + +/// Manages map position persistence and initial positioning. /// Handles saving/loading last map position and moving to initial locations. class MapPositionManager { LatLng? _initialLocation; @@ -89,33 +89,7 @@ class MapPositionManager { } } - /// Save the follow-me mode to persistent storage - static Future saveFollowMeMode(FollowMeMode mode) async { - try { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(kFollowMeModeKey, mode.index); - debugPrint('[MapPositionManager] Saved follow-me mode: $mode'); - } catch (e) { - debugPrint('[MapPositionManager] Failed to save follow-me mode: $e'); - } - } - /// Load the follow-me mode from persistent storage - static Future loadFollowMeMode() async { - try { - final prefs = await SharedPreferences.getInstance(); - final modeIndex = prefs.getInt(kFollowMeModeKey); - if (modeIndex != null && modeIndex < FollowMeMode.values.length) { - final mode = FollowMeMode.values[modeIndex]; - debugPrint('[MapPositionManager] Loaded follow-me mode: $mode'); - return mode; - } - } catch (e) { - debugPrint('[MapPositionManager] Failed to load follow-me mode: $e'); - } - // Default to northUp if no saved mode - return FollowMeMode.northUp; - } /// Clear any stored map position (useful for recovery from invalid data) static Future clearStoredMapPosition() async { diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 3e02157..9a4fee0 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -21,7 +21,7 @@ import 'map/camera_refresh_controller.dart'; import 'map/gps_controller.dart'; import 'network_status_indicator.dart'; import '../dev_config.dart'; -import '../screens/home_screen.dart' show FollowMeMode; +import '../app_state.dart' show FollowMeMode; class MapView extends StatefulWidget { final AnimatedMapController controller; @@ -78,6 +78,18 @@ class MapViewState extends State { followMeMode: widget.followMeMode, controller: _controller, onLocationUpdated: () => setState(() {}), + getCurrentFollowMeMode: () { + // Use mounted check to avoid calling context when widget is disposed + if (mounted) { + try { + return context.read().followMeMode; + } catch (e) { + debugPrint('[MapView] Could not read AppState, defaulting to off: $e'); + return FollowMeMode.off; + } + } + return FollowMeMode.off; + }, ); // Fetch initial cameras @@ -111,12 +123,6 @@ class MapViewState extends State { } /// Expose static methods from MapPositionManager for external access - static Future saveFollowMeMode(FollowMeMode mode) => - MapPositionManager.saveFollowMeMode(mode); - - static Future loadFollowMeMode() => - MapPositionManager.loadFollowMeMode(); - static Future clearStoredMapPosition() => MapPositionManager.clearStoredMapPosition();