mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-13 09:12:56 +00:00
Compare commits
3 Commits
v2.1.2-rc
...
v2.1.3-rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6363cabacf | ||
|
|
5312456a15 | ||
|
|
8493679526 |
@@ -242,6 +242,10 @@ Users expect instant response to their actions. By immediately updating the cach
|
||||
- **Orange ring**: Node currently being edited
|
||||
- **Red ring**: Nodes pending deletion
|
||||
|
||||
**Node dimming behavior:**
|
||||
- **Dimmed (50% opacity)**: Non-selected nodes when a specific node is selected for tag viewing, or all nodes during search/navigation modes
|
||||
- **Selection persistence**: When viewing a node's tag sheet, other nodes remain dimmed even when the map is moved, until the sheet is closed (v2.1.3+ fix)
|
||||
|
||||
**Direction cone visual states:**
|
||||
- **Full opacity**: Active session direction (currently being edited)
|
||||
- **Reduced opacity (40%)**: Inactive session directions
|
||||
|
||||
20
README.md
20
README.md
@@ -98,27 +98,25 @@ cp lib/keys.dart.example lib/keys.dart
|
||||
## Roadmap
|
||||
|
||||
### Needed Bugfixes
|
||||
- Clean cache when nodes have been deleted by others
|
||||
- Are offline areas preferred for fast loading even when online? Check working.
|
||||
|
||||
### Current Development
|
||||
- Optional reason message when deleting
|
||||
- Option to import profiles from deflock identify page?
|
||||
- Import/Export map providers, profiles (profiles from deflock identify page?)
|
||||
|
||||
### On Pause
|
||||
- Import/Export map providers, profiles
|
||||
- Clean cache when nodes have been deleted by others
|
||||
- Improve offline area node refresh live display
|
||||
- Offline navigation (pending vector map tiles)
|
||||
|
||||
### Future Features & Wishlist
|
||||
- Update offline area nodes while browsing?
|
||||
- Offline navigation (pending vector map tiles)
|
||||
- Android Auto / CarPlay
|
||||
- Optional reason message when deleting
|
||||
- Update offline area data while browsing?
|
||||
|
||||
### Maybes
|
||||
- Yellow ring for devices missing specific tag details
|
||||
- "Cache accumulating" offline area
|
||||
- "Offline areas" as tile provider
|
||||
- Grab the full latest database for each profile just like for suspected locations (instead of overpass)
|
||||
- Android Auto / CarPlay
|
||||
- "Cache accumulating" offline area?
|
||||
- "Offline areas" as tile provider?
|
||||
- Grab the full latest database for each profile just like for suspected locations (instead of overpass)?
|
||||
- Optional custom icons for profiles to aid identification
|
||||
- Custom device providers and OSM/Overpass alternatives
|
||||
- Offer options for extracting nodes which are attached to a way/relation:
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"2.1.3": {
|
||||
"content": [
|
||||
"• Fixed nodes losing their greyed-out appearance when map is moved while viewing a node's tag sheet",
|
||||
"• Improved GPS location handling - follow-me button is now greyed out when location is unavailable",
|
||||
"• Added approximate location fallback - if precise location is denied, app will use approximate location",
|
||||
"• Higher frequency GPS updates when follow-me modes are active for smoother tracking (1-second updates vs 5-second)"
|
||||
]
|
||||
},
|
||||
"2.1.2": {
|
||||
"content": [
|
||||
"• New positioning tutorial - first-time users must drag the map to refine location when creating or editing nodes, helping ensure accurate positioning",
|
||||
|
||||
@@ -433,16 +433,18 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
IconButton(
|
||||
tooltip: _getFollowMeTooltip(appState.followMeMode),
|
||||
icon: Icon(_getFollowMeIcon(appState.followMeMode)),
|
||||
onPressed: () {
|
||||
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 (newMode != FollowMeMode.off) {
|
||||
_mapViewKey.currentState?.retryLocationInit();
|
||||
}
|
||||
},
|
||||
onPressed: _mapViewKey.currentState?.hasLocation == true
|
||||
? () {
|
||||
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 (newMode != FollowMeMode.off) {
|
||||
_mapViewKey.currentState?.retryLocationInit();
|
||||
}
|
||||
}
|
||||
: null, // Grey out when no location
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: LocalizationService.instance,
|
||||
@@ -490,11 +492,24 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
_isNodeLimitActive = isLimited;
|
||||
});
|
||||
},
|
||||
onLocationStatusChanged: () {
|
||||
// Re-render when location status changes (for follow-me button state)
|
||||
setState(() {});
|
||||
},
|
||||
onUserGesture: () {
|
||||
_mapInteractionHandler.handleUserGesture(
|
||||
context: context,
|
||||
onSelectedNodeChanged: (id) => setState(() => _selectedNodeId = id),
|
||||
);
|
||||
// Only clear selected node if tag sheet is not open
|
||||
// This prevents nodes from losing their grey-out when map is moved while viewing tags
|
||||
if (_sheetCoordinator.tagSheetHeight == 0) {
|
||||
_mapInteractionHandler.handleUserGesture(
|
||||
context: context,
|
||||
onSelectedNodeChanged: (id) => setState(() => _selectedNodeId = id),
|
||||
);
|
||||
} else {
|
||||
// Tag sheet is open - only handle suspected location clearing, not node selection
|
||||
final appState = context.read<AppState>();
|
||||
appState.clearSuspectedLocationSelection();
|
||||
}
|
||||
|
||||
if (appState.followMeMode != FollowMeMode.off) {
|
||||
appState.setFollowMeMode(FollowMeMode.off);
|
||||
}
|
||||
|
||||
@@ -15,29 +15,91 @@ import '../../models/node_profile.dart';
|
||||
class GpsController {
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
LatLng? _currentLatLng;
|
||||
bool _hasLocation = false;
|
||||
Timer? _retryTimer;
|
||||
|
||||
/// Get the current GPS location (if available)
|
||||
LatLng? get currentLocation => _currentLatLng;
|
||||
|
||||
/// Whether we currently have a valid GPS location
|
||||
bool get hasLocation => _hasLocation;
|
||||
|
||||
/// Initialize GPS location tracking
|
||||
Future<void> initializeLocation() async {
|
||||
final perm = await Geolocator.requestPermission();
|
||||
if (perm == LocationPermission.denied ||
|
||||
perm == LocationPermission.deniedForever) {
|
||||
debugPrint('[GpsController] Location permission denied');
|
||||
// Check if location services are enabled first
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[GpsController] Location services disabled');
|
||||
_hasLocation = false;
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
_positionSub = Geolocator.getPositionStream().listen((Position position) {
|
||||
final latLng = LatLng(position.latitude, position.longitude);
|
||||
_currentLatLng = latLng;
|
||||
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude}');
|
||||
});
|
||||
final perm = await Geolocator.requestPermission();
|
||||
debugPrint('[GpsController] Location permission result: $perm');
|
||||
|
||||
if (perm == LocationPermission.denied ||
|
||||
perm == LocationPermission.deniedForever) {
|
||||
debugPrint('[GpsController] Precise location permission denied, trying approximate location');
|
||||
|
||||
// Try approximate location as fallback
|
||||
try {
|
||||
await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.low,
|
||||
timeLimit: const Duration(seconds: 10),
|
||||
);
|
||||
debugPrint('[GpsController] Approximate location available, proceeding with location stream');
|
||||
// If we got here, approximate location works, continue with stream setup below
|
||||
} catch (e) {
|
||||
debugPrint('[GpsController] Approximate location also unavailable: $e');
|
||||
_hasLocation = false;
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
} else if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) {
|
||||
debugPrint('[GpsController] Location permission granted: $perm');
|
||||
// Permission is granted, continue with normal setup
|
||||
} else {
|
||||
debugPrint('[GpsController] Unexpected permission state: $perm');
|
||||
_hasLocation = false;
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
_positionSub?.cancel(); // Cancel any existing subscription
|
||||
debugPrint('[GpsController] Starting GPS position stream');
|
||||
_positionSub = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 5, // Update when moved at least 5 meters (standard frequency)
|
||||
),
|
||||
).listen(
|
||||
(Position position) {
|
||||
final latLng = LatLng(position.latitude, position.longitude);
|
||||
_currentLatLng = latLng;
|
||||
if (!_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location acquired');
|
||||
}
|
||||
_hasLocation = true;
|
||||
_cancelRetry(); // Got location, stop retrying
|
||||
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude} (accuracy: ${position.accuracy}m)');
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('[GpsController] Position stream error: $error');
|
||||
if (_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location lost, starting retry attempts');
|
||||
}
|
||||
_hasLocation = false;
|
||||
_currentLatLng = null;
|
||||
_scheduleRetry(); // Lost location, start retrying
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Retry location initialization (e.g., after permission granted)
|
||||
Future<void> retryLocationInit() async {
|
||||
debugPrint('[GpsController] Retrying location initialization');
|
||||
debugPrint('[GpsController] Manual retry of location initialization');
|
||||
_cancelRetry(); // Cancel automatic retries, this is a manual retry
|
||||
await initializeLocation();
|
||||
}
|
||||
|
||||
@@ -50,6 +112,9 @@ class GpsController {
|
||||
}) {
|
||||
debugPrint('[GpsController] Follow-me mode changed: $oldMode → $newMode');
|
||||
|
||||
// Restart position stream with appropriate frequency for new mode
|
||||
_restartPositionStream(newMode);
|
||||
|
||||
// Only act when follow-me is first enabled and we have a current location
|
||||
if (newMode != FollowMeMode.off &&
|
||||
oldMode == FollowMeMode.off &&
|
||||
@@ -98,6 +163,8 @@ class GpsController {
|
||||
}) {
|
||||
final latLng = LatLng(position.latitude, position.longitude);
|
||||
_currentLatLng = latLng;
|
||||
_hasLocation = true;
|
||||
_cancelRetry(); // Got location, stop any retries
|
||||
|
||||
// Notify that location was updated (for setState, etc.)
|
||||
onLocationUpdated();
|
||||
@@ -169,38 +236,184 @@ class GpsController {
|
||||
VoidCallback? onMapMovedProgrammatically,
|
||||
|
||||
}) async {
|
||||
final perm = await Geolocator.requestPermission();
|
||||
if (perm == LocationPermission.denied ||
|
||||
perm == LocationPermission.deniedForever) {
|
||||
debugPrint('[GpsController] Location permission denied');
|
||||
// Check if location services are enabled first
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[GpsController] Location services disabled');
|
||||
_hasLocation = false;
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
_positionSub = Geolocator.getPositionStream().listen((Position position) {
|
||||
// Get the current follow-me mode from the app state each time
|
||||
final currentFollowMeMode = getCurrentFollowMeMode();
|
||||
final proximityAlertsEnabled = getProximityAlertsEnabled();
|
||||
final proximityAlertDistance = getProximityAlertDistance();
|
||||
final nearbyNodes = getNearbyNodes();
|
||||
final enabledProfiles = getEnabledProfiles();
|
||||
processPositionUpdate(
|
||||
position: position,
|
||||
followMeMode: currentFollowMeMode,
|
||||
controller: controller,
|
||||
onLocationUpdated: onLocationUpdated,
|
||||
proximityAlertsEnabled: proximityAlertsEnabled,
|
||||
proximityAlertDistance: proximityAlertDistance,
|
||||
nearbyNodes: nearbyNodes,
|
||||
enabledProfiles: enabledProfiles,
|
||||
onMapMovedProgrammatically: onMapMovedProgrammatically,
|
||||
);
|
||||
final perm = await Geolocator.requestPermission();
|
||||
debugPrint('[GpsController] Location permission result: $perm');
|
||||
|
||||
if (perm == LocationPermission.denied ||
|
||||
perm == LocationPermission.deniedForever) {
|
||||
debugPrint('[GpsController] Precise location permission denied, trying approximate location');
|
||||
|
||||
// Try approximate location as fallback
|
||||
try {
|
||||
await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.low,
|
||||
timeLimit: const Duration(seconds: 10),
|
||||
);
|
||||
debugPrint('[GpsController] Approximate location available, proceeding with location stream');
|
||||
// If we got here, approximate location works, continue with stream setup below
|
||||
} catch (e) {
|
||||
debugPrint('[GpsController] Approximate location also unavailable: $e');
|
||||
_hasLocation = false;
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
} else if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) {
|
||||
debugPrint('[GpsController] Location permission granted: $perm');
|
||||
// Permission is granted, continue with normal setup
|
||||
} else {
|
||||
debugPrint('[GpsController] Unexpected permission state: $perm');
|
||||
_hasLocation = false;
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
_positionSub?.cancel(); // Cancel any existing subscription
|
||||
debugPrint('[GpsController] Starting GPS position stream');
|
||||
_positionSub = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 5, // Update when moved at least 5 meters (standard frequency)
|
||||
),
|
||||
).listen(
|
||||
(Position position) {
|
||||
if (!_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location acquired');
|
||||
}
|
||||
_hasLocation = true;
|
||||
_cancelRetry(); // Got location, stop retrying
|
||||
|
||||
// Get the current follow-me mode from the app state each time
|
||||
final currentFollowMeMode = getCurrentFollowMeMode();
|
||||
final proximityAlertsEnabled = getProximityAlertsEnabled();
|
||||
final proximityAlertDistance = getProximityAlertDistance();
|
||||
final nearbyNodes = getNearbyNodes();
|
||||
final enabledProfiles = getEnabledProfiles();
|
||||
processPositionUpdate(
|
||||
position: position,
|
||||
followMeMode: currentFollowMeMode,
|
||||
controller: controller,
|
||||
onLocationUpdated: onLocationUpdated,
|
||||
proximityAlertsEnabled: proximityAlertsEnabled,
|
||||
proximityAlertDistance: proximityAlertDistance,
|
||||
nearbyNodes: nearbyNodes,
|
||||
enabledProfiles: enabledProfiles,
|
||||
onMapMovedProgrammatically: onMapMovedProgrammatically,
|
||||
);
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('[GpsController] Position stream error: $error');
|
||||
if (_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location lost, starting retry attempts');
|
||||
}
|
||||
_hasLocation = false;
|
||||
_currentLatLng = null;
|
||||
onLocationUpdated(); // Notify UI that location was lost
|
||||
_scheduleRetry(); // Lost location, start retrying
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Schedule periodic retry attempts to get location
|
||||
void _scheduleRetry() {
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
|
||||
debugPrint('[GpsController] Automatic retry of location initialization (attempt ${timer.tick})');
|
||||
initializeLocation(); // This will cancel the timer if successful
|
||||
});
|
||||
}
|
||||
|
||||
/// Cancel any scheduled retry attempts
|
||||
void _cancelRetry() {
|
||||
if (_retryTimer != null) {
|
||||
debugPrint('[GpsController] Canceling location retry timer');
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart position stream with frequency optimized for follow-me mode
|
||||
void _restartPositionStream(FollowMeMode followMeMode) {
|
||||
if (_positionSub == null || !_hasLocation) {
|
||||
// No active stream or no location - let normal initialization handle it
|
||||
return;
|
||||
}
|
||||
|
||||
_positionSub?.cancel();
|
||||
|
||||
// Use higher frequency when follow-me is enabled
|
||||
if (followMeMode != FollowMeMode.off) {
|
||||
debugPrint('[GpsController] Starting high-frequency GPS updates for follow-me mode');
|
||||
_positionSub = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 1, // Update when moved at least 1 meter
|
||||
),
|
||||
).listen(
|
||||
(Position position) {
|
||||
final latLng = LatLng(position.latitude, position.longitude);
|
||||
_currentLatLng = latLng;
|
||||
if (!_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location acquired');
|
||||
}
|
||||
_hasLocation = true;
|
||||
_cancelRetry(); // Got location, stop retrying
|
||||
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude} (accuracy: ${position.accuracy}m)');
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('[GpsController] Position stream error: $error');
|
||||
if (_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location lost, starting retry attempts');
|
||||
}
|
||||
_hasLocation = false;
|
||||
_currentLatLng = null;
|
||||
_scheduleRetry(); // Lost location, start retrying
|
||||
},
|
||||
);
|
||||
} else {
|
||||
debugPrint('[GpsController] Starting standard-frequency GPS updates');
|
||||
_positionSub = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 5, // Update when moved at least 5 meters
|
||||
),
|
||||
).listen(
|
||||
(Position position) {
|
||||
final latLng = LatLng(position.latitude, position.longitude);
|
||||
_currentLatLng = latLng;
|
||||
if (!_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location acquired');
|
||||
}
|
||||
_hasLocation = true;
|
||||
_cancelRetry(); // Got location, stop retrying
|
||||
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude} (accuracy: ${position.accuracy}m)');
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('[GpsController] Position stream error: $error');
|
||||
if (_hasLocation) {
|
||||
debugPrint('[GpsController] GPS location lost, starting retry attempts');
|
||||
}
|
||||
_hasLocation = false;
|
||||
_currentLatLng = null;
|
||||
_scheduleRetry(); // Lost location, start retrying
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispose of GPS resources
|
||||
void dispose() {
|
||||
_positionSub?.cancel();
|
||||
_positionSub = null;
|
||||
_cancelRetry();
|
||||
debugPrint('[GpsController] GPS controller disposed');
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class MapView extends StatefulWidget {
|
||||
this.onSuspectedLocationTap,
|
||||
this.onSearchPressed,
|
||||
this.onNodeLimitChanged,
|
||||
this.onLocationStatusChanged,
|
||||
});
|
||||
|
||||
final FollowMeMode followMeMode;
|
||||
@@ -54,6 +55,7 @@ class MapView extends StatefulWidget {
|
||||
final void Function(SuspectedLocation)? onSuspectedLocationTap;
|
||||
final VoidCallback? onSearchPressed;
|
||||
final void Function(bool isLimited)? onNodeLimitChanged;
|
||||
final VoidCallback? onLocationStatusChanged;
|
||||
|
||||
@override
|
||||
State<MapView> createState() => MapViewState();
|
||||
@@ -121,7 +123,10 @@ class MapViewState extends State<MapView> {
|
||||
_gpsController.initializeWithCallback(
|
||||
followMeMode: widget.followMeMode,
|
||||
controller: _controller,
|
||||
onLocationUpdated: () => setState(() {}),
|
||||
onLocationUpdated: () {
|
||||
setState(() {});
|
||||
widget.onLocationStatusChanged?.call(); // Notify parent about location status change
|
||||
},
|
||||
getCurrentFollowMeMode: () {
|
||||
// Use mounted check to avoid calling context when widget is disposed
|
||||
if (mounted) {
|
||||
@@ -230,6 +235,9 @@ class MapViewState extends State<MapView> {
|
||||
LatLng? getUserLocation() {
|
||||
return _gpsController.currentLocation;
|
||||
}
|
||||
|
||||
/// Whether we currently have a valid GPS location
|
||||
bool get hasLocation => _gpsController.hasLocation;
|
||||
|
||||
/// Expose static methods from MapPositionManager for external access
|
||||
static Future<void> clearStoredMapPosition() =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: deflockapp
|
||||
description: Map public surveillance infrastructure with OpenStreetMap
|
||||
publish_to: "none"
|
||||
version: 2.1.2+35 # The thing after the + is the version code, incremented with each release
|
||||
version: 2.1.3+36 # The thing after the + is the version code, incremented with each release
|
||||
|
||||
environment:
|
||||
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
|
||||
|
||||
Reference in New Issue
Block a user