mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-15 21:48:18 +02:00
Preview tile fetching on startup, offline area refresh, more cameras->nodes
This commit is contained in:
@@ -9,6 +9,7 @@ import 'models/pending_upload.dart';
|
||||
import 'models/tile_provider.dart';
|
||||
import 'services/offline_area_service.dart';
|
||||
import 'services/node_cache.dart';
|
||||
import 'services/tile_preview_service.dart';
|
||||
import 'widgets/camera_provider_with_cache.dart';
|
||||
import 'state/auth_state.dart';
|
||||
import 'state/operator_profile_state.dart';
|
||||
@@ -101,6 +102,10 @@ class AppState extends ChangeNotifier {
|
||||
Future<void> _init() async {
|
||||
// Initialize all state modules
|
||||
await _settingsState.init();
|
||||
|
||||
// Attempt to fetch missing tile type preview tiles (fails silently)
|
||||
_fetchMissingTilePreviews();
|
||||
|
||||
await _operatorProfileState.init();
|
||||
await _profileState.init();
|
||||
await _uploadQueueState.init();
|
||||
@@ -297,6 +302,15 @@ class AppState extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// ---------- Private Methods ----------
|
||||
/// Attempts to fetch missing tile preview images in the background (fire and forget)
|
||||
void _fetchMissingTilePreviews() {
|
||||
// Run asynchronously without awaiting to avoid blocking app startup
|
||||
TilePreviewService.fetchMissingPreviews(_settingsState).catchError((error) {
|
||||
// Silently ignore errors - this is best effort
|
||||
debugPrint('AppState: Tile preview fetching failed silently: $error');
|
||||
});
|
||||
}
|
||||
|
||||
void _startUploader() {
|
||||
_uploadQueueState.startUploader(
|
||||
offlineMode: offlineMode,
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "Innerhalb {} Kachel-Limit",
|
||||
"exceedsTileLimit": "Aktuelle Auswahl überschreitet {} Kachel-Limit",
|
||||
"offlineModeWarning": "Downloads im Offline-Modus deaktiviert. Deaktivieren Sie den Offline-Modus, um neue Bereiche herunterzuladen.",
|
||||
"downloadStarted": "Download gestartet! Lade Kacheln und Kameras...",
|
||||
"downloadStarted": "Download gestartet! Lade Kacheln und Knoten...",
|
||||
"downloadFailed": "Download konnte nicht gestartet werden: {}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "Länge",
|
||||
"tiles": "Kacheln",
|
||||
"size": "Größe",
|
||||
"cameras": "Kameras",
|
||||
"nodes": "Knoten",
|
||||
"areaIdFallback": "Bereich {}...",
|
||||
"renameArea": "Bereich umbenennen",
|
||||
"refreshWorldTiles": "Welt-Kacheln aktualisieren/neu herunterladen",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "Umbenennen",
|
||||
"megabytes": "MB",
|
||||
"kilobytes": "KB",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "Bereich aktualisieren",
|
||||
"refreshAreaDialogTitle": "Offline-Bereich aktualisieren",
|
||||
"refreshAreaDialogSubtitle": "Wählen Sie aus, was für diesen Bereich aktualisiert werden soll:",
|
||||
"refreshTiles": "Karten-Kacheln aktualisieren",
|
||||
"refreshTilesSubtitle": "Alle Kacheln für aktualisierte Bilder erneut herunterladen",
|
||||
"refreshNodes": "Knoten aktualisieren",
|
||||
"refreshNodesSubtitle": "Knotendaten für diesen Bereich erneut abrufen",
|
||||
"startRefresh": "Aktualisierung starten",
|
||||
"refreshStarted": "Aktualisierung gestartet!",
|
||||
"refreshFailed": "Aktualisierung fehlgeschlagen: {}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "Tags Verfeinern",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "Within {} tile limit",
|
||||
"exceedsTileLimit": "Current selection exceeds {} tile limit",
|
||||
"offlineModeWarning": "Downloads disabled while in offline mode. Disable offline mode to download new areas.",
|
||||
"downloadStarted": "Download started! Fetching tiles and cameras...",
|
||||
"downloadStarted": "Download started! Fetching tiles and nodes...",
|
||||
"downloadFailed": "Failed to start download: {}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "Lon",
|
||||
"tiles": "Tiles",
|
||||
"size": "Size",
|
||||
"cameras": "Cameras",
|
||||
"nodes": "Nodes",
|
||||
"areaIdFallback": "Area {}...",
|
||||
"renameArea": "Rename area",
|
||||
"refreshWorldTiles": "Refresh/re-download world tiles",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "Rename",
|
||||
"megabytes": "MB",
|
||||
"kilobytes": "KB",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "Refresh area",
|
||||
"refreshAreaDialogTitle": "Refresh Offline Area",
|
||||
"refreshAreaDialogSubtitle": "Choose what to refresh for this area:",
|
||||
"refreshTiles": "Refresh Map Tiles",
|
||||
"refreshTilesSubtitle": "Re-download all tiles for updated imagery",
|
||||
"refreshNodes": "Refresh Nodes",
|
||||
"refreshNodesSubtitle": "Re-fetch node data for this area",
|
||||
"startRefresh": "Start Refresh",
|
||||
"refreshStarted": "Refresh started!",
|
||||
"refreshFailed": "Refresh failed: {}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "Refine Tags",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "Dentro del límite de {} mosaicos",
|
||||
"exceedsTileLimit": "La selección actual excede el límite de {} mosaicos",
|
||||
"offlineModeWarning": "Descargas deshabilitadas en modo sin conexión. Deshabilite el modo sin conexión para descargar nuevas áreas.",
|
||||
"downloadStarted": "¡Descarga iniciada! Obteniendo mosaicos y cámaras...",
|
||||
"downloadStarted": "¡Descarga iniciada! Obteniendo mosaicos y nodos...",
|
||||
"downloadFailed": "Error al iniciar la descarga: {}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "Lon",
|
||||
"tiles": "Teselas",
|
||||
"size": "Tamaño",
|
||||
"cameras": "Cámaras",
|
||||
"nodes": "Nodos",
|
||||
"areaIdFallback": "Área {}...",
|
||||
"renameArea": "Renombrar área",
|
||||
"refreshWorldTiles": "Actualizar/re-descargar teselas mundiales",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "Renombrar",
|
||||
"megabytes": "MB",
|
||||
"kilobytes": "KB",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "Actualizar área",
|
||||
"refreshAreaDialogTitle": "Actualizar Área sin Conexión",
|
||||
"refreshAreaDialogSubtitle": "Elija qué actualizar para esta área:",
|
||||
"refreshTiles": "Actualizar Mosaicos del Mapa",
|
||||
"refreshTilesSubtitle": "Volver a descargar todos los mosaicos para imágenes actualizadas",
|
||||
"refreshNodes": "Actualizar Nodos",
|
||||
"refreshNodesSubtitle": "Volver a obtener datos de nodos para esta área",
|
||||
"startRefresh": "Iniciar Actualización",
|
||||
"refreshStarted": "¡Actualización iniciada!",
|
||||
"refreshFailed": "Actualización falló: {}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "Refinar Etiquetas",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "Dans la limite de {} tuiles",
|
||||
"exceedsTileLimit": "La sélection actuelle dépasse la limite de {} tuiles",
|
||||
"offlineModeWarning": "Téléchargements désactivés en mode hors ligne. Désactivez le mode hors ligne pour télécharger de nouvelles zones.",
|
||||
"downloadStarted": "Téléchargement démarré! Récupération des tuiles et caméras...",
|
||||
"downloadStarted": "Téléchargement démarré! Récupération des tuiles et nœuds...",
|
||||
"downloadFailed": "Échec du démarrage du téléchargement: {}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "Lon",
|
||||
"tiles": "Tuiles",
|
||||
"size": "Taille",
|
||||
"cameras": "Caméras",
|
||||
"nodes": "Nœuds",
|
||||
"areaIdFallback": "Zone {}...",
|
||||
"renameArea": "Renommer la zone",
|
||||
"refreshWorldTiles": "Actualiser/re-télécharger les tuiles mondiales",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "Renommer",
|
||||
"megabytes": "Mo",
|
||||
"kilobytes": "Ko",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "Actualiser la zone",
|
||||
"refreshAreaDialogTitle": "Actualiser la Zone Hors Ligne",
|
||||
"refreshAreaDialogSubtitle": "Choisissez quoi actualiser pour cette zone :",
|
||||
"refreshTiles": "Actualiser les Tuiles de Carte",
|
||||
"refreshTilesSubtitle": "Télécharger à nouveau toutes les tuiles pour des images mises à jour",
|
||||
"refreshNodes": "Actualiser les Nœuds",
|
||||
"refreshNodesSubtitle": "Récupérer à nouveau les données de nœuds pour cette zone",
|
||||
"startRefresh": "Démarrer l'Actualisation",
|
||||
"refreshStarted": "Actualisation démarrée !",
|
||||
"refreshFailed": "Actualisation échouée : {}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "Affiner les Étiquettes",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "Entro il limite di {} tile",
|
||||
"exceedsTileLimit": "La selezione corrente supera il limite di {} tile",
|
||||
"offlineModeWarning": "Download disabilitati in modalità offline. Disabilita la modalità offline per scaricare nuove aree.",
|
||||
"downloadStarted": "Download avviato! Recupero tile e telecamere...",
|
||||
"downloadStarted": "Download avviato! Recupero tile e nodi...",
|
||||
"downloadFailed": "Impossibile avviare il download: {}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "Lon",
|
||||
"tiles": "Tile",
|
||||
"size": "Dimensione",
|
||||
"cameras": "Telecamere",
|
||||
"nodes": "Nodi",
|
||||
"areaIdFallback": "Area {}...",
|
||||
"renameArea": "Rinomina area",
|
||||
"refreshWorldTiles": "Aggiorna/ri-scarica tile mondiali",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "Rinomina",
|
||||
"megabytes": "MB",
|
||||
"kilobytes": "KB",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "Aggiorna area",
|
||||
"refreshAreaDialogTitle": "Aggiorna Area Offline",
|
||||
"refreshAreaDialogSubtitle": "Scegli cosa aggiornare per quest'area:",
|
||||
"refreshTiles": "Aggiorna Tile Mappa",
|
||||
"refreshTilesSubtitle": "Riscarica tutte le tile per immagini aggiornate",
|
||||
"refreshNodes": "Aggiorna Nodi",
|
||||
"refreshNodesSubtitle": "Ricarica i dati dei nodi per quest'area",
|
||||
"startRefresh": "Avvia Aggiornamento",
|
||||
"refreshStarted": "Aggiornamento avviato!",
|
||||
"refreshFailed": "Aggiornamento fallito: {}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "Affina Tag",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "Dentro do limite de {} tiles",
|
||||
"exceedsTileLimit": "A seleção atual excede o limite de {} tiles",
|
||||
"offlineModeWarning": "Downloads desabilitados no modo offline. Desative o modo offline para baixar novas áreas.",
|
||||
"downloadStarted": "Download iniciado! Buscando tiles e câmeras...",
|
||||
"downloadStarted": "Download iniciado! Buscando tiles e nós...",
|
||||
"downloadFailed": "Falha ao iniciar o download: {}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "Lon",
|
||||
"tiles": "Tiles",
|
||||
"size": "Tamanho",
|
||||
"cameras": "Câmeras",
|
||||
"nodes": "Nós",
|
||||
"areaIdFallback": "Área {}...",
|
||||
"renameArea": "Renomear área",
|
||||
"refreshWorldTiles": "Atualizar/rebaixar tiles mundiais",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "Renomear",
|
||||
"megabytes": "MB",
|
||||
"kilobytes": "KB",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "Atualizar área",
|
||||
"refreshAreaDialogTitle": "Atualizar Área Offline",
|
||||
"refreshAreaDialogSubtitle": "Escolha o que atualizar para esta área:",
|
||||
"refreshTiles": "Atualizar Tiles do Mapa",
|
||||
"refreshTilesSubtitle": "Baixar novamente todos os tiles para imagens atualizadas",
|
||||
"refreshNodes": "Atualizar Nós",
|
||||
"refreshNodesSubtitle": "Buscar novamente os dados dos nós para esta área",
|
||||
"startRefresh": "Iniciar Atualização",
|
||||
"refreshStarted": "Atualização iniciada!",
|
||||
"refreshFailed": "Atualização falhou: {}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "Refinar Tags",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"withinTileLimit": "在 {} 瓦片限制内",
|
||||
"exceedsTileLimit": "当前选择超出 {} 瓦片限制",
|
||||
"offlineModeWarning": "离线模式下禁用下载。禁用离线模式以下载新区域。",
|
||||
"downloadStarted": "下载已开始!正在获取瓦片和摄像头...",
|
||||
"downloadStarted": "下载已开始!正在获取瓦片和节点...",
|
||||
"downloadFailed": "启动下载失败:{}"
|
||||
},
|
||||
"uploadMode": {
|
||||
@@ -225,7 +225,7 @@
|
||||
"longitude": "经度",
|
||||
"tiles": "瓦片",
|
||||
"size": "大小",
|
||||
"cameras": "摄像头",
|
||||
"nodes": "节点",
|
||||
"areaIdFallback": "区域 {}...",
|
||||
"renameArea": "重命名区域",
|
||||
"refreshWorldTiles": "刷新/重新下载世界瓦片",
|
||||
@@ -236,7 +236,17 @@
|
||||
"renameButton": "重命名",
|
||||
"megabytes": "MB",
|
||||
"kilobytes": "KB",
|
||||
"progress": "{}%"
|
||||
"progress": "{}%",
|
||||
"refreshArea": "刷新区域",
|
||||
"refreshAreaDialogTitle": "刷新离线区域",
|
||||
"refreshAreaDialogSubtitle": "选择要为此区域刷新的内容:",
|
||||
"refreshTiles": "刷新地图瓦片",
|
||||
"refreshTilesSubtitle": "重新下载所有瓦片以获取更新的图像",
|
||||
"refreshNodes": "刷新节点",
|
||||
"refreshNodesSubtitle": "重新获取此区域的节点数据",
|
||||
"startRefresh": "开始刷新",
|
||||
"refreshStarted": "刷新已开始!",
|
||||
"refreshFailed": "刷新失败:{}"
|
||||
},
|
||||
"refineTagsSheet": {
|
||||
"title": "细化标签",
|
||||
|
||||
@@ -24,6 +24,39 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
|
||||
});
|
||||
}
|
||||
|
||||
void _showRefreshDialog(OfflineArea area) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _RefreshAreaDialog(
|
||||
area: area,
|
||||
onRefresh: (refreshTiles, refreshNodes) {
|
||||
try {
|
||||
// ignore: unawaited_futures
|
||||
service.refreshArea(
|
||||
id: area.id,
|
||||
refreshTiles: refreshTiles,
|
||||
refreshNodes: refreshNodes,
|
||||
onProgress: (progress) => setState(() {}),
|
||||
onComplete: (status) => setState(() {}),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(LocalizationService.instance.t('offlineAreas.refreshStarted')),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(LocalizationService.instance.t('offlineAreas.refreshFailed', params: [e.toString()])),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
@@ -59,7 +92,7 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
|
||||
subtitle += '\n${locService.t('offlineAreas.tiles')}: ${area.tilesTotal}';
|
||||
}
|
||||
subtitle += '\n${locService.t('offlineAreas.size')}: $diskStr';
|
||||
subtitle += '\n${locService.t('offlineAreas.cameras')}: ${area.nodes.length}';
|
||||
subtitle += '\n${locService.t('offlineAreas.nodes')}: ${area.nodes.length}';
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: Icon(area.status == OfflineAreaStatus.complete
|
||||
@@ -113,7 +146,12 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
|
||||
}
|
||||
},
|
||||
),
|
||||
if (area.status != OfflineAreaStatus.downloading)
|
||||
if (area.status != OfflineAreaStatus.downloading) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, color: Colors.blue),
|
||||
tooltip: locService.t('offlineAreas.refreshArea'),
|
||||
onPressed: () => _showRefreshDialog(area),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
tooltip: locService.t('offlineAreas.deleteOfflineArea'),
|
||||
@@ -122,6 +160,7 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Text(subtitle),
|
||||
@@ -168,3 +207,67 @@ class _OfflineAreasSectionState extends State<OfflineAreasSection> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RefreshAreaDialog extends StatefulWidget {
|
||||
final OfflineArea area;
|
||||
final Function(bool refreshTiles, bool refreshNodes) onRefresh;
|
||||
|
||||
const _RefreshAreaDialog({
|
||||
required this.area,
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_RefreshAreaDialog> createState() => _RefreshAreaDialogState();
|
||||
}
|
||||
|
||||
class _RefreshAreaDialogState extends State<_RefreshAreaDialog> {
|
||||
bool _refreshTiles = true;
|
||||
bool _refreshNodes = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(locService.t('offlineAreas.refreshAreaDialogTitle')),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(locService.t('offlineAreas.refreshAreaDialogSubtitle')),
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile(
|
||||
title: Text(locService.t('offlineAreas.refreshTiles')),
|
||||
subtitle: Text(locService.t('offlineAreas.refreshTilesSubtitle')),
|
||||
value: _refreshTiles,
|
||||
onChanged: (value) => setState(() => _refreshTiles = value ?? true),
|
||||
dense: true,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text(locService.t('offlineAreas.refreshNodes')),
|
||||
subtitle: Text(locService.t('offlineAreas.refreshNodesSubtitle')),
|
||||
value: _refreshNodes,
|
||||
onChanged: (value) => setState(() => _refreshNodes = value ?? true),
|
||||
dense: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(locService.t('actions.cancel')),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: (_refreshTiles || _refreshNodes)
|
||||
? () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onRefresh(_refreshTiles, _refreshNodes);
|
||||
}
|
||||
: null,
|
||||
child: Text(locService.t('offlineAreas.startRefresh')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,115 @@ class OfflineAreaService {
|
||||
_areas.remove(area);
|
||||
await saveAreasToDisk();
|
||||
}
|
||||
|
||||
/// Refresh/update an existing offline area - tiles, nodes, or both
|
||||
Future<void> refreshArea({
|
||||
required String id,
|
||||
required bool refreshTiles,
|
||||
required bool refreshNodes,
|
||||
void Function(double progress)? onProgress,
|
||||
void Function(OfflineAreaStatus status)? onComplete,
|
||||
}) async {
|
||||
final area = _areas.firstWhere((a) => a.id == id, orElse: () => throw 'Area not found');
|
||||
|
||||
if (area.status == OfflineAreaStatus.downloading) {
|
||||
throw 'Area is already downloading';
|
||||
}
|
||||
|
||||
// Set area to downloading state
|
||||
area.status = OfflineAreaStatus.downloading;
|
||||
area.progress = 0.0;
|
||||
area.tilesDownloaded = 0;
|
||||
await saveAreasToDisk();
|
||||
|
||||
try {
|
||||
bool success = true;
|
||||
|
||||
if (refreshTiles && refreshNodes) {
|
||||
// Refresh both - use the full download process
|
||||
success = await OfflineAreaDownloader.downloadArea(
|
||||
area: area,
|
||||
bounds: area.bounds,
|
||||
minZoom: area.minZoom,
|
||||
maxZoom: area.maxZoom,
|
||||
directory: area.directory,
|
||||
onProgress: onProgress,
|
||||
saveAreasToDisk: saveAreasToDisk,
|
||||
getAreaSizeBytes: getAreaSizeBytes,
|
||||
);
|
||||
} else if (refreshTiles) {
|
||||
// Refresh tiles only
|
||||
success = await _refreshTilesOnly(area, onProgress);
|
||||
} else if (refreshNodes) {
|
||||
// Refresh nodes only
|
||||
success = await _refreshNodesOnly(area, onProgress);
|
||||
} else {
|
||||
// Neither option selected - shouldn't happen but handle gracefully
|
||||
success = true;
|
||||
area.progress = 1.0;
|
||||
}
|
||||
|
||||
await getAreaSizeBytes(area);
|
||||
|
||||
if (success) {
|
||||
area.status = OfflineAreaStatus.complete;
|
||||
area.progress = 1.0;
|
||||
debugPrint('Area $id: refresh completed successfully.');
|
||||
} else {
|
||||
area.status = OfflineAreaStatus.error;
|
||||
debugPrint('Area $id: refresh failed after maximum retry attempts.');
|
||||
}
|
||||
await saveAreasToDisk();
|
||||
onComplete?.call(area.status);
|
||||
} catch (e) {
|
||||
area.status = OfflineAreaStatus.error;
|
||||
await saveAreasToDisk();
|
||||
onComplete?.call(area.status);
|
||||
debugPrint('Area $id: refresh failed with exception: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh only the tiles for an area
|
||||
Future<bool> _refreshTilesOnly(OfflineArea area, void Function(double progress)? onProgress) async {
|
||||
final allTiles = computeTileList(area.bounds, area.minZoom, area.maxZoom);
|
||||
area.tilesTotal = allTiles.length;
|
||||
|
||||
return await OfflineAreaDownloader.downloadTilesWithRetry(
|
||||
area: area,
|
||||
allTiles: allTiles,
|
||||
directory: area.directory,
|
||||
onProgress: onProgress,
|
||||
saveAreasToDisk: saveAreasToDisk,
|
||||
getAreaSizeBytes: getAreaSizeBytes,
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresh only the nodes for an area
|
||||
Future<bool> _refreshNodesOnly(OfflineArea area, void Function(double progress)? onProgress) async {
|
||||
try {
|
||||
// Use the same logic as in the downloader for consistency
|
||||
final nodeZoom = (area.minZoom + 1).clamp(8, 16);
|
||||
final expandedNodeBounds = OfflineAreaDownloader.calculateNodeBounds(area.bounds, nodeZoom);
|
||||
|
||||
final nodes = await MapDataProvider().getAllNodesForDownload(
|
||||
bounds: expandedNodeBounds,
|
||||
profiles: AppState.instance.profiles,
|
||||
);
|
||||
|
||||
area.nodes = nodes;
|
||||
await OfflineAreaDownloader.saveNodes(nodes, area.directory);
|
||||
|
||||
// Set progress to complete for nodes-only refresh
|
||||
onProgress?.call(1.0);
|
||||
area.progress = 1.0;
|
||||
|
||||
debugPrint('Area ${area.id}: Refreshed ${nodes.length} nodes');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('Area ${area.id}: Failed to refresh nodes: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove any legacy world areas from previous versions
|
||||
Future<void> _cleanupLegacyWorldAreas() async {
|
||||
|
||||
@@ -30,7 +30,7 @@ class OfflineAreaDownloader {
|
||||
area.tilesTotal = allTiles.length;
|
||||
|
||||
// Download tiles with retry logic
|
||||
final success = await _downloadTilesWithRetry(
|
||||
final success = await downloadTilesWithRetry(
|
||||
area: area,
|
||||
allTiles: allTiles,
|
||||
directory: directory,
|
||||
@@ -51,7 +51,7 @@ class OfflineAreaDownloader {
|
||||
}
|
||||
|
||||
/// Download tiles with retry logic
|
||||
static Future<bool> _downloadTilesWithRetry({
|
||||
static Future<bool> downloadTilesWithRetry({
|
||||
required OfflineArea area,
|
||||
required Set<List<int>> allTiles,
|
||||
required String directory,
|
||||
@@ -138,7 +138,7 @@ class OfflineAreaDownloader {
|
||||
// Modest expansion: use tiles at minZoom + 1 instead of minZoom
|
||||
// This gives a reasonable buffer without capturing entire states
|
||||
final nodeZoom = (minZoom + 1).clamp(8, 16); // Reasonable bounds for node fetching
|
||||
final expandedNodeBounds = _calculateNodeBounds(bounds, nodeZoom);
|
||||
final expandedNodeBounds = calculateNodeBounds(bounds, nodeZoom);
|
||||
|
||||
final nodes = await MapDataProvider().getAllNodesForDownload(
|
||||
bounds: expandedNodeBounds,
|
||||
@@ -150,7 +150,7 @@ class OfflineAreaDownloader {
|
||||
}
|
||||
|
||||
/// Calculate expanded bounds that cover the entire tile area at minimum zoom
|
||||
static LatLngBounds _calculateNodeBounds(LatLngBounds visibleBounds, int minZoom) {
|
||||
static LatLngBounds calculateNodeBounds(LatLngBounds visibleBounds, int minZoom) {
|
||||
final tiles = computeTileList(visibleBounds, minZoom, minZoom);
|
||||
if (tiles.isEmpty) return visibleBounds;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../models/tile_provider.dart';
|
||||
import '../state/settings_state.dart';
|
||||
|
||||
/// Service for fetching missing tile preview images
|
||||
class TilePreviewService {
|
||||
static const int _previewZoom = 10;
|
||||
static const int _previewX = 512;
|
||||
static const int _previewY = 384;
|
||||
static const Duration _timeout = Duration(seconds: 10);
|
||||
|
||||
/// Attempt to fetch missing preview tiles for tile types that don't already have preview data
|
||||
/// Fails silently - no error handling or user notification on failure
|
||||
static Future<void> fetchMissingPreviews(SettingsState settingsState) async {
|
||||
try {
|
||||
bool anyUpdates = false;
|
||||
|
||||
for (final provider in settingsState.tileProviders) {
|
||||
final updatedTileTypes = <TileType>[];
|
||||
bool providerNeedsUpdate = false;
|
||||
|
||||
for (final tileType in provider.tileTypes) {
|
||||
// Only fetch if preview tile is missing
|
||||
if (tileType.previewTile == null) {
|
||||
// Skip if tile type requires API key but provider doesn't have one
|
||||
if (tileType.requiresApiKey && (provider.apiKey == null || provider.apiKey!.isEmpty)) {
|
||||
updatedTileTypes.add(tileType);
|
||||
continue;
|
||||
}
|
||||
|
||||
final previewData = await _fetchPreviewForTileType(tileType, provider.apiKey);
|
||||
if (previewData != null) {
|
||||
// Create updated tile type with preview data
|
||||
final updatedTileType = tileType.copyWith(previewTile: previewData);
|
||||
updatedTileTypes.add(updatedTileType);
|
||||
providerNeedsUpdate = true;
|
||||
} else {
|
||||
updatedTileTypes.add(tileType);
|
||||
}
|
||||
} else {
|
||||
updatedTileTypes.add(tileType);
|
||||
}
|
||||
}
|
||||
|
||||
if (providerNeedsUpdate) {
|
||||
final updatedProvider = provider.copyWith(tileTypes: updatedTileTypes);
|
||||
await settingsState.addOrUpdateTileProvider(updatedProvider);
|
||||
anyUpdates = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyUpdates) {
|
||||
debugPrint('TilePreviewService: Updated providers with new preview tiles');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail silently as requested
|
||||
debugPrint('TilePreviewService: Error during preview fetching: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Uint8List?> _fetchPreviewForTileType(TileType tileType, String? apiKey) async {
|
||||
try {
|
||||
final url = tileType.getTileUrl(_previewZoom, _previewX, _previewY, apiKey: apiKey);
|
||||
|
||||
final response = await http.get(Uri.parse(url)).timeout(_timeout);
|
||||
|
||||
if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) {
|
||||
debugPrint('TilePreviewService: Fetched preview for ${tileType.name}');
|
||||
return response.bodyBytes;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fail silently - just log for debugging
|
||||
debugPrint('TilePreviewService: Failed to fetch preview for ${tileType.name}: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user