mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-09-21 00:30:41 +02:00
feat: Added Notifications to app
This commit is contained in:
committed by
Ryan Brown
parent
2c64fe610e
commit
51037f3710
@@ -81,7 +81,6 @@
|
||||
"enableOfflineFeaturesTitle": "Offline-Funktionen aktivieren",
|
||||
"enableOfflineFeaturesSubtitle": "Herunterladen und Nutzen von Offline-Kartenbereichen erlauben. Wenn deaktiviert, sind Offline-Modus und heruntergeladene Bereiche nicht verfügbar."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Bildschirm während der App-Nutzung aktiv lassen",
|
||||
"subtitle": "Verhindert, dass sich der Bildschirm während der Nutzung der App automatisch ausschaltet"
|
||||
@@ -90,7 +89,6 @@
|
||||
"title": "Zoom-Steuerung ausblenden",
|
||||
"subtitle": "Blendet die +/- Zoom-Schaltflächen und die Zoomstufen-Anzeige aus. Sie können weiterhin mit Fingergesten zoomen."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Benachrichtigung erhalten beim Annähern an Überwachungsgeräte",
|
||||
"batteryUsage": "Verbraucht zusätzlich Batterie für kontinuierliche Standortüberwachung",
|
||||
@@ -101,7 +99,13 @@
|
||||
"enableNotifications": "Benachrichtigungen Aktivieren",
|
||||
"checkingPermissions": "Berechtigungen prüfen...",
|
||||
"alertDistance": "Warnentfernung: ",
|
||||
"rangeInfo": "Bereich: {}-{} {} (Standard: {})"
|
||||
"rangeInfo": "Bereich: {}-{} {} (Standard: {})",
|
||||
"backgroundServiceTitle": "Näherungswarnungen aktiv",
|
||||
"backgroundServiceText": "Sucht nach Überwachungsgeräten in der Nähe",
|
||||
"backgroundServiceChannel": "Überwachung von Näherungswarnungen",
|
||||
"openSettings": "Einstellungen öffnen",
|
||||
"blockedTitle": "Benachrichtigungen sind blockiert",
|
||||
"blockedMessage": "Benachrichtigungen für DeFlock sind deaktiviert. Aktiviere sie in den Systemeinstellungen, um Näherungswarnungen zu erhalten."
|
||||
},
|
||||
"node": {
|
||||
"title": "Knoten #{}",
|
||||
@@ -129,9 +133,6 @@
|
||||
"viewInQueue": "In Warteschlange anzeigen",
|
||||
"deletedSuccessfully": "Dieser Knoten wurde erfolgreich von OpenStreetMap gelöscht."
|
||||
},
|
||||
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profil",
|
||||
"selectProfile": "Profil auswählen...",
|
||||
@@ -281,7 +282,6 @@
|
||||
"inFlightCompleteTitle": "Übermittlung Abgeschlossen",
|
||||
"inFlightCompleteMessage": "Ihre Übermittlung wurde erfolgreich abgeschlossen. Ihre Einstellung wird jetzt angewendet."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Kachel-Anbieter",
|
||||
"noProvidersConfigured": "Keine Kachel-Anbieter konfiguriert",
|
||||
@@ -433,7 +433,6 @@
|
||||
"keepData": "Daten Behalten",
|
||||
"deleteData": "Daten Löschen"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Tags Verfeinern",
|
||||
"operatorProfile": "Betreiber-Profil",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "Enable offline features",
|
||||
"enableOfflineFeaturesSubtitle": "Allow downloading and using offline map areas. When disabled, offline mode and downloaded areas are unavailable."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Keep screen awake while using app",
|
||||
"subtitle": "Prevents the screen from turning off automatically while using the app"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Hide zoom controls",
|
||||
"subtitle": "Hide the +/- zoom buttons and zoom level indicator. You can still zoom with pinch gestures."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Get notified when approaching surveillance devices",
|
||||
"batteryUsage": "Uses extra battery for continuous location monitoring",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Enable Notifications",
|
||||
"checkingPermissions": "Checking permissions...",
|
||||
"alertDistance": "Alert distance: ",
|
||||
"rangeInfo": "Range: {}-{} {} (default: {})"
|
||||
"rangeInfo": "Range: {}-{} {} (default: {})",
|
||||
"backgroundServiceTitle": "Proximity alerts active",
|
||||
"backgroundServiceText": "Watching for nearby surveillance devices",
|
||||
"backgroundServiceChannel": "Proximity Alert Monitoring",
|
||||
"openSettings": "Open Settings",
|
||||
"blockedTitle": "Notifications are blocked",
|
||||
"blockedMessage": "Notifications are turned off for DeFlock. Enable them in your system settings to receive proximity alerts."
|
||||
},
|
||||
"node": {
|
||||
"title": "Node #{}",
|
||||
@@ -166,8 +170,6 @@
|
||||
"viewInQueue": "View in Queue",
|
||||
"deletedSuccessfully": "This node was successfully deleted from OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profile",
|
||||
"selectProfile": "Select a profile...",
|
||||
@@ -317,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Submission Complete",
|
||||
"inFlightCompleteMessage": "Your submission finished successfully. Applying your setting now."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Tile Providers",
|
||||
"noProvidersConfigured": "No tile providers configured",
|
||||
@@ -326,7 +327,7 @@
|
||||
"needsApiKey": "Needs API key",
|
||||
"editProvider": "Edit Provider",
|
||||
"addProvider": "Add Provider",
|
||||
"deleteProvider": "Delete Provider",
|
||||
"deleteProvider": "Delete Provider",
|
||||
"deleteProviderConfirm": "Are you sure you want to delete \"{}\"?",
|
||||
"clearCaches": "Clear Caches",
|
||||
"clearCachesConfirm": "Clear all tile caches for \"{}\"? This will free up storage space but tiles will need to be downloaded again when viewing those areas.",
|
||||
@@ -469,7 +470,6 @@
|
||||
"keepData": "Keep Data",
|
||||
"deleteData": "Delete Data"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Refine Tags",
|
||||
"operatorProfile": "Operator Profile",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "Activar funciones sin conexión",
|
||||
"enableOfflineFeaturesSubtitle": "Permitir descargar y usar áreas de mapa sin conexión. Si se desactiva, el modo sin conexión y las áreas descargadas no estarán disponibles."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Mantener la pantalla encendida mientras se usa la app",
|
||||
"subtitle": "Evita que la pantalla se apague automáticamente mientras se usa la app"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Ocultar controles de zoom",
|
||||
"subtitle": "Oculta los botones de zoom +/- y el indicador de nivel de zoom. Aún puedes hacer zoom con gestos de pellizco."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Recibe notificaciones al acercarte a dispositivos de vigilancia",
|
||||
"batteryUsage": "Usa batería extra para monitoreo continuo de ubicación",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Habilitar Notificaciones",
|
||||
"checkingPermissions": "Verificando permisos...",
|
||||
"alertDistance": "Distancia de alerta: ",
|
||||
"rangeInfo": "Rango: {}-{} {} (predeterminado: {})"
|
||||
"rangeInfo": "Rango: {}-{} {} (predeterminado: {})",
|
||||
"backgroundServiceTitle": "Alertas de proximidad activas",
|
||||
"backgroundServiceText": "Buscando dispositivos de vigilancia cercanos",
|
||||
"backgroundServiceChannel": "Monitoreo de alertas de proximidad",
|
||||
"openSettings": "Abrir ajustes",
|
||||
"blockedTitle": "Las notificaciones están bloqueadas",
|
||||
"blockedMessage": "Las notificaciones están desactivadas para DeFlock. Actívalas en los ajustes del sistema para recibir alertas de proximidad."
|
||||
},
|
||||
"node": {
|
||||
"title": "Nodo #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/vandalismo",
|
||||
"deleteReasonCustom": "Personalizado..",
|
||||
"deleteReasonHint": "Por favor describa la razón...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Debes iniciar sesión con una cuenta de OpenStreetMap para contribuir con datos del mapa.",
|
||||
"uploadPending": "Subida pendiente",
|
||||
"uploadCreatingChangeset": "Creando changeset...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Ver en la Cola",
|
||||
"deletedSuccessfully": "Este nodo fue eliminado con éxito de OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Perfil",
|
||||
"selectProfile": "Seleccionar un perfil...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Envío Completado",
|
||||
"inFlightCompleteMessage": "Su envío finalizó exitosamente. Aplicando su configuración ahora."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Proveedores de Tiles",
|
||||
"noProvidersConfigured": "No hay proveedores de tiles configurados",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Conservar Datos",
|
||||
"deleteData": "Eliminar Datos"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Refinar Etiquetas",
|
||||
"operatorProfile": "Perfil de Operador",
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"enableOfflineFeaturesSubtitle": "Autoriser le téléchargement et l'utilisation de zones de carte hors ligne. Si désactivé, le mode hors ligne et les zones téléchargées ne seront pas disponibles."
|
||||
},
|
||||
"keepScreenAwake": {
|
||||
|
||||
"title": "Maintenir l'écran allumé lors de l'utilisation de l'application",
|
||||
"subtitle": "Empêche l'écran de s'éteindre automatiquement pendant l'utilisation de l'application"
|
||||
},
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Masquer les contrôles de zoom",
|
||||
"subtitle": "Masque les boutons de zoom +/- et l'indicateur de niveau de zoom. Vous pouvez toujours zoomer avec des gestes de pincement."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Recevoir des notifications en s'approchant de dispositifs de surveillance",
|
||||
"batteryUsage": "Utilise de la batterie supplémentaire pour la surveillance continue de la localisation",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Activer les Notifications",
|
||||
"checkingPermissions": "Vérification des autorisations...",
|
||||
"alertDistance": "Distance d'alerte : ",
|
||||
"rangeInfo": "Plage : {}-{} {} (par défaut : {})"
|
||||
"rangeInfo": "Plage : {}-{} {} (par défaut : {})",
|
||||
"backgroundServiceTitle": "Alertes de proximité actives",
|
||||
"backgroundServiceText": "Recherche d'appareils de surveillance à proximité",
|
||||
"backgroundServiceChannel": "Surveillance des alertes de proximité",
|
||||
"openSettings": "Ouvrir les réglages",
|
||||
"blockedTitle": "Les notifications sont bloquées",
|
||||
"blockedMessage": "Les notifications sont désactivées pour DeFlock. Activez-les dans les réglages du système pour recevoir les alertes de proximité."
|
||||
},
|
||||
"node": {
|
||||
"title": "Nœud #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/vandalisme",
|
||||
"deleteReasonCustom": "Personnalisé..",
|
||||
"deleteReasonHint": "Veuillez décrire la raison...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Vous devez être connecté avec un compte OpenStreetMap pour contribuer aux données cartographiques.",
|
||||
"uploadPending": "Téléchargement en attente",
|
||||
"uploadCreatingChangeset": "Création du changeset...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Voir dans la File d'Attente",
|
||||
"deletedSuccessfully": "Ce nœud a été supprimé avec succès d'OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profil",
|
||||
"selectProfile": "Sélectionner un profil...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Soumission Terminée",
|
||||
"inFlightCompleteMessage": "Votre soumission s'est terminée avec succès. Application de votre paramètre maintenant."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Fournisseurs de Tuiles",
|
||||
"noProvidersConfigured": "Aucun fournisseur de tuiles configuré",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Conserver les Données",
|
||||
"deleteData": "Supprimer les Données"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Affiner les Étiquettes",
|
||||
"operatorProfile": "Profil d'Opérateur",
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"enableOfflineFeaturesSubtitle": "Consenti il download e l'uso di aree mappa offline. Se disabilitato, la modalità offline e le aree scaricate non saranno disponibili."
|
||||
},
|
||||
"keepScreenAwake": {
|
||||
|
||||
"title": "Mantieni lo schermo attivo durante l'utilizzo dell'app",
|
||||
"subtitle": "Impedisce allo schermo di spegnersi automaticamente durante l'utilizzo dell'app"
|
||||
},
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Nascondi i controlli dello zoom",
|
||||
"subtitle": "Nasconde i pulsanti di zoom +/- e l'indicatore del livello di zoom. Puoi ancora usare i gesti di pizzico per lo zoom."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Ricevi notifiche quando ti avvicini a dispositivi di sorveglianza",
|
||||
"batteryUsage": "Utilizza batteria extra per il monitoraggio continuo della posizione",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Abilita Notifiche",
|
||||
"checkingPermissions": "Controllo autorizzazioni...",
|
||||
"alertDistance": "Distanza di avviso: ",
|
||||
"rangeInfo": "Intervallo: {}-{} {} (predefinito: {})"
|
||||
"rangeInfo": "Intervallo: {}-{} {} (predefinito: {})",
|
||||
"backgroundServiceTitle": "Avvisi di prossimità attivi",
|
||||
"backgroundServiceText": "Ricerca di dispositivi di sorveglianza nelle vicinanze",
|
||||
"backgroundServiceChannel": "Monitoraggio avvisi di prossimità",
|
||||
"openSettings": "Apri impostazioni",
|
||||
"blockedTitle": "Le notifiche sono bloccate",
|
||||
"blockedMessage": "Le notifiche sono disattivate per DeFlock. Attivale nelle impostazioni di sistema per ricevere gli avvisi di prossimità."
|
||||
},
|
||||
"node": {
|
||||
"title": "Nodo #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/vandalismo",
|
||||
"deleteReasonCustom": "Personalizzato..",
|
||||
"deleteReasonHint": "Descrivi il motivo...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Devi aver effettuato l'accesso con un account OpenStreetMap per contribuire ai dati della mappa.",
|
||||
"uploadPending": "Caricamento in sospeso",
|
||||
"uploadCreatingChangeset": "Creazione changeset...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Visualizza in Coda",
|
||||
"deletedSuccessfully": "Questo nodo è stato eliminato con successo da OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profilo",
|
||||
"selectProfile": "Seleziona un profilo...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Invio Completato",
|
||||
"inFlightCompleteMessage": "Il tuo invio è terminato con successo. Applicazione della tua impostazione in corso."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Fornitori di Tile",
|
||||
"noProvidersConfigured": "Nessun fornitore di tile configurato",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Conserva Dati",
|
||||
"deleteData": "Elimina Dati"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Affina Tag",
|
||||
"operatorProfile": "Profilo Operatore",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "Offline functies inschakelen",
|
||||
"enableOfflineFeaturesSubtitle": "Sta het downloaden en gebruiken van offline kaartgebieden toe. Indien uitgeschakeld, zijn offline modus en gedownloade gebieden niet beschikbaar."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Houd het scherm actief tijdens het gebruik van de app",
|
||||
"subtitle": "Voorkomt dat het scherm automatisch uitschakelt tijdens het gebruik van de app"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Zoomknoppen verbergen",
|
||||
"subtitle": "Verbergt de +/- zoomknoppen en de zoomniveau-indicator. U kunt nog steeds zoomen met knijpgebaren."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Krijg meldingen wanneer u surveillance apparaten nadert",
|
||||
"batteryUsage": "Gebruikt extra batterij voor continue locatie monitoring",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Schakel Meldingen In",
|
||||
"checkingPermissions": "Toestemmingen controleren...",
|
||||
"alertDistance": "Waarschuwingsafstand: ",
|
||||
"rangeInfo": "Bereik: {}-{} {} (standaard: {})"
|
||||
"rangeInfo": "Bereik: {}-{} {} (standaard: {})",
|
||||
"backgroundServiceTitle": "Nabijheidswaarschuwingen actief",
|
||||
"backgroundServiceText": "Zoekt naar bewakingsapparatuur in de buurt",
|
||||
"backgroundServiceChannel": "Monitoring van nabijheidswaarschuwingen",
|
||||
"openSettings": "Instellingen openen",
|
||||
"blockedTitle": "Meldingen zijn geblokkeerd",
|
||||
"blockedMessage": "Meldingen zijn uitgeschakeld voor DeFlock. Schakel ze in via je systeeminstellingen om nabijheidswaarschuwingen te ontvangen."
|
||||
},
|
||||
"node": {
|
||||
"title": "Node #{}",
|
||||
@@ -146,7 +150,7 @@
|
||||
"queuedForUpload": "Node in wachtrij geplaatst voor upload",
|
||||
"editQueuedForUpload": "Node bewerking in wachtrij geplaatst voor upload",
|
||||
"deleteQueuedForUpload": "Node verwijdering in wachtrij geplaatst voor upload",
|
||||
"confirmDeleteTitle": "Verwijder Node",
|
||||
"confirmDeleteTitle": "Verwijder Node",
|
||||
"confirmDeleteMessage": "Weet u zeker dat u node #{} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"deleteReasonLabel": "Reden voor Verwijdering",
|
||||
"deleteReasonSelectHint": "Selecteer...",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/vandalisme",
|
||||
"deleteReasonCustom": "Aangepast..",
|
||||
"deleteReasonHint": "Beschrijf de reden...",
|
||||
|
||||
"mustBeLoggedInToDelete": "U moet ingelogd zijn met een OpenStreetMap-account om kaartgegevens bij te dragen.",
|
||||
"uploadPending": "Upload in behandeling",
|
||||
"uploadCreatingChangeset": "Changeset maken...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Bekijk in Wachtrij",
|
||||
"deletedSuccessfully": "Deze node is succesvol verwijderd van OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profiel",
|
||||
"selectProfile": "Selecteer een profiel...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Indiening Voltooid",
|
||||
"inFlightCompleteMessage": "Uw indiening is succesvol afgerond. Uw instelling wordt nu toegepast."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Tile Providers",
|
||||
"noProvidersConfigured": "Geen tile providers geconfigureerd",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Gegevens Behouden",
|
||||
"deleteData": "Gegevens Verwijderen"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Verfijn Tags",
|
||||
"operatorProfile": "Operator Profiel",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "Włącz funkcje offline",
|
||||
"enableOfflineFeaturesSubtitle": "Zezwól na pobieranie i korzystanie z map offline. Jeśli wyłączone, tryb offline i pobrane obszary będą niedostępne."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Nie wygaszaj ekranu podczas korzystania z aplikacji",
|
||||
"subtitle": "Zapobiega automatycznemu wygaszaniu ekranu podczas korzystania z aplikacji"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Ukryj kontrolki zoomu",
|
||||
"subtitle": "Ukrywa przyciski zoomu +/- oraz wskaźnik poziomu powiększenia. Nadal możesz powiększać za pomocą gestów szczypania."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Otrzymuj powiadomienia przy zbliżaniu się do urządzeń nadzoru",
|
||||
"batteryUsage": "Używa dodatkowej baterii do ciągłego monitorowania lokalizacji",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Włącz Powiadomienia",
|
||||
"checkingPermissions": "Sprawdzanie uprawnień...",
|
||||
"alertDistance": "Odległość alertu: ",
|
||||
"rangeInfo": "Zakres: {}-{} {} (domyślnie: {})"
|
||||
"rangeInfo": "Zakres: {}-{} {} (domyślnie: {})",
|
||||
"backgroundServiceTitle": "Alerty o bliskości aktywne",
|
||||
"backgroundServiceText": "Wyszukiwanie pobliskich urządzeń monitorujących",
|
||||
"backgroundServiceChannel": "Monitorowanie alertów o bliskości",
|
||||
"openSettings": "Otwórz ustawienia",
|
||||
"blockedTitle": "Powiadomienia są zablokowane",
|
||||
"blockedMessage": "Powiadomienia dla DeFlock są wyłączone. Włącz je w ustawieniach systemowych, aby otrzymywać alerty o bliskości."
|
||||
},
|
||||
"node": {
|
||||
"title": "Węzeł #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/wandalizm",
|
||||
"deleteReasonCustom": "Niestandardowy..",
|
||||
"deleteReasonHint": "Opisz powód...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Musisz być zalogowany na konto OpenStreetMap, aby dodawać dane do mapy.",
|
||||
"uploadPending": "Przesyłanie oczekujące",
|
||||
"uploadCreatingChangeset": "Tworzenie zestawu zmian...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Zobacz w Kolejce",
|
||||
"deletedSuccessfully": "Ten węzeł został pomyślnie usunięty z OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profil",
|
||||
"selectProfile": "Wybierz profil...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Przesyłanie Zakończone",
|
||||
"inFlightCompleteMessage": "Twoje przesyłanie zakończyło się pomyślnie. Stosowanie ustawienia teraz."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Dostawcy Kafelków",
|
||||
"noProvidersConfigured": "Brak skonfigurowanych dostawców kafelków",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Zachowaj Dane",
|
||||
"deleteData": "Usuń Dane"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Doprecyzuj Tagi",
|
||||
"operatorProfile": "Profil Operatora",
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
"enableOfflineFeaturesSubtitle": "Permitir descarregar e usar áreas de mapa offline. Se desativado, o modo offline e as áreas descarregadas não estarão disponíveis."
|
||||
},
|
||||
"keepScreenAwake": {
|
||||
|
||||
"title": "Manter o ecrã ativo durante o uso do app",
|
||||
"subtitle": "Impede que o ecrã se desligue automaticamente enquanto a app estiver em uso"
|
||||
},
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Ocultar controlos de zoom",
|
||||
"subtitle": "Oculta os botões de zoom +/- e o indicador de nível de zoom. Ainda pode ampliar com gestos de pinça."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Receba notificações ao se aproximar de dispositivos de vigilância",
|
||||
"batteryUsage": "Usa bateria extra para monitoramento contínuo de localização",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Habilitar Notificações",
|
||||
"checkingPermissions": "Verificando permissões...",
|
||||
"alertDistance": "Distância de alerta: ",
|
||||
"rangeInfo": "Faixa: {}-{} {} (padrão: {})"
|
||||
"rangeInfo": "Faixa: {}-{} {} (padrão: {})",
|
||||
"backgroundServiceTitle": "Alertas de proximidade ativos",
|
||||
"backgroundServiceText": "Procurando dispositivos de vigilância próximos",
|
||||
"backgroundServiceChannel": "Monitoramento de alertas de proximidade",
|
||||
"openSettings": "Abrir configurações",
|
||||
"blockedTitle": "As notificações estão bloqueadas",
|
||||
"blockedMessage": "As notificações estão desativadas para o DeFlock. Ative-as nas configurações do sistema para receber alertas de proximidade."
|
||||
},
|
||||
"node": {
|
||||
"title": "Nó #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/vandalismo",
|
||||
"deleteReasonCustom": "Personalizado..",
|
||||
"deleteReasonHint": "Descreva o motivo...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Você precisa estar conectado com uma conta do OpenStreetMap para contribuir com dados do mapa.",
|
||||
"uploadPending": "Envio pendente",
|
||||
"uploadCreatingChangeset": "Criando changeset...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Ver na Fila",
|
||||
"deletedSuccessfully": "Este nó foi excluído com sucesso do OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Perfil",
|
||||
"selectProfile": "Selecionar um perfil...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Envio Concluído",
|
||||
"inFlightCompleteMessage": "Seu envio foi concluído com sucesso. Aplicando sua configuração agora."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Provedores de Tiles",
|
||||
"noProvidersConfigured": "Nenhum provedor de tiles configurado",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Manter Dados",
|
||||
"deleteData": "Eliminar Dados"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Refinar Tags",
|
||||
"operatorProfile": "Perfil de Operador",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "Çevrimdışı özellikleri etkinleştir",
|
||||
"enableOfflineFeaturesSubtitle": "Çevrimdışı harita alanlarını indirmeye ve kullanmaya izin ver. Devre dışı bırakılırsa, çevrimdışı mod ve indirilen alanlar kullanılamaz."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Uygulamayı kullanırken ekranı açık tut",
|
||||
"subtitle": "Uygulamayı kullanırken ekranın otomatik olarak kapanmasını engeller"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Yakınlaştırma kontrollerini gizle",
|
||||
"subtitle": "+/- yakınlaştırma düğmelerini ve yakınlaştırma seviyesi göstergesini gizler. Yine de kıstırma hareketleriyle yakınlaştırabilirsiniz."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Gözetleme cihazlarına yaklaşırken bildirim al",
|
||||
"batteryUsage": "Sürekli konum izleme için ekstra batarya kullanır",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Bildirimleri Etkinleştir",
|
||||
"checkingPermissions": "İzinler kontrol ediliyor...",
|
||||
"alertDistance": "Uyarı mesafesi: ",
|
||||
"rangeInfo": "Aralık: {}-{} {} (varsayılan: {})"
|
||||
"rangeInfo": "Aralık: {}-{} {} (varsayılan: {})",
|
||||
"backgroundServiceTitle": "Yakınlık uyarıları etkin",
|
||||
"backgroundServiceText": "Yakındaki gözetim cihazları aranıyor",
|
||||
"backgroundServiceChannel": "Yakınlık Uyarısı İzleme",
|
||||
"openSettings": "Ayarları aç",
|
||||
"blockedTitle": "Bildirimler engellendi",
|
||||
"blockedMessage": "DeFlock için bildirimler kapalı. Yakınlık uyarıları almak için sistem ayarlarından etkinleştirin."
|
||||
},
|
||||
"node": {
|
||||
"title": "Düğüm #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Spam/vandalizm",
|
||||
"deleteReasonCustom": "Özel..",
|
||||
"deleteReasonHint": "Lütfen nedeni açıklayın...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Harita verilerine katkıda bulunmak için bir OpenStreetMap hesabıyla oturum açmış olmanız gerekir.",
|
||||
"uploadPending": "Yükleme bekliyor",
|
||||
"uploadCreatingChangeset": "Değişiklik seti oluşturuluyor...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Sırada Görüntüle",
|
||||
"deletedSuccessfully": "Bu düğüm OpenStreetMap'ten başarıyla silindi."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Profil",
|
||||
"selectProfile": "Bir profil seçin...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Gönderim Tamamlandı",
|
||||
"inFlightCompleteMessage": "Gönderiminiz başarıyla tamamlandı. Ayarınız şimdi uygulanıyor."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Döşeme Sağlayıcıları",
|
||||
"noProvidersConfigured": "Döşeme sağlayıcısı yapılandırılmamış",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Verileri Sakla",
|
||||
"deleteData": "Verileri Sil"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Etiketleri Düzenle",
|
||||
"operatorProfile": "Operatör Profili",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "Увімкнути офлайн-функції",
|
||||
"enableOfflineFeaturesSubtitle": "Дозволити завантаження та використання офлайн-областей карти. Якщо вимкнено, офлайн-режим і завантажені області будуть недоступні."
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "Тримати екран активним під час використання програми",
|
||||
"subtitle": "Запобігає автоматичному вимкненню екрана під час використання програми"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "Приховати елементи керування масштабом",
|
||||
"subtitle": "Приховує кнопки масштабування +/- та індикатор рівня масштабу. Ви все ще можете масштабувати жестами."
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "Отримувати сповіщення при наближенні до пристроїв спостереження",
|
||||
"batteryUsage": "Використовує додаткову батарею для безперервного моніторингу місцезнаходження",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "Увімкнути Сповіщення",
|
||||
"checkingPermissions": "Перевірка дозволів...",
|
||||
"alertDistance": "Відстань сповіщення: ",
|
||||
"rangeInfo": "Діапазон: {}-{} {} (за замовчуванням: {})"
|
||||
"rangeInfo": "Діапазон: {}-{} {} (за замовчуванням: {})",
|
||||
"backgroundServiceTitle": "Сповіщення про наближення активні",
|
||||
"backgroundServiceText": "Пошук пристроїв стеження поблизу",
|
||||
"backgroundServiceChannel": "Моніторинг сповіщень про наближення",
|
||||
"openSettings": "Відкрити налаштування",
|
||||
"blockedTitle": "Сповіщення заблоковано",
|
||||
"blockedMessage": "Сповіщення для DeFlock вимкнено. Увімкніть їх у налаштуваннях системи, щоб отримувати сповіщення про наближення."
|
||||
},
|
||||
"node": {
|
||||
"title": "Вузол #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "Спам/вандалізм",
|
||||
"deleteReasonCustom": "Власний..",
|
||||
"deleteReasonHint": "Опишіть причину...",
|
||||
|
||||
"mustBeLoggedInToDelete": "Ви повинні увійти в обліковий запис OpenStreetMap, щоб надавати дані карти.",
|
||||
"uploadPending": "Завантаження очікує",
|
||||
"uploadCreatingChangeset": "Створення набору змін...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "Переглянути в Черзі",
|
||||
"deletedSuccessfully": "Цей вузол було успішно видалено з OpenStreetMap."
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "Профіль",
|
||||
"selectProfile": "Оберіть профіль...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "Подання Завершено",
|
||||
"inFlightCompleteMessage": "Ваше подання успішно завершено. Застосування вашого налаштування зараз."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Постачальники Плиток",
|
||||
"noProvidersConfigured": "Постачальників плиток не налаштовано",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "Зберегти Дані",
|
||||
"deleteData": "Видалити Дані"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "Уточнити Теги",
|
||||
"operatorProfile": "Профіль Оператора",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"enableOfflineFeaturesTitle": "启用离线功能",
|
||||
"enableOfflineFeaturesSubtitle": "允许下载和使用离线地图区域。禁用后,离线模式和已下载的区域将不可用。"
|
||||
},
|
||||
|
||||
"keepScreenAwake": {
|
||||
"title": "使用应用时保持屏幕常亮",
|
||||
"subtitle": "防止在使用应用期间屏幕自动熄灭"
|
||||
@@ -127,7 +126,6 @@
|
||||
"title": "隐藏缩放控件",
|
||||
"subtitle": "隐藏 +/- 缩放按钮和缩放级别指示器。您仍然可以使用捏合手势进行缩放。"
|
||||
},
|
||||
|
||||
"proximityAlerts": {
|
||||
"getNotified": "接近监控设备时接收通知",
|
||||
"batteryUsage": "使用额外电量进行连续位置监控",
|
||||
@@ -138,7 +136,13 @@
|
||||
"enableNotifications": "启用通知",
|
||||
"checkingPermissions": "检查权限中...",
|
||||
"alertDistance": "警报距离:",
|
||||
"rangeInfo": "范围:{}-{} {}(默认:{})"
|
||||
"rangeInfo": "范围:{}-{} {}(默认:{})",
|
||||
"backgroundServiceTitle": "邻近提醒已启用",
|
||||
"backgroundServiceText": "正在搜索附近的监控设备",
|
||||
"backgroundServiceChannel": "邻近提醒监控",
|
||||
"openSettings": "打开设置",
|
||||
"blockedTitle": "通知已被阻止",
|
||||
"blockedMessage": "DeFlock 的通知已关闭。请在系统设置中启用,以接收邻近提醒。"
|
||||
},
|
||||
"node": {
|
||||
"title": "节点 #{}",
|
||||
@@ -156,7 +160,6 @@
|
||||
"deleteReasonSpamVandalism": "垃圾信息/破坏",
|
||||
"deleteReasonCustom": "自定义..",
|
||||
"deleteReasonHint": "请描述原因...",
|
||||
|
||||
"mustBeLoggedInToDelete": "您必须使用 OpenStreetMap 账户登录才能贡献地图数据。",
|
||||
"uploadPending": "上传待处理",
|
||||
"uploadCreatingChangeset": "正在创建变更集...",
|
||||
@@ -167,8 +170,6 @@
|
||||
"viewInQueue": "在队列中查看",
|
||||
"deletedSuccessfully": "此节点已成功从 OpenStreetMap 删除。"
|
||||
},
|
||||
|
||||
|
||||
"addNode": {
|
||||
"profile": "配置文件",
|
||||
"selectProfile": "选择配置文件...",
|
||||
@@ -318,7 +319,6 @@
|
||||
"inFlightCompleteTitle": "提交完成",
|
||||
"inFlightCompleteMessage": "您的提交已成功完成。正在应用您的设置。"
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "瓦片提供商",
|
||||
"noProvidersConfigured": "未配置瓦片提供商",
|
||||
@@ -470,7 +470,6 @@
|
||||
"keepData": "保留数据",
|
||||
"deleteData": "删除数据"
|
||||
},
|
||||
|
||||
"refineTagsSheet": {
|
||||
"title": "细化标签",
|
||||
"operatorProfile": "运营商配置文件",
|
||||
|
||||
@@ -18,7 +18,8 @@ class ProximityAlertsSection extends StatefulWidget {
|
||||
State<ProximityAlertsSection> createState() => _ProximityAlertsSectionState();
|
||||
}
|
||||
|
||||
class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
|
||||
class _ProximityAlertsSectionState extends State<ProximityAlertsSection>
|
||||
with WidgetsBindingObserver {
|
||||
late final TextEditingController _distanceController;
|
||||
bool _notificationsEnabled = false;
|
||||
bool _checkingPermissions = false;
|
||||
@@ -26,6 +27,9 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Re-check on resume: the user may have just granted notifications in the
|
||||
// system settings app and come back.
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
final appState = context.read<AppState>();
|
||||
// Convert meters to display units for the text field
|
||||
final displayValue = DistanceService.convertFromMeters(
|
||||
@@ -57,19 +61,59 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
|
||||
setState(() {
|
||||
_checkingPermissions = true;
|
||||
});
|
||||
|
||||
|
||||
final enabled = await ProximityAlertService().requestNotificationPermissions();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_notificationsEnabled = enabled;
|
||||
_checkingPermissions = false;
|
||||
});
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_notificationsEnabled = enabled;
|
||||
_checkingPermissions = false;
|
||||
});
|
||||
|
||||
// Still not granted. On iOS the system prompt only ever appears once, so
|
||||
// re-requesting silently does nothing — the only remaining path is the
|
||||
// system settings app.
|
||||
if (!enabled) {
|
||||
await _promptOpenSystemSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _promptOpenSystemSettings() async {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
final shouldOpen = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(locService.t('proximityAlerts.blockedTitle')),
|
||||
content: Text(locService.t('proximityAlerts.blockedMessage')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: Text(locService.t('actions.cancel')),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: Text(locService.t('proximityAlerts.openSettings')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldOpen == true) {
|
||||
await ProximityAlertService().openSystemSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_checkNotificationPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_distanceController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Mean meters per degree of latitude. Constant enough at any latitude for
|
||||
/// picking a node lookup box.
|
||||
const double _metersPerDegreeLat = 111320.0;
|
||||
|
||||
/// Smallest cos(latitude) we will divide by. Past ~89.4° the true factor tends
|
||||
/// to zero and the longitude span would blow up to the whole globe.
|
||||
const double _minCosLatitude = 0.01;
|
||||
|
||||
/// A square of roughly [radiusMeters] in every direction around [center].
|
||||
///
|
||||
/// Used to pull cached nodes near a GPS position, as opposed to near whatever
|
||||
/// the map camera is showing — the camera is frozen while the app is
|
||||
/// backgrounded, so it is not a usable proxy for where the user is.
|
||||
///
|
||||
/// A degree of longitude shrinks by cos(latitude), so the box is widened east
|
||||
/// to west to keep the real-world radius honest as you move away from the
|
||||
/// equator.
|
||||
LatLngBounds boundsAround(LatLng center, num radiusMeters) {
|
||||
final radius = radiusMeters.toDouble().abs();
|
||||
final latDelta = radius / _metersPerDegreeLat;
|
||||
final cosLat = math.cos(center.latitude * math.pi / 180).abs();
|
||||
final lngDelta =
|
||||
radius / (_metersPerDegreeLat * math.max(cosLat, _minCosLatitude));
|
||||
|
||||
return LatLngBounds(
|
||||
LatLng(
|
||||
(center.latitude - latDelta).clamp(-90.0, 90.0),
|
||||
(center.longitude - lngDelta).clamp(-180.0, 180.0),
|
||||
),
|
||||
LatLng(
|
||||
(center.latitude + latDelta).clamp(-90.0, 90.0),
|
||||
(center.longitude + lngDelta).clamp(-180.0, 180.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import '../dev_config.dart';
|
||||
class RecentAlert {
|
||||
final int nodeId;
|
||||
final DateTime alertTime;
|
||||
|
||||
|
||||
RecentAlert({required this.nodeId, required this.alertTime});
|
||||
}
|
||||
|
||||
@@ -24,125 +24,189 @@ class ProximityAlertService {
|
||||
ProximityAlertService._internal();
|
||||
|
||||
FlutterLocalNotificationsPlugin? _notifications;
|
||||
bool _isInitialized = false;
|
||||
|
||||
|
||||
/// Whether the plugin itself is usable. Deliberately NOT derived from
|
||||
/// initialize()'s return value: on iOS that reports the outcome of the
|
||||
/// permission request, which we intentionally disable below, so it returns
|
||||
/// false even though the plugin works fine. Gating on it made every
|
||||
/// notification path dead on iOS.
|
||||
bool _pluginReady = false;
|
||||
|
||||
/// Guards against re-initializing on every settings visit.
|
||||
Future<void>? _initFuture;
|
||||
|
||||
// Simple in-memory tracking of recent alerts to prevent spam
|
||||
final List<RecentAlert> _recentAlerts = [];
|
||||
static const Duration _alertCooldown = kProximityAlertCooldown;
|
||||
|
||||
|
||||
// Callback for showing in-app visual alerts
|
||||
VoidCallback? _onVisualAlert;
|
||||
|
||||
/// Initialize the notification plugin and request permissions
|
||||
Future<void> initialize({VoidCallback? onVisualAlert}) async {
|
||||
_onVisualAlert = onVisualAlert;
|
||||
|
||||
|
||||
/// Initialize the notification plugin. Permissions are requested separately,
|
||||
/// on demand, when the user enables proximity alerts.
|
||||
Future<void> initialize({VoidCallback? onVisualAlert}) {
|
||||
if (onVisualAlert != null) _onVisualAlert = onVisualAlert;
|
||||
return _initFuture ??= _doInitialize();
|
||||
}
|
||||
|
||||
Future<void> _doInitialize() async {
|
||||
_notifications = FlutterLocalNotificationsPlugin();
|
||||
|
||||
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestSoundPermission: false,
|
||||
);
|
||||
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
|
||||
try {
|
||||
final initialized = await _notifications!.initialize(initSettings);
|
||||
_isInitialized = initialized ?? false;
|
||||
|
||||
// Note: We don't request notification permissions here anymore.
|
||||
// Permissions are requested on-demand when user enables proximity alerts.
|
||||
|
||||
debugPrint('[ProximityAlertService] Initialized: $_isInitialized (permissions deferred)');
|
||||
await _notifications!.initialize(initSettings);
|
||||
// Completing without throwing is the real signal that the plugin is up.
|
||||
_pluginReady = true;
|
||||
debugPrint('[ProximityAlertService] Plugin ready (permissions deferred)');
|
||||
} catch (e) {
|
||||
debugPrint('[ProximityAlertService] Failed to initialize: $e');
|
||||
_isInitialized = false;
|
||||
_pluginReady = false;
|
||||
_initFuture = null; // Allow a later retry
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the plugin is up before touching it, so callers that arrive before
|
||||
/// MapView's fire-and-forget initialize() completes still work.
|
||||
Future<bool> _ensureReady() async {
|
||||
if (_pluginReady) return true;
|
||||
await initialize();
|
||||
return _pluginReady;
|
||||
}
|
||||
|
||||
/// Request notification permissions on both platforms
|
||||
/// Request notification permissions on both platforms.
|
||||
///
|
||||
/// Note that on iOS the system prompt appears only ONCE, ever. After the
|
||||
/// user has answered it, this call returns silently without showing
|
||||
/// anything — which is why callers must check the result and fall back to
|
||||
/// sending the user to system settings.
|
||||
Future<void> _requestNotificationPermissions() async {
|
||||
if (_notifications == null) return;
|
||||
|
||||
|
||||
try {
|
||||
// Request permissions - this will show the permission dialog on Android 13+
|
||||
final result = await _notifications!
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
|
||||
debugPrint('[ProximityAlertService] Android notification permission result: $result');
|
||||
|
||||
// Also request for iOS (though this was already done in initialization)
|
||||
await _notifications!
|
||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
final android = _notifications!
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||
if (android != null) {
|
||||
final result = await android.requestNotificationsPermission();
|
||||
debugPrint('[ProximityAlertService] Android permission result: $result');
|
||||
return;
|
||||
}
|
||||
|
||||
final ios = _notifications!
|
||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
||||
if (ios != null) {
|
||||
final result = await ios.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
debugPrint('[ProximityAlertService] iOS permission result: $result');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ProximityAlertService] Failed to request permissions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the OS settings page for this app, so the user can grant
|
||||
/// notifications after the one-shot iOS prompt has already been answered.
|
||||
/// Uses Geolocator's platform channel purely as a way to open app settings;
|
||||
/// it is not location-specific.
|
||||
Future<bool> openSystemSettings() async {
|
||||
try {
|
||||
return await Geolocator.openAppSettings();
|
||||
} catch (e) {
|
||||
debugPrint('[ProximityAlertService] Failed to open app settings: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check proximity to nodes and trigger alerts if needed
|
||||
/// This should be called on GPS position updates
|
||||
/// Check proximity to nodes and trigger alerts if needed.
|
||||
/// This should be called on GPS position updates.
|
||||
Future<void> checkProximity({
|
||||
required LatLng userLocation,
|
||||
required List<OsmNode> nodes,
|
||||
required List<NodeProfile> enabledProfiles,
|
||||
required int alertDistance,
|
||||
}) async {
|
||||
if (!_isInitialized || nodes.isEmpty) return;
|
||||
|
||||
if (!_pluginReady || nodes.isEmpty) return;
|
||||
|
||||
// Clean up old alerts (anything older than cooldown period)
|
||||
final cutoffTime = DateTime.now().subtract(_alertCooldown);
|
||||
_recentAlerts.removeWhere((alert) => alert.alertTime.isBefore(cutoffTime));
|
||||
|
||||
|
||||
// Check each node for proximity
|
||||
for (final node in nodes) {
|
||||
// Skip if we recently alerted for this node
|
||||
if (_recentAlerts.any((alert) => alert.nodeId == node.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate distance using Geolocator's distanceBetween
|
||||
if (_recentAlerts.any((alert) => alert.nodeId == node.id)) continue;
|
||||
|
||||
final distance = Geolocator.distanceBetween(
|
||||
userLocation.latitude,
|
||||
userLocation.longitude,
|
||||
node.coord.latitude,
|
||||
node.coord.longitude,
|
||||
);
|
||||
|
||||
// Check if within alert distance
|
||||
|
||||
if (distance <= alertDistance) {
|
||||
// Determine node type for alert message
|
||||
final nodeType = _getNodeTypeDescription(node, enabledProfiles);
|
||||
|
||||
// Trigger both push notification and visual alert
|
||||
|
||||
await _showNotification(node, nodeType, distance.round());
|
||||
_showVisualAlert();
|
||||
|
||||
// Track this alert to prevent spam
|
||||
_recentAlerts.add(RecentAlert(
|
||||
nodeId: node.id,
|
||||
alertTime: DateTime.now(),
|
||||
));
|
||||
|
||||
|
||||
_recentAlerts.add(RecentAlert(nodeId: node.id, alertTime: DateTime.now()));
|
||||
|
||||
debugPrint('[ProximityAlertService] Alert triggered for node ${node.id} ($nodeType) at ${distance.round()}m');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Notification IDs must fit in a signed 32-bit int, but OSM node IDs are
|
||||
/// well past that (currently ~12 billion), so passing one through raises
|
||||
/// "must fit within the size of a 32-bit integer" and the notification is
|
||||
/// dropped. Fold to a stable positive 31-bit value instead.
|
||||
///
|
||||
/// Deterministic across runs, so re-alerting for the same node replaces the
|
||||
/// existing notification rather than stacking a duplicate.
|
||||
static int _notificationId(int nodeId) => nodeId.abs() % 0x7FFFFFFF;
|
||||
|
||||
/// Show push notification for proximity alert
|
||||
Future<void> _showNotification(OsmNode node, String nodeType, int distance) async {
|
||||
if (!_isInitialized || _notifications == null) return;
|
||||
|
||||
await _show(
|
||||
id: _notificationId(node.id),
|
||||
title: 'Surveillance Device Nearby',
|
||||
body: '$nodeType detected ${distance}m ahead',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _show({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
if (!_pluginReady || _notifications == null) {
|
||||
debugPrint('[ProximityAlertService] NOT SHOWN: plugin not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// A notification that silently no-ops because permission was never granted
|
||||
// is indistinguishable from one that was never triggered. Say which.
|
||||
if (!await areNotificationsEnabled()) {
|
||||
debugPrint(
|
||||
'[ProximityAlertService] NOT SHOWN: OS notification permission not '
|
||||
'granted. Settings → Proximity Alerts → Enable Notifications.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'proximity_alerts',
|
||||
'Proximity Alerts',
|
||||
@@ -154,7 +218,13 @@ class ProximityAlertService {
|
||||
);
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
// presentAlert is dead on iOS 14+: the plugin's willPresentNotification
|
||||
// only consults presentBanner/presentList there and ignores presentAlert
|
||||
// entirely. Setting just presentAlert produced no visible banner at all.
|
||||
// Keep it for iOS 13 and below, but presentBanner is what actually works.
|
||||
presentAlert: true,
|
||||
presentBanner: true,
|
||||
presentList: true,
|
||||
presentBadge: false,
|
||||
presentSound: true,
|
||||
);
|
||||
@@ -164,16 +234,16 @@ class ProximityAlertService {
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
final title = 'Surveillance Device Nearby';
|
||||
final body = '$nodeType detected ${distance}m ahead';
|
||||
|
||||
try {
|
||||
await _notifications!.show(
|
||||
node.id, // Use node ID as notification ID
|
||||
id, // Use node ID as notification ID
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
);
|
||||
debugPrint('[ProximityAlertService] SHOWN id=$id "$title" — $body');
|
||||
} on ArgumentError catch (e) {
|
||||
debugPrint('[ProximityAlertService] Rejected notification id=$id: $e');
|
||||
} catch (e) {
|
||||
debugPrint('[ProximityAlertService] Failed to show notification: $e');
|
||||
}
|
||||
@@ -226,30 +296,46 @@ class ProximityAlertService {
|
||||
_recentAlerts.clear();
|
||||
}
|
||||
|
||||
/// Check if notification permissions are granted
|
||||
/// Check if notification permissions are granted.
|
||||
Future<bool> areNotificationsEnabled() async {
|
||||
if (!_isInitialized || _notifications == null) return false;
|
||||
|
||||
if (!await _ensureReady() || _notifications == null) return false;
|
||||
|
||||
try {
|
||||
// Check Android permissions
|
||||
final androidImpl = _notifications!
|
||||
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
||||
if (androidImpl != null) {
|
||||
final result = await androidImpl.areNotificationsEnabled();
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
// For iOS, assume enabled if we got this far (permissions were requested during init)
|
||||
return true;
|
||||
|
||||
// iOS: actually ask the system rather than assuming. The old code
|
||||
// returned a blind `true` here, so the UI could never tell whether
|
||||
// notifications were really authorized.
|
||||
final iosImpl = _notifications!
|
||||
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
|
||||
if (iosImpl != null) {
|
||||
final options = await iosImpl.checkPermissions();
|
||||
if (options == null) return false;
|
||||
return options.isEnabled || options.isProvisionalEnabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('[ProximityAlertService] Failed to check notification permissions: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Request permissions again (can be called from settings)
|
||||
|
||||
/// Request permissions and report whether they ended up granted.
|
||||
///
|
||||
/// A `false` result does NOT mean the prompt was declined just now — on iOS
|
||||
/// the prompt may not have appeared at all because it was already answered
|
||||
/// in a previous launch. Callers should offer [openSystemSettings] on false.
|
||||
Future<bool> requestNotificationPermissions() async {
|
||||
if (!await _ensureReady()) return false;
|
||||
await _requestNotificationPermissions();
|
||||
return await areNotificationsEnabled();
|
||||
final enabled = await areNotificationsEnabled();
|
||||
debugPrint('[ProximityAlertService] Permissions after request: $enabled');
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../../dev_config.dart';
|
||||
import '../../app_state.dart' show FollowMeMode;
|
||||
import '../../services/proximity_alert_service.dart';
|
||||
import '../../services/coordinate_validation.dart';
|
||||
import '../../services/localization_service.dart';
|
||||
import '../../models/osm_node.dart';
|
||||
import '../../models/node_profile.dart';
|
||||
|
||||
@@ -21,7 +22,11 @@ import '../../models/node_profile.dart';
|
||||
class GpsController {
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
Timer? _retryTimer;
|
||||
|
||||
|
||||
/// Whether the live stream was started with background delivery armed, so a
|
||||
/// proximity-alerts toggle knows whether it actually needs to rebuild it.
|
||||
bool _streamIsBackgroundCapable = false;
|
||||
|
||||
// Location state
|
||||
LatLng? _currentLocation;
|
||||
bool _hasLocation = false;
|
||||
@@ -32,7 +37,7 @@ class GpsController {
|
||||
FollowMeMode Function()? _getCurrentFollowMeMode;
|
||||
bool Function()? _getProximityAlertsEnabled;
|
||||
int Function()? _getProximityAlertDistance;
|
||||
List<OsmNode> Function()? _getNearbyNodes;
|
||||
List<OsmNode> Function(LatLng userLocation, int radiusMeters)? _getNearbyNodes;
|
||||
List<NodeProfile> Function()? _getEnabledProfiles;
|
||||
VoidCallback? _onMapMovedProgrammatically;
|
||||
bool Function()? _isUserInteracting;
|
||||
@@ -50,7 +55,7 @@ class GpsController {
|
||||
required FollowMeMode Function() getCurrentFollowMeMode,
|
||||
required bool Function() getProximityAlertsEnabled,
|
||||
required int Function() getProximityAlertDistance,
|
||||
required List<OsmNode> Function() getNearbyNodes,
|
||||
required List<OsmNode> Function(LatLng userLocation, int radiusMeters) getNearbyNodes,
|
||||
required List<NodeProfile> Function() getEnabledProfiles,
|
||||
VoidCallback? onMapMovedProgrammatically,
|
||||
bool Function()? isUserInteracting,
|
||||
@@ -86,6 +91,38 @@ class GpsController {
|
||||
_handleInitialFollowMeAnimation(newMode, oldMode);
|
||||
}
|
||||
|
||||
/// Force a fresh fix, e.g. when the app returns from the background.
|
||||
///
|
||||
/// The position stream can stay silent after a resume — and a simulated
|
||||
/// location that "teleported" while the app was suspended produces no
|
||||
/// movement event at all — leaving _currentLocation stale, so proximity
|
||||
/// alerts would keep evaluating against the old position.
|
||||
Future<void> refreshLocation() async {
|
||||
debugPrint('[GpsController] Refreshing location after resume');
|
||||
|
||||
// Restart the stream first: iOS often stops delivering to a subscription
|
||||
// that spanned suspension.
|
||||
if (_positionSub != null) {
|
||||
_stopLocationTracking();
|
||||
_startPositionStream();
|
||||
}
|
||||
|
||||
try {
|
||||
// geolocator 10.x: getCurrentPosition takes desiredAccuracy, unlike
|
||||
// getPositionStream above which takes a LocationSettings object.
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
// Match the stream's forceLocationManager: true, so the one-shot and
|
||||
// the stream cannot disagree about where we are on Android.
|
||||
forceAndroidLocationManager: true,
|
||||
timeLimit: const Duration(seconds: 10),
|
||||
);
|
||||
_onPositionReceived(position);
|
||||
} catch (e) {
|
||||
debugPrint('[GpsController] Failed to refresh location: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Manual retry (e.g., user pressed follow-me button)
|
||||
Future<void> retryLocationInit() async {
|
||||
debugPrint('[GpsController] Manual retry of location initialization');
|
||||
@@ -149,24 +186,29 @@ class GpsController {
|
||||
final followMeMode = _getCurrentFollowMeMode?.call() ?? FollowMeMode.off;
|
||||
final distanceFilter = followMeMode == FollowMeMode.off ? 5 : 1; // 5m normal, 1m follow-me
|
||||
|
||||
debugPrint('[GpsController] Starting GPS position stream (${distanceFilter}m filter)');
|
||||
// Both platforms stop feeding a backgrounded app by default, which is why
|
||||
// alerts only ever fired with the app on screen. Keeping the stream alive
|
||||
// in the background is visible to the user (persistent notification on
|
||||
// Android, blue status bar on iOS), so only ask for it when proximity
|
||||
// alerts are actually switched on.
|
||||
final background = _getProximityAlertsEnabled?.call() ?? false;
|
||||
|
||||
debugPrint(
|
||||
'[GpsController] Starting GPS position stream '
|
||||
'(${distanceFilter}m filter, background=$background)',
|
||||
);
|
||||
|
||||
try {
|
||||
_positionSub = Geolocator.getPositionStream(
|
||||
locationSettings: defaultTargetPlatform == TargetPlatform.android
|
||||
? AndroidSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: distanceFilter,
|
||||
forceLocationManager: true,
|
||||
)
|
||||
: LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: distanceFilter,
|
||||
),
|
||||
locationSettings: _buildLocationSettings(
|
||||
distanceFilter: distanceFilter,
|
||||
background: background,
|
||||
),
|
||||
).listen(
|
||||
_onPositionReceived,
|
||||
onError: _onPositionError,
|
||||
);
|
||||
_streamIsBackgroundCapable = background;
|
||||
} catch (e) {
|
||||
debugPrint('[GpsController] Failed to start position stream: $e');
|
||||
_hasLocation = false;
|
||||
@@ -175,6 +217,79 @@ class GpsController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Platform-specific location settings.
|
||||
///
|
||||
/// When [background] is false these are exactly the old foreground-only
|
||||
/// settings, so nothing changes for users who leave proximity alerts off.
|
||||
LocationSettings _buildLocationSettings({
|
||||
required int distanceFilter,
|
||||
required bool background,
|
||||
}) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
return AndroidSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: distanceFilter,
|
||||
forceLocationManager: true,
|
||||
// Supplying this config is what makes geolocator run its
|
||||
// GeolocatorLocationService as a foreground service. Without it
|
||||
// Android throttles a backgrounded app to a few updates per hour and
|
||||
// then stops entirely, so alerts never fire.
|
||||
foregroundNotificationConfig: background
|
||||
? ForegroundNotificationConfig(
|
||||
notificationTitle:
|
||||
locService.t('proximityAlerts.backgroundServiceTitle'),
|
||||
notificationText:
|
||||
locService.t('proximityAlerts.backgroundServiceText'),
|
||||
notificationChannelName:
|
||||
locService.t('proximityAlerts.backgroundServiceChannel'),
|
||||
// Without a wake lock the system sleeps and delivers the
|
||||
// queued positions in one burst on wake — far too late to warn
|
||||
// anyone about a device they already drove past.
|
||||
enableWakeLock: true,
|
||||
setOngoing: true,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
return AppleSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: distanceFilter,
|
||||
// Pairs with UIBackgroundModes=location in Info.plist. Together they
|
||||
// stop iOS from suspending the app on background, which is what killed
|
||||
// the position stream (and therefore every alert) before.
|
||||
allowBackgroundLocationUpdates: background,
|
||||
// iOS otherwise pauses updates when it decides you have stopped moving
|
||||
// and never reliably resumes them — a silent death for alerts.
|
||||
pauseLocationUpdatesAutomatically: false,
|
||||
// The blue status bar pill. Non-negotiable honesty: the app is reading
|
||||
// location off screen and the user should be able to see that.
|
||||
showBackgroundLocationIndicator: background,
|
||||
activityType: ActivityType.otherNavigation,
|
||||
);
|
||||
}
|
||||
|
||||
return LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: distanceFilter,
|
||||
);
|
||||
}
|
||||
|
||||
/// Rebuild the stream when the proximity-alerts setting is toggled, so
|
||||
/// background delivery is armed (or dropped) immediately rather than at the
|
||||
/// next unrelated stream restart.
|
||||
void updateProximityAlertsEnabled(bool enabled) {
|
||||
if (_positionSub == null || _streamIsBackgroundCapable == enabled) return;
|
||||
|
||||
debugPrint('[GpsController] Proximity alerts $enabled — rebuilding stream');
|
||||
_stopLocationTracking();
|
||||
_startPositionStream();
|
||||
}
|
||||
|
||||
/// Restart position stream with current follow-me settings
|
||||
void _restartPositionStream() {
|
||||
if (_positionSub == null) {
|
||||
@@ -217,7 +332,7 @@ class GpsController {
|
||||
|
||||
// Handle proximity alerts
|
||||
_checkProximityAlerts(newLocation);
|
||||
|
||||
|
||||
// Handle follow-me animations
|
||||
_handleFollowMeUpdate(position, newLocation);
|
||||
}
|
||||
@@ -239,13 +354,20 @@ class GpsController {
|
||||
void _checkProximityAlerts(LatLng userLocation) {
|
||||
final proximityEnabled = _getProximityAlertsEnabled?.call() ?? false;
|
||||
if (!proximityEnabled) return;
|
||||
|
||||
final nearbyNodes = _getNearbyNodes?.call() ?? [];
|
||||
if (nearbyNodes.isEmpty) return;
|
||||
|
||||
|
||||
final alertDistance = _getProximityAlertDistance?.call() ?? 200;
|
||||
|
||||
// Look up nodes around where the user actually is, not around whatever the
|
||||
// map camera happens to show. Backgrounded, the camera never moves, so the
|
||||
// old viewport-based lookup went stale the moment you drove out of it —
|
||||
// and even in the foreground it missed alerts whenever the map was panned
|
||||
// away from your position.
|
||||
final nearbyNodes = _getNearbyNodes?.call(userLocation, alertDistance) ?? [];
|
||||
if (nearbyNodes.isEmpty) return;
|
||||
|
||||
final enabledProfiles = _getEnabledProfiles?.call() ?? [];
|
||||
|
||||
|
||||
|
||||
ProximityAlertService().checkProximity(
|
||||
userLocation: userLocation,
|
||||
nodes: nearbyNodes,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../app_state.dart' show AppState, FollowMeMode;
|
||||
import '../services/offline_area_service.dart';
|
||||
import '../services/geo_bounds.dart';
|
||||
|
||||
import '../models/osm_node.dart';
|
||||
import '../models/suspected_location.dart';
|
||||
@@ -64,7 +65,7 @@ class MapView extends StatefulWidget {
|
||||
State<MapView> createState() => MapViewState();
|
||||
}
|
||||
|
||||
class MapViewState extends State<MapView> {
|
||||
class MapViewState extends State<MapView> with WidgetsBindingObserver {
|
||||
late final AnimatedMapController _controller;
|
||||
final Debouncer _cameraDebounce = Debouncer(kDebounceCameraRefresh);
|
||||
final Debouncer _tileDebounce = Debouncer(const Duration(milliseconds: 150));
|
||||
@@ -126,6 +127,8 @@ class MapViewState extends State<MapView> {
|
||||
_dataManager = MapDataManager();
|
||||
_interactionManager = MapInteractionManager();
|
||||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// Initialize proximity alert service
|
||||
ProximityAlertService().initialize(
|
||||
onVisualAlert: () {
|
||||
@@ -186,16 +189,16 @@ class MapViewState extends State<MapView> {
|
||||
}
|
||||
return 200;
|
||||
},
|
||||
getNearbyNodes: () {
|
||||
getNearbyNodes: (userLocation, radiusMeters) {
|
||||
if (mounted) {
|
||||
try {
|
||||
final LatLngBounds mapBounds;
|
||||
try {
|
||||
mapBounds = _controller.mapController.camera.visibleBounds;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
return NodeProviderWithCache.instance.getCachedNodesForBounds(mapBounds);
|
||||
// Deliberately not the map camera's visibleBounds: while the app
|
||||
// is backgrounded the camera is frozen wherever the user left it,
|
||||
// so a viewport lookup returns nodes from a place they may be
|
||||
// miles from by now. Box around the live GPS position instead.
|
||||
return NodeProviderWithCache.instance.getCachedNodesForBounds(
|
||||
boundsAround(userLocation, radiusMeters),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[MapView] Could not get nearby nodes: $e');
|
||||
return [];
|
||||
@@ -231,8 +234,18 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
// The position stream can go quiet across suspension, so pull a fresh fix
|
||||
// rather than trusting the last one we saw before backgrounding.
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_gpsController.refreshLocation();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_cameraDebounce.dispose();
|
||||
_tileDebounce.dispose();
|
||||
_mapPositionDebounce.dispose();
|
||||
@@ -329,6 +342,11 @@ class MapViewState extends State<MapView> {
|
||||
// Keep screen awake based on user setting
|
||||
_updateWakelock(appState.keepScreenAwake);
|
||||
|
||||
// Arm or drop background location delivery when the proximity alert
|
||||
// setting is toggled. No-op when the stream already matches, same as
|
||||
// _updateWakelock above.
|
||||
_gpsController.updateProximityAlertsEnabled(appState.proximityAlertsEnabled);
|
||||
|
||||
// Check if enabled profiles changed and refresh nodes if needed
|
||||
_nodeController.checkAndHandleProfileChanges(
|
||||
currentEnabledProfiles: appState.enabledProfiles,
|
||||
|
||||
Reference in New Issue
Block a user