From 2cf840e74d8990360d32f671ffb0e74e322a5289 Mon Sep 17 00:00:00 2001 From: stopflock Date: Thu, 13 Nov 2025 13:22:46 -0600 Subject: [PATCH] Improvements to suspected locations --- assets/changelog.json | 2 +- lib/app_state.dart | 6 +- .../settings/sections/max_nodes_section.dart | 2 +- .../sections/proximity_alerts_section.dart | 2 +- .../sections/suspected_locations_section.dart | 35 ++++------- lib/services/suspected_location_cache.dart | 10 ++-- lib/services/suspected_location_service.dart | 60 +++++++------------ lib/state/suspected_location_state.dart | 30 ++++++++-- 8 files changed, 66 insertions(+), 81 deletions(-) diff --git a/assets/changelog.json b/assets/changelog.json index 48c5ba8..28a1e3d 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,6 +1,6 @@ { "1.3.1": { - "content": "• UX: Network status indicator is now always enabled by default\n• UX: Direction control buttons now use configurable dimensions\n• INTERNAL: Cleaned up advanced settings - removed redundant network status toggle" + "content": "• UX: Network status indicator is now always enabled by default\n• UX: Direction control buttons now use configurable dimensions\n• UX: Fixed iOS keyboard missing 'Done' button on integer input fields\n• UX: Direction control buttons now always visible but greyed out when not needed\n• UX: Fixed multi-direction nodes showing all directions in upload queue\n• UX: Improved suspected locations loading indicator - removed popup, fixed stuck spinner\n• INTERNAL: Cleaned up advanced settings - removed redundant network status toggle" }, "1.2.8": { "content": "• UX: Profile selection is now a required step to prevent accidental submission of default profile.\n• NEW: Note in welcome message about not submitting data you cannot vouch for personally (no street view etc)\n• NEW: Added default operator profiles for the most common private operators nationwide (Lowe's, Home Depot, et al)\n• NEW: Support for cardinal directions in OSM data, multiple directions on a node." diff --git a/lib/app_state.dart b/lib/app_state.dart index b69f211..6f09473 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -485,10 +485,8 @@ class AppState extends ChangeNotifier { await _suspectedLocationState.setEnabled(enabled); } - Future refreshSuspectedLocations({ - void Function(String message, double? progress)? onProgress, - }) async { - return await _suspectedLocationState.refreshData(onProgress: onProgress); + Future refreshSuspectedLocations() async { + return await _suspectedLocationState.refreshData(); } void selectSuspectedLocation(SuspectedLocation location) { diff --git a/lib/screens/settings/sections/max_nodes_section.dart b/lib/screens/settings/sections/max_nodes_section.dart index 5266caa..3788d23 100644 --- a/lib/screens/settings/sections/max_nodes_section.dart +++ b/lib/screens/settings/sections/max_nodes_section.dart @@ -70,7 +70,7 @@ class _MaxNodesSectionState extends State { width: 80, child: TextFormField( controller: _controller, - keyboardType: TextInputType.number, + keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true), textInputAction: TextInputAction.done, decoration: const InputDecoration( isDense: true, diff --git a/lib/screens/settings/sections/proximity_alerts_section.dart b/lib/screens/settings/sections/proximity_alerts_section.dart index f30704b..2994ce2 100644 --- a/lib/screens/settings/sections/proximity_alerts_section.dart +++ b/lib/screens/settings/sections/proximity_alerts_section.dart @@ -181,7 +181,7 @@ class _ProximityAlertsSectionState extends State { width: 80, child: TextField( controller: _distanceController, - keyboardType: TextInputType.number, + keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true), textInputAction: TextInputAction.done, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, diff --git a/lib/screens/settings/sections/suspected_locations_section.dart b/lib/screens/settings/sections/suspected_locations_section.dart index 690ee43..553b6ae 100644 --- a/lib/screens/settings/sections/suspected_locations_section.dart +++ b/lib/screens/settings/sections/suspected_locations_section.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../app_state.dart'; import '../../../services/localization_service.dart'; -import '../../../widgets/suspected_location_progress_dialog.dart'; class SuspectedLocationsSection extends StatelessWidget { const SuspectedLocationsSection({super.key}); @@ -39,31 +38,19 @@ class SuspectedLocationsSection extends StatelessWidget { Future handleRefresh() async { if (!context.mounted) return; - // Show simple progress dialog - showDialog( - context: context, - barrierDismissible: false, - builder: (progressContext) => SuspectedLocationProgressDialog( - title: locService.t('suspectedLocations.updating'), - message: locService.t('suspectedLocations.downloadingAndProcessing'), - ), - ); - - // Start the refresh + // Use the inline loading indicator by calling refreshSuspectedLocations + // The loading state will be managed by suspected location state final success = await appState.refreshSuspectedLocations(); - // Close progress dialog + // Show result snackbar if (context.mounted) { - Navigator.of(context).pop(); - - // Show result snackbar - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? locService.t('suspectedLocations.updateSuccess') - : locService.t('suspectedLocations.updateFailed')), - ), - ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success + ? locService.t('suspectedLocations.updateSuccess') + : locService.t('suspectedLocations.updateFailed')), + ), + ); } } @@ -139,7 +126,7 @@ class SuspectedLocationsSection extends StatelessWidget { width: 80, child: TextFormField( initialValue: appState.suspectedLocationMinDistance.toString(), - keyboardType: TextInputType.number, + keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true), textInputAction: TextInputAction.done, decoration: const InputDecoration( isDense: true, diff --git a/lib/services/suspected_location_cache.dart b/lib/services/suspected_location_cache.dart index d728a41..0333795 100644 --- a/lib/services/suspected_location_cache.dart +++ b/lib/services/suspected_location_cache.dart @@ -127,9 +127,8 @@ class SuspectedLocationCache extends ChangeNotifier { /// Process raw CSV data and save to storage (calculates centroids once) Future processAndSave( List> rawData, - DateTime fetchTime, { - void Function(String message, double? progress)? onProgress, - }) async { + DateTime fetchTime, + ) async { try { debugPrint('[SuspectedLocationCache] Processing ${rawData.length} raw entries...'); @@ -141,10 +140,9 @@ class SuspectedLocationCache extends ChangeNotifier { for (int i = 0; i < rawData.length; i++) { final rowData = rawData[i]; - // Report progress every 1000 entries + // Log progress every 1000 entries for debugging if (i % 1000 == 0) { - final progress = i / rawData.length; - onProgress?.call('Calculating coordinates: ${i + 1}/${rawData.length}', progress); + debugPrint('[SuspectedLocationCache] Processed ${i + 1}/${rawData.length} entries...'); } try { diff --git a/lib/services/suspected_location_service.dart b/lib/services/suspected_location_service.dart index 6935749..e7fe269 100644 --- a/lib/services/suspected_location_service.dart +++ b/lib/services/suspected_location_service.dart @@ -22,7 +22,6 @@ class SuspectedLocationService { final SuspectedLocationCache _cache = SuspectedLocationCache(); bool _isEnabled = false; - bool _isLoading = false; /// Get last fetch time DateTime? get lastFetchTime => _cache.lastFetchTime; @@ -30,9 +29,6 @@ class SuspectedLocationService { /// Check if suspected locations are enabled bool get isEnabled => _isEnabled; - /// Check if currently loading - bool get isLoading => _isLoading; - /// Initialize the service - load from storage and check if refresh needed Future init({bool offlineMode = false}) async { await _loadFromStorage(); @@ -55,22 +51,31 @@ class SuspectedLocationService { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_prefsKeyEnabled, enabled); - // If enabling for the first time and no data, fetch it in background - if (enabled && !_cache.hasData) { - _fetchData(); // Don't await - let it run in background so UI updates immediately - } - // If disabling, clear the cache if (!enabled) { _cache.clear(); } + // Note: If enabling and no data, the state layer will call fetchDataIfNeeded() } - /// Manually refresh the data - Future refreshData({ - void Function(String message, double? progress)? onProgress, - }) async { - return await _fetchData(onProgress: onProgress); + /// Check if cache has any data + bool get hasData => _cache.hasData; + + /// Get last fetch time + DateTime? get lastFetch => _cache.lastFetchTime; + + /// Fetch data if needed (for enabling suspected locations when no data exists) + Future fetchDataIfNeeded() async { + if (!_shouldRefresh()) { + debugPrint('[SuspectedLocationService] Data is fresh, skipping fetch'); + return true; // Already have fresh data + } + return await _fetchData(); + } + + /// Force refresh the data (for manual refresh button) + Future forceRefresh() async { + return await _fetchData(); } /// Check if data should be refreshed @@ -95,14 +100,8 @@ class SuspectedLocationService { } /// Fetch data from the CSV URL - Future _fetchData({ - void Function(String message, double? progress)? onProgress, - }) async { - if (_isLoading) return false; - - _isLoading = true; + Future _fetchData() async { try { - onProgress?.call('Downloading CSV data...', null); debugPrint('[SuspectedLocationService] Fetching CSV data from $kSuspectedLocationsCsvUrl'); final response = await http.get( @@ -117,8 +116,6 @@ class SuspectedLocationService { return false; } - onProgress?.call('Parsing CSV data...', 0.2); - // Parse CSV with proper field separator and quote handling final csvData = await compute(_parseCSV, response.body); debugPrint('[SuspectedLocationService] Parsed ${csvData.length} rows from CSV'); @@ -170,11 +167,6 @@ class SuspectedLocationService { validRows++; } - // Report progress every 1000 rows - if (rowIndex % 1000 == 0) { - final progress = 0.4 + (rowIndex / dataRows.length) * 0.4; // 40% to 80% of total - onProgress?.call('Processing row $rowIndex...', progress); - } } catch (e, stackTrace) { // Skip rows that can't be parsed debugPrint('[SuspectedLocationService] Error parsing row $rowIndex: $e'); @@ -182,18 +174,12 @@ class SuspectedLocationService { } } - onProgress?.call('Calculating coordinates...', 0.8); + debugPrint('[SuspectedLocationService] Parsed $validRows valid rows from ${dataRows.length} total rows'); final fetchTime = DateTime.now(); // Process raw data and save (calculates centroids once) - await _cache.processAndSave(rawDataList, fetchTime, onProgress: (message, progress) { - // Map cache progress to final 20% (0.8 to 1.0) - final finalProgress = 0.8 + (progress ?? 0) * 0.2; - onProgress?.call(message, finalProgress); - }); - - onProgress?.call('Complete!', 1.0); + await _cache.processAndSave(rawDataList, fetchTime); debugPrint('[SuspectedLocationService] Successfully fetched and stored $validRows valid raw entries (${rawDataList.length} total)'); return true; @@ -202,8 +188,6 @@ class SuspectedLocationService { debugPrint('[SuspectedLocationService] Error fetching data: $e'); debugPrint('[SuspectedLocationService] Stack trace: $stackTrace'); return false; - } finally { - _isLoading = false; } } diff --git a/lib/state/suspected_location_state.dart b/lib/state/suspected_location_state.dart index 328706b..b88acba 100644 --- a/lib/state/suspected_location_state.dart +++ b/lib/state/suspected_location_state.dart @@ -31,7 +31,7 @@ class SuspectedLocationState extends ChangeNotifier { bool get isEnabled => _service.isEnabled; /// Whether currently loading data - bool get isLoading => _isLoading || _service.isLoading; + bool get isLoading => _isLoading; /// Last time data was fetched DateTime? get lastFetchTime => _service.lastFetchTime; @@ -45,18 +45,36 @@ class SuspectedLocationState extends ChangeNotifier { /// Enable or disable suspected locations Future setEnabled(bool enabled) async { await _service.setEnabled(enabled); + + // If enabling and no data exists, fetch it now + if (enabled && !_service.hasData) { + await _fetchData(); + } + notifyListeners(); } - /// Manually refresh the data - Future refreshData({ - void Function(String message, double? progress)? onProgress, - }) async { + /// Manually refresh the data (force refresh) + Future refreshData() async { _isLoading = true; notifyListeners(); try { - final success = await _service.refreshData(onProgress: onProgress); + final success = await _service.forceRefresh(); + return success; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + /// Internal method to fetch data if needed with loading state management + Future _fetchData() async { + _isLoading = true; + notifyListeners(); + + try { + final success = await _service.fetchDataIfNeeded(); return success; } finally { _isLoading = false;