mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-15 05:30:33 +02:00
Better suspected locations download indicator
This commit is contained in:
@@ -175,6 +175,7 @@ class AppState extends ChangeNotifier {
|
||||
SuspectedLocation? get selectedSuspectedLocation => _suspectedLocationState.selectedLocation;
|
||||
bool get suspectedLocationsEnabled => _suspectedLocationState.isEnabled;
|
||||
bool get suspectedLocationsLoading => _suspectedLocationState.isLoading;
|
||||
double? get suspectedLocationsDownloadProgress => _suspectedLocationState.downloadProgress;
|
||||
Future<DateTime?> get suspectedLocationsLastFetch => _suspectedLocationState.lastFetchTime;
|
||||
|
||||
void _onStateChanged() {
|
||||
|
||||
@@ -12,6 +12,7 @@ class SuspectedLocationsSection extends StatefulWidget {
|
||||
|
||||
class _SuspectedLocationsSectionState extends State<SuspectedLocationsSection> {
|
||||
DateTime? _lastFetch;
|
||||
bool _wasLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -38,8 +39,26 @@ class _SuspectedLocationsSectionState extends State<SuspectedLocationsSection> {
|
||||
final appState = context.watch<AppState>();
|
||||
final isEnabled = appState.suspectedLocationsEnabled;
|
||||
final isLoading = appState.suspectedLocationsLoading;
|
||||
final downloadProgress = appState.suspectedLocationsDownloadProgress;
|
||||
|
||||
// Check if loading just finished and reload last fetch time
|
||||
if (_wasLoading && !isLoading) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadLastFetch();
|
||||
});
|
||||
}
|
||||
_wasLoading = isLoading;
|
||||
|
||||
String getLastFetchText() {
|
||||
// Show status during loading
|
||||
if (isLoading) {
|
||||
if (downloadProgress != null) {
|
||||
return 'Downloading data... (this may take a few minutes)';
|
||||
} else {
|
||||
return 'Processing data...';
|
||||
}
|
||||
}
|
||||
|
||||
if (_lastFetch == null) {
|
||||
return locService.t('suspectedLocations.neverFetched');
|
||||
} else {
|
||||
@@ -112,10 +131,31 @@ class _SuspectedLocationsSectionState extends State<SuspectedLocationsSection> {
|
||||
title: Text(locService.t('suspectedLocations.lastUpdated')),
|
||||
subtitle: Text(getLastFetchText()),
|
||||
trailing: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
? SizedBox(
|
||||
width: 80,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
child: downloadProgress != null
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
value: downloadProgress,
|
||||
backgroundColor: Colors.grey[300],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${(downloadProgress * 100).toInt()}%',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
|
||||
@@ -66,17 +66,17 @@ class SuspectedLocationService {
|
||||
Future<DateTime?> get lastFetch => _cache.lastFetchTime;
|
||||
|
||||
/// Fetch data if needed (for enabling suspected locations when no data exists)
|
||||
Future<bool> fetchDataIfNeeded() async {
|
||||
Future<bool> fetchDataIfNeeded({void Function(double)? onProgress}) async {
|
||||
if (!(await _shouldRefresh())) {
|
||||
debugPrint('[SuspectedLocationService] Data is fresh, skipping fetch');
|
||||
return true; // Already have fresh data
|
||||
}
|
||||
return await _fetchData();
|
||||
return await _fetchData(onProgress: onProgress);
|
||||
}
|
||||
|
||||
/// Force refresh the data (for manual refresh button)
|
||||
Future<bool> forceRefresh() async {
|
||||
return await _fetchData();
|
||||
Future<bool> forceRefresh({void Function(double)? onProgress}) async {
|
||||
return await _fetchData(onProgress: onProgress);
|
||||
}
|
||||
|
||||
/// Check if data should be refreshed
|
||||
@@ -102,7 +102,7 @@ class SuspectedLocationService {
|
||||
}
|
||||
|
||||
/// Fetch data from the CSV URL
|
||||
Future<bool> _fetchData() async {
|
||||
Future<bool> _fetchData({void Function(double)? onProgress}) async {
|
||||
const maxRetries = 3;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
@@ -112,23 +112,52 @@ class SuspectedLocationService {
|
||||
debugPrint('[SuspectedLocationService] This may take up to ${_timeout.inMinutes} minutes for large datasets...');
|
||||
}
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse(kSuspectedLocationsCsvUrl),
|
||||
headers: {
|
||||
'User-Agent': 'DeFlock/1.0 (OSM surveillance mapping app)',
|
||||
},
|
||||
).timeout(_timeout);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
debugPrint('[SuspectedLocationService] HTTP error ${response.statusCode}');
|
||||
throw Exception('HTTP ${response.statusCode}');
|
||||
// Use streaming download for progress tracking
|
||||
final request = http.Request('GET', Uri.parse(kSuspectedLocationsCsvUrl));
|
||||
request.headers['User-Agent'] = 'DeFlock/1.0 (OSM surveillance mapping app)';
|
||||
|
||||
final client = http.Client();
|
||||
final streamedResponse = await client.send(request).timeout(_timeout);
|
||||
|
||||
if (streamedResponse.statusCode != 200) {
|
||||
debugPrint('[SuspectedLocationService] HTTP error ${streamedResponse.statusCode}');
|
||||
client.close();
|
||||
throw Exception('HTTP ${streamedResponse.statusCode}');
|
||||
}
|
||||
|
||||
final responseSize = response.contentLength ?? response.bodyBytes.length;
|
||||
debugPrint('[SuspectedLocationService] Downloaded ${responseSize} bytes, parsing CSV...');
|
||||
final contentLength = streamedResponse.contentLength;
|
||||
debugPrint('[SuspectedLocationService] Starting download of ${contentLength != null ? '$contentLength bytes' : 'unknown size'}...');
|
||||
|
||||
// Download with progress tracking
|
||||
final chunks = <List<int>>[];
|
||||
int downloadedBytes = 0;
|
||||
|
||||
await for (final chunk in streamedResponse.stream) {
|
||||
chunks.add(chunk);
|
||||
downloadedBytes += chunk.length;
|
||||
|
||||
// Report progress if we know the total size
|
||||
if (contentLength != null && onProgress != null) {
|
||||
try {
|
||||
final progress = downloadedBytes / contentLength;
|
||||
onProgress(progress.clamp(0.0, 1.0));
|
||||
} catch (e) {
|
||||
// Don't let progress callback errors break the download
|
||||
debugPrint('[SuspectedLocationService] Progress callback error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.close();
|
||||
|
||||
// Combine chunks into single response body
|
||||
final bodyBytes = chunks.expand((chunk) => chunk).toList();
|
||||
final responseBody = String.fromCharCodes(bodyBytes);
|
||||
|
||||
debugPrint('[SuspectedLocationService] Downloaded $downloadedBytes bytes, parsing CSV...');
|
||||
|
||||
// Parse CSV with proper field separator and quote handling
|
||||
final csvData = await compute(_parseCSV, response.body);
|
||||
final csvData = await compute(_parseCSV, responseBody);
|
||||
debugPrint('[SuspectedLocationService] Parsed ${csvData.length} rows from CSV');
|
||||
|
||||
if (csvData.isEmpty) {
|
||||
|
||||
@@ -8,6 +8,7 @@ class SuspectedLocationState extends ChangeNotifier {
|
||||
|
||||
SuspectedLocation? _selectedLocation;
|
||||
bool _isLoading = false;
|
||||
double? _downloadProgress; // 0.0 to 1.0, null when not downloading
|
||||
|
||||
/// Currently selected suspected location (for detail view)
|
||||
SuspectedLocation? get selectedLocation => _selectedLocation;
|
||||
@@ -47,6 +48,9 @@ class SuspectedLocationState extends ChangeNotifier {
|
||||
|
||||
/// Whether currently loading data
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
/// Download progress (0.0 to 1.0), null when not downloading
|
||||
double? get downloadProgress => _downloadProgress;
|
||||
|
||||
/// Last time data was fetched
|
||||
Future<DateTime?> get lastFetchTime => _service.lastFetchTime;
|
||||
@@ -72,13 +76,15 @@ class SuspectedLocationState extends ChangeNotifier {
|
||||
/// Manually refresh the data (force refresh)
|
||||
Future<bool> refreshData() async {
|
||||
_isLoading = true;
|
||||
_downloadProgress = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final success = await _service.forceRefresh();
|
||||
final success = await _service.forceRefresh(onProgress: _updateDownloadProgress);
|
||||
return success;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
_downloadProgress = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -86,16 +92,24 @@ class SuspectedLocationState extends ChangeNotifier {
|
||||
/// Internal method to fetch data if needed with loading state management
|
||||
Future<bool> _fetchData() async {
|
||||
_isLoading = true;
|
||||
_downloadProgress = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final success = await _service.fetchDataIfNeeded();
|
||||
final success = await _service.fetchDataIfNeeded(onProgress: _updateDownloadProgress);
|
||||
return success;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
_downloadProgress = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update download progress
|
||||
void _updateDownloadProgress(double progress) {
|
||||
_downloadProgress = progress;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Select a suspected location for detail view
|
||||
void selectLocation(SuspectedLocation location) {
|
||||
|
||||
Reference in New Issue
Block a user