mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
refactor follow me mode state handling
This commit is contained in:
@@ -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<TileProvider> get tileProviders => _settingsState.tileProviders;
|
||||
@@ -230,7 +231,10 @@ class AppState extends ChangeNotifier {
|
||||
await _settingsState.deleteTileProvider(providerId);
|
||||
}
|
||||
|
||||
|
||||
/// Set follow-me mode
|
||||
Future<void> setFollowMeMode(FollowMeMode mode) async {
|
||||
await _settingsState.setFollowMeMode(mode);
|
||||
}
|
||||
|
||||
// ---------- Queue Methods ----------
|
||||
void clearQueue() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HomeScreen> with TickerProviderStateMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<MapViewState> _mapViewKey = GlobalKey<MapViewState>();
|
||||
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<void> _loadFollowMeMode() async {
|
||||
final savedMode = await MapViewState.loadFollowMeMode();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_followMeMode = savedMode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -56,8 +37,8 @@ class _HomeScreenState extends State<HomeScreen> 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<HomeScreen> 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<HomeScreen> 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<HomeScreen> 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<AppState>();
|
||||
// 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<HomeScreen> 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<AppState>();
|
||||
// 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<HomeScreen> 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<HomeScreen> 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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<TileProvider> _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<TileProvider> 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<void> setFollowMeMode(FollowMeMode mode) async {
|
||||
if (_followMeMode != mode) {
|
||||
_followMeMode = mode;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_followMeModePrefsKey, mode.index);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Position>? _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<void> 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,
|
||||
);
|
||||
|
||||
@@ -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<void> 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<FollowMeMode> 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<void> clearStoredMapPosition() async {
|
||||
|
||||
@@ -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<MapView> {
|
||||
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<AppState>().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<MapView> {
|
||||
}
|
||||
|
||||
/// Expose static methods from MapPositionManager for external access
|
||||
static Future<void> saveFollowMeMode(FollowMeMode mode) =>
|
||||
MapPositionManager.saveFollowMeMode(mode);
|
||||
|
||||
static Future<FollowMeMode> loadFollowMeMode() =>
|
||||
MapPositionManager.loadFollowMeMode();
|
||||
|
||||
static Future<void> clearStoredMapPosition() =>
|
||||
MapPositionManager.clearStoredMapPosition();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user