diff --git a/README.md b/README.md index acf1215a..8ad65f99 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Live Map View**: Real-time visualization of mesh network nodes on an interactive map - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range - **Location Sharing**: Share GPS coordinates and custom markers with contacts -- **Offline Maps**: Download map tiles for offline use in remote areas +- **Offline Maps**: Download map tiles for offline use in remote areas (with [StadiaMaps](https://stadiamaps.com/pricing/) Free Subscription API-Key) - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format ### Device Management diff --git a/documentation/settings.md b/documentation/settings.md index 2e363ff6..8567cf4c 100644 --- a/documentation/settings.md +++ b/documentation/settings.md @@ -129,6 +129,10 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de - **Show Other Nodes**: Toggle room/sensor markers - **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week - **Units**: Metric / Imperial +- **Raster Tile Source**: Sets the MAP theme and with that from where to get the map tile data: + - OpenStreetMap (Auto/Standard/Dark) is provided by the free OpenStreetMap tile server. (Can only be used for live view or already cached.) + - Stamen Terrain / AlidadeSmooth Dark / Outdoors / OSM Bright are [StadiaMaps.com raster tile maps](https://stadiamaps.com/products/maps/interactive-basemaps/) for which you can choose an Hosted Endpoint (Worldwide / Europe hosted) and have to provide the API key to your subscription. (You can cache these maps for offline Map usage.) + There will be no account provided by meshcore-open. You will have to get you own subscription. StadiaMaps offers a [free](https://stadiamaps.com/pricing/) subscription to download up to 200'000 Standart Raster Basemap tiles. - **Offline Map Cache**: Navigate to tile download screen ### Translation diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 4d1b0538..b98e3c5d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Последни 6 часа", "appSettings_last24Hours": "Последно 24 часа", "appSettings_lastWeek": "Миналата седмица", + "appSettings_rasterTileSource": "Източник на растерни плочки", + "appSettings_stadiaEndpoint": "Крайна точка на Stadia", + "appSettings_stadiaApiKey": "API ключ за Stadia", + "appSettings_stadiaApiKeyRequired": "Задължително за използване на Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Конфигурирано: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.", "appSettings_offlineMapCache": "Кеш на офлайн карти", "appSettings_noAreaSelected": "Няма избрана област", "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", @@ -771,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c005c712..f5cc3685 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Letzte 6 Stunden", "appSettings_last24Hours": "Letzte 24 Stunden", "appSettings_lastWeek": "Letzte Woche", + "appSettings_rasterTileSource": "Quelle für Rasterkacheln", + "appSettings_stadiaEndpoint": "Stadia-Endpunkt", + "appSettings_stadiaApiKey": "Stadia-API-Schlüssel", + "appSettings_stadiaApiKeyRequired": "Für die Nutzung von Stadia Maps erforderlich", + "appSettings_stadiaApiKeyConfigured": "Konfiguriert: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.", "appSettings_offlineMapCache": "Offline-Karten-Cache", "appSettings_noAreaSelected": "Kein Bereich ausgewählt", "appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})", @@ -793,6 +806,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 637332fd..325a96c9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -363,6 +363,19 @@ "appSettings_last6Hours": "Last 6 hours", "appSettings_last24Hours": "Last 24 hours", "appSettings_lastWeek": "Last week", + "appSettings_rasterTileSource": "Raster Tile Source", + "appSettings_stadiaEndpoint": "Stadia Endpoint", + "appSettings_stadiaApiKey": "Stadia API Key", + "appSettings_stadiaApiKeyRequired": "Required for Stadia Maps usage", + "appSettings_stadiaApiKeyConfigured": "Configured: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Enter your Stadia Maps API key. This app uses it for raster tile requests.", "appSettings_offlineMapCache": "Offline Map Cache", "appSettings_unitsTitle": "Units", "appSettings_unitsMetric": "Metric (m / km)", @@ -1093,6 +1106,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a24fc034..f181ad1b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Últimas 6 horas", "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "La semana pasada", + "appSettings_rasterTileSource": "Fuente de teselas ráster", + "appSettings_stadiaEndpoint": "Punto de acceso de Stadia", + "appSettings_stadiaApiKey": "Clave API de Stadia", + "appSettings_stadiaApiKeyRequired": "Obligatorio para usar Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.", "appSettings_offlineMapCache": "Caché de mapa sin conexión", "appSettings_noAreaSelected": "No se ha seleccionado ningún área", "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", @@ -793,6 +806,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 705b279a..24dbf401 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Dernières 6 heures", "appSettings_last24Hours": "Dernières 24 heures", "appSettings_lastWeek": "La semaine dernière", + "appSettings_rasterTileSource": "Source de tuiles raster", + "appSettings_stadiaEndpoint": "Point de terminaison Stadia", + "appSettings_stadiaApiKey": "Clé API Stadia", + "appSettings_stadiaApiKeyRequired": "Requis pour l’utilisation de Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configuré : {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.", "appSettings_offlineMapCache": "Cache de carte hors ligne", "appSettings_noAreaSelected": "Aucune zone sélectionnée", "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", @@ -793,6 +806,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 20ef8197..c174c44c 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -334,6 +334,19 @@ "appSettings_last6Hours": "Utolsó 6 óra", "appSettings_last24Hours": "Az elmúlt 24 óra", "appSettings_lastWeek": "Múlt héten", + "appSettings_rasterTileSource": "Raszteres csempeforrás", + "appSettings_stadiaEndpoint": "Stadia végpont", + "appSettings_stadiaApiKey": "Stadia API-kulcs", + "appSettings_stadiaApiKeyRequired": "A Stadia Maps használatához kötelező", + "appSettings_stadiaApiKeyConfigured": "Konfigurálva: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.", "appSettings_offlineMapCache": "Offline térképgyorsítótár", "appSettings_unitsTitle": "Egységek", "appSettings_unitsMetric": "Metrikus (m/km)", @@ -954,6 +967,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 71f8c0ab..db0e118c 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -240,6 +240,19 @@ "appSettings_last6Hours": "Ultimi 6 ore", "appSettings_last24Hours": "Ultime 24 ore", "appSettings_lastWeek": "La settimana scorsa", + "appSettings_rasterTileSource": "Sorgente tile raster", + "appSettings_stadiaEndpoint": "Endpoint Stadia", + "appSettings_stadiaApiKey": "Chiave API Stadia", + "appSettings_stadiaApiKeyRequired": "Obbligatoria per usare Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configurata: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.", "appSettings_offlineMapCache": "Cache Mappa Offline", "appSettings_noAreaSelected": "Nessun'area selezionata", "appSettings_areaSelectedZoom": "Area selezionata (zoom {minZoom}-{maxZoom})", @@ -837,6 +850,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index ba3dc29a..51021664 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -340,6 +340,19 @@ "appSettings_last6Hours": "過去6時間", "appSettings_last24Hours": "過去24時間", "appSettings_lastWeek": "過去1週間", + "appSettings_rasterTileSource": "ラスタタイルのソース", + "appSettings_stadiaEndpoint": "Stadia エンドポイント", + "appSettings_stadiaApiKey": "Stadia API キー", + "appSettings_stadiaApiKeyRequired": "Stadia Maps の利用に必要です", + "appSettings_stadiaApiKeyConfigured": "設定済み: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。", "appSettings_offlineMapCache": "オフライン地図キャッシュ", "appSettings_unitsTitle": "単位", "appSettings_unitsMetric": "メートル法 (m / km)", @@ -972,6 +985,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 809b19bb..8db5120e 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -334,6 +334,19 @@ "appSettings_last6Hours": "지난 6시간", "appSettings_last24Hours": "지난 24시간", "appSettings_lastWeek": "지난 주", + "appSettings_rasterTileSource": "래스터 타일 소스", + "appSettings_stadiaEndpoint": "Stadia 엔드포인트", + "appSettings_stadiaApiKey": "Stadia API 키", + "appSettings_stadiaApiKeyRequired": "Stadia Maps를 사용하려면 필요합니다", + "appSettings_stadiaApiKeyConfigured": "설정됨: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.", "appSettings_offlineMapCache": "오프라인 지도 캐시", "appSettings_unitsTitle": "단위", "appSettings_unitsMetric": "미터법 (m / km)", @@ -954,6 +967,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e692313b..b20808dd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1804,6 +1804,42 @@ abstract class AppLocalizations { /// **'Last week'** String get appSettings_lastWeek; + /// No description provided for @appSettings_rasterTileSource. + /// + /// In en, this message translates to: + /// **'Raster Tile Source'** + String get appSettings_rasterTileSource; + + /// No description provided for @appSettings_stadiaEndpoint. + /// + /// In en, this message translates to: + /// **'Stadia Endpoint'** + String get appSettings_stadiaEndpoint; + + /// No description provided for @appSettings_stadiaApiKey. + /// + /// In en, this message translates to: + /// **'Stadia API Key'** + String get appSettings_stadiaApiKey; + + /// No description provided for @appSettings_stadiaApiKeyRequired. + /// + /// In en, this message translates to: + /// **'Required for Stadia Maps usage'** + String get appSettings_stadiaApiKeyRequired; + + /// No description provided for @appSettings_stadiaApiKeyConfigured. + /// + /// In en, this message translates to: + /// **'Configured: {maskedKey}'** + String appSettings_stadiaApiKeyConfigured(String maskedKey); + + /// No description provided for @appSettings_stadiaApiKeyDialogDescription. + /// + /// In en, this message translates to: + /// **'Enter your Stadia Maps API key. This app uses it for raster tile requests.'** + String get appSettings_stadiaApiKeyDialogDescription; + /// No description provided for @appSettings_offlineMapCache. /// /// In en, this message translates to: @@ -3808,6 +3844,54 @@ abstract class AppLocalizations { /// **'Failed downloads: {count}'** String mapCache_failedDownloads(int count); + /// No description provided for @mapCache_cachedTilesLabel. + /// + /// In en, this message translates to: + /// **'Cached tiles'** + String get mapCache_cachedTilesLabel; + + /// No description provided for @mapCache_cachedTileSummaryLabel. + /// + /// In en, this message translates to: + /// **'Cached tile summary'** + String get mapCache_cachedTileSummaryLabel; + + /// No description provided for @mapCache_bulkDownloadDisabledForSource. + /// + /// In en, this message translates to: + /// **'Offline bulk downloads are disabled for {source}.'** + String mapCache_bulkDownloadDisabledForSource(String source); + + /// No description provided for @mapCache_bulkDownloadDisabledInConfig. + /// + /// In en, this message translates to: + /// **'Offline bulk downloads are disabled for {source} in this app configuration.'** + String mapCache_bulkDownloadDisabledInConfig(String source); + + /// No description provided for @mapCache_summarySource. + /// + /// In en, this message translates to: + /// **'Source: {source}'** + String mapCache_summarySource(String source); + + /// No description provided for @mapCache_summaryCachedTilesForSource. + /// + /// In en, this message translates to: + /// **'Cached tiles for source: {count}'** + String mapCache_summaryCachedTilesForSource(int count); + + /// No description provided for @mapCache_summaryCachedInSelection. + /// + /// In en, this message translates to: + /// **'Cached in selected area/zoom: {count}'** + String mapCache_summaryCachedInSelection(int count); + + /// No description provided for @mapCache_summaryApproxCacheSize. + /// + /// In en, this message translates to: + /// **'Approx cache size: {size}'** + String mapCache_summaryApproxCacheSize(String size); + /// No description provided for @mapCache_boundsLabel. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index ee22d67b..9fc2e22b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -947,6 +947,28 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_lastWeek => 'Миналата седмица'; + @override + String get appSettings_rasterTileSource => 'Източник на растерни плочки'; + + @override + String get appSettings_stadiaEndpoint => 'Крайна точка на Stadia'; + + @override + String get appSettings_stadiaApiKey => 'API ключ за Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Задължително за използване на Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Конфигурирано: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.'; + @override String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; @@ -2098,6 +2120,42 @@ class AppLocalizationsBg extends AppLocalizations { return 'Неуспешни изтегляния: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 432838c8..05ae2ac1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -944,6 +944,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_lastWeek => 'Letzte Woche'; + @override + String get appSettings_rasterTileSource => 'Quelle für Rasterkacheln'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia-Endpunkt'; + + @override + String get appSettings_stadiaApiKey => 'Stadia-API-Schlüssel'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Für die Nutzung von Stadia Maps erforderlich'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Konfiguriert: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.'; + @override String get appSettings_offlineMapCache => 'Offline-Karten-Cache'; @@ -2098,6 +2120,42 @@ class AppLocalizationsDe extends AppLocalizations { return 'Fehlgeschlagene Downloads: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a0463e45..d4eac6c8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -927,6 +927,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_lastWeek => 'Last week'; + @override + String get appSettings_rasterTileSource => 'Raster Tile Source'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia Endpoint'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API Key'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Required for Stadia Maps usage'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configured: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Enter your Stadia Maps API key. This app uses it for raster tile requests.'; + @override String get appSettings_offlineMapCache => 'Offline Map Cache'; @@ -2059,6 +2081,42 @@ class AppLocalizationsEn extends AppLocalizations { return 'Failed downloads: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8bd9eb64..ee8dfa87 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -941,6 +941,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_lastWeek => 'La semana pasada'; + @override + String get appSettings_rasterTileSource => 'Fuente de teselas ráster'; + + @override + String get appSettings_stadiaEndpoint => 'Punto de acceso de Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Clave API de Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obligatorio para usar Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configurado: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.'; + @override String get appSettings_offlineMapCache => 'Caché de mapa sin conexión'; @@ -2092,6 +2114,42 @@ class AppLocalizationsEs extends AppLocalizations { return 'Descargas fallidas: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 5dacff6f..77e92600 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -945,6 +945,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_lastWeek => 'La semaine dernière'; + @override + String get appSettings_rasterTileSource => 'Source de tuiles raster'; + + @override + String get appSettings_stadiaEndpoint => 'Point de terminaison Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Clé API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Requis pour l’utilisation de Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configuré : $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.'; + @override String get appSettings_offlineMapCache => 'Cache de carte hors ligne'; @@ -2099,6 +2121,42 @@ class AppLocalizationsFr extends AppLocalizations { return 'Téléchargements échoués : $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 480c8b0a..43640852 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -937,6 +937,28 @@ class AppLocalizationsHu extends AppLocalizations { @override String get appSettings_lastWeek => 'Múlt héten'; + @override + String get appSettings_rasterTileSource => 'Raszteres csempeforrás'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia végpont'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API-kulcs'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'A Stadia Maps használatához kötelező'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Konfigurálva: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.'; + @override String get appSettings_offlineMapCache => 'Offline térképgyorsítótár'; @@ -2090,6 +2112,42 @@ class AppLocalizationsHu extends AppLocalizations { return 'Sikertelen letöltések: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 4b796fd6..b8931b74 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -944,6 +944,28 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_lastWeek => 'La settimana scorsa'; + @override + String get appSettings_rasterTileSource => 'Sorgente tile raster'; + + @override + String get appSettings_stadiaEndpoint => 'Endpoint Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Chiave API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obbligatoria per usare Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configurata: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.'; + @override String get appSettings_offlineMapCache => 'Cache Mappa Offline'; @@ -2094,6 +2116,42 @@ class AppLocalizationsIt extends AppLocalizations { return 'Download falliti: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index bdcd9f58..59300f32 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -891,6 +891,27 @@ class AppLocalizationsJa extends AppLocalizations { @override String get appSettings_lastWeek => '過去1週間'; + @override + String get appSettings_rasterTileSource => 'ラスタタイルのソース'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia エンドポイント'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API キー'; + + @override + String get appSettings_stadiaApiKeyRequired => 'Stadia Maps の利用に必要です'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return '設定済み: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。'; + @override String get appSettings_offlineMapCache => 'オフライン地図キャッシュ'; @@ -2006,6 +2027,42 @@ class AppLocalizationsJa extends AppLocalizations { return '失敗したダウンロード: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3fb3fa54..c305f7f2 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -894,6 +894,27 @@ class AppLocalizationsKo extends AppLocalizations { @override String get appSettings_lastWeek => '지난 주'; + @override + String get appSettings_rasterTileSource => '래스터 타일 소스'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia 엔드포인트'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API 키'; + + @override + String get appSettings_stadiaApiKeyRequired => 'Stadia Maps를 사용하려면 필요합니다'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return '설정됨: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.'; + @override String get appSettings_offlineMapCache => '오프라인 지도 캐시'; @@ -2010,6 +2031,42 @@ class AppLocalizationsKo extends AppLocalizations { return '실패한 다운로드: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index c925be1b..fdd98eb6 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -936,6 +936,28 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_lastWeek => 'Afgelopen week'; + @override + String get appSettings_rasterTileSource => 'Rastertegelbron'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia-eindpunt'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API-sleutel'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Vereist voor gebruik van Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Geconfigureerd: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.'; + @override String get appSettings_offlineMapCache => 'Offline Kaartcache'; @@ -2082,6 +2104,42 @@ class AppLocalizationsNl extends AppLocalizations { return 'Mislukte downloads: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 4f4e55cb..2ae0261f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -945,6 +945,28 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_lastWeek => 'Ostatni tydzień'; + @override + String get appSettings_rasterTileSource => 'Źródło kafelków rastrowych'; + + @override + String get appSettings_stadiaEndpoint => 'Punkt końcowy Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Klucz API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Wymagane do korzystania ze Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Skonfigurowano: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.'; + @override String get appSettings_offlineMapCache => 'Pamięć podręczna map offline'; @@ -2112,6 +2134,42 @@ class AppLocalizationsPl extends AppLocalizations { return 'Nieudane pobrania: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index cec4f35d..d3f70fec 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -942,6 +942,28 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_lastWeek => 'Da última semana'; + @override + String get appSettings_rasterTileSource => 'Fonte de blocos raster'; + + @override + String get appSettings_stadiaEndpoint => 'Endpoint da Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Chave da API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obrigatório para usar o Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Configurado: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.'; + @override String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; @@ -2095,6 +2117,42 @@ class AppLocalizationsPt extends AppLocalizations { return 'Downloads falhas: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 62fa5442..23d868c1 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -945,6 +945,28 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_lastWeek => 'Последнюю неделю'; + @override + String get appSettings_rasterTileSource => 'Источник растровых тайлов'; + + @override + String get appSettings_stadiaEndpoint => 'Конечная точка Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Ключ API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Требуется для использования Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Настроено: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.'; + @override String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; @@ -2099,6 +2121,42 @@ class AppLocalizationsRu extends AppLocalizations { return 'Неудачных загрузок: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 9c2dffca..fa1eaed9 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -932,6 +932,28 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_lastWeek => 'Minul týždeň'; + @override + String get appSettings_rasterTileSource => 'Zdroj rastrových dlaždíc'; + + @override + String get appSettings_stadiaEndpoint => 'Koncový bod Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Kľúč API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Vyžaduje sa na používanie Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Nakonfigurované: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.'; + @override String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; @@ -2085,6 +2107,42 @@ class AppLocalizationsSk extends AppLocalizations { return 'Neúspešné stiahnutia: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 42af93e9..315f63fd 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -933,6 +933,28 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_lastWeek => 'Prejšnji teden'; + @override + String get appSettings_rasterTileSource => 'Vir rastrskih ploščic'; + + @override + String get appSettings_stadiaEndpoint => 'Končna točka Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Ključ API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Obvezno za uporabo Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Nastavljeno: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.'; + @override String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; @@ -2082,6 +2104,42 @@ class AppLocalizationsSl extends AppLocalizations { return 'Poslovniški izniki: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 2621192c..13f2c230 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -926,6 +926,28 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_lastWeek => 'Förra veckan'; + @override + String get appSettings_rasterTileSource => 'Källa för rasterplattor'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia-slutpunkt'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API-nyckel'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Krävs för att använda Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Konfigurerad: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.'; + @override String get appSettings_offlineMapCache => 'Offline Kartcache'; @@ -2070,6 +2092,42 @@ class AppLocalizationsSv extends AppLocalizations { return 'Misslyckade nedladdningar: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 39a4f3a7..a26b0892 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -939,6 +939,28 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_lastWeek => 'Минулий тиждень'; + @override + String get appSettings_rasterTileSource => 'Джерело растрових тайлів'; + + @override + String get appSettings_stadiaEndpoint => 'Кінцева точка Stadia'; + + @override + String get appSettings_stadiaApiKey => 'Ключ API Stadia'; + + @override + String get appSettings_stadiaApiKeyRequired => + 'Потрібно для використання Stadia Maps'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return 'Налаштовано: $maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + 'Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.'; + @override String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; @@ -2092,6 +2114,42 @@ class AppLocalizationsUk extends AppLocalizations { return 'Невдалі завантаження: $count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 95f0c658..b98bd442 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -878,6 +878,27 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_lastWeek => '上周'; + @override + String get appSettings_rasterTileSource => '栅格瓦片源'; + + @override + String get appSettings_stadiaEndpoint => 'Stadia 端点'; + + @override + String get appSettings_stadiaApiKey => 'Stadia API 密钥'; + + @override + String get appSettings_stadiaApiKeyRequired => '使用 Stadia Maps 时必需'; + + @override + String appSettings_stadiaApiKeyConfigured(String maskedKey) { + return '已配置:$maskedKey'; + } + + @override + String get appSettings_stadiaApiKeyDialogDescription => + '请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。'; + @override String get appSettings_offlineMapCache => '离线地图缓存'; @@ -1985,6 +2006,42 @@ class AppLocalizationsZh extends AppLocalizations { return '下载失败:$count'; } + @override + String get mapCache_cachedTilesLabel => 'Cached tiles'; + + @override + String get mapCache_cachedTileSummaryLabel => 'Cached tile summary'; + + @override + String mapCache_bulkDownloadDisabledForSource(String source) { + return 'Offline bulk downloads are disabled for $source.'; + } + + @override + String mapCache_bulkDownloadDisabledInConfig(String source) { + return 'Offline bulk downloads are disabled for $source in this app configuration.'; + } + + @override + String mapCache_summarySource(String source) { + return 'Source: $source'; + } + + @override + String mapCache_summaryCachedTilesForSource(int count) { + return 'Cached tiles for source: $count'; + } + + @override + String mapCache_summaryCachedInSelection(int count) { + return 'Cached in selected area/zoom: $count'; + } + + @override + String mapCache_summaryApproxCacheSize(String size) { + return 'Approx cache size: $size'; + } + @override String mapCache_boundsLabel( String north, diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index f6f574de..335af8d7 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Afgelopen 6 uur", "appSettings_last24Hours": "Afgelopen 24 uur", "appSettings_lastWeek": "Afgelopen week", + "appSettings_rasterTileSource": "Rastertegelbron", + "appSettings_stadiaEndpoint": "Stadia-eindpunt", + "appSettings_stadiaApiKey": "Stadia API-sleutel", + "appSettings_stadiaApiKeyRequired": "Vereist voor gebruik van Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Geconfigureerd: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.", "appSettings_offlineMapCache": "Offline Kaartcache", "appSettings_noAreaSelected": "Geen gebied geselecteerd", "appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})", @@ -771,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 7f03d14b..8802e1fa 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Ostatnie 6 godzin", "appSettings_last24Hours": "Ostatnie 24 godziny", "appSettings_lastWeek": "Ostatni tydzień", + "appSettings_rasterTileSource": "Źródło kafelków rastrowych", + "appSettings_stadiaEndpoint": "Punkt końcowy Stadia", + "appSettings_stadiaApiKey": "Klucz API Stadia", + "appSettings_stadiaApiKeyRequired": "Wymagane do korzystania ze Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Skonfigurowano: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.", "appSettings_offlineMapCache": "Pamięć podręczna map offline", "appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.", "appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})", @@ -781,6 +794,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8eb79a9d..d1716ae2 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Últimos 6 horas", "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "Da última semana", + "appSettings_rasterTileSource": "Fonte de blocos raster", + "appSettings_stadiaEndpoint": "Endpoint da Stadia", + "appSettings_stadiaApiKey": "Chave da API Stadia", + "appSettings_stadiaApiKeyRequired": "Obrigatório para usar o Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.", "appSettings_offlineMapCache": "Cache de Mapa Offline", "appSettings_noAreaSelected": "Nenhuma área selecionada", "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", @@ -771,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 677339c5..23afe703 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -192,6 +192,19 @@ "appSettings_last6Hours": "Последние 6 часов", "appSettings_last24Hours": "Последние 24 часа", "appSettings_lastWeek": "Последнюю неделю", + "appSettings_rasterTileSource": "Источник растровых тайлов", + "appSettings_stadiaEndpoint": "Конечная точка Stadia", + "appSettings_stadiaApiKey": "Ключ API Stadia", + "appSettings_stadiaApiKeyRequired": "Требуется для использования Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Настроено: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.", "appSettings_offlineMapCache": "Кэш офлайн-карты", "appSettings_noAreaSelected": "Область не выбрана", "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", @@ -440,6 +453,56 @@ "mapCache_downloadTilesButton": "Загрузить плитки", "mapCache_clearCacheButton": "Очистить кэш", "mapCache_failedDownloads": "Неудачных загрузок: {count}", + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", "time_justNow": "Только что", "time_minutesAgo": "{minutes} мин назад", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index f2e8cb72..a50c4c7f 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Posledné 6 hodín", "appSettings_last24Hours": "Posledných 24 hodín", "appSettings_lastWeek": "Minul týždeň", + "appSettings_rasterTileSource": "Zdroj rastrových dlaždíc", + "appSettings_stadiaEndpoint": "Koncový bod Stadia", + "appSettings_stadiaApiKey": "Kľúč API Stadia", + "appSettings_stadiaApiKeyRequired": "Vyžaduje sa na používanie Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Nakonfigurované: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.", "appSettings_offlineMapCache": "Offline Mapa Pamäť", "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť", "appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})", @@ -771,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6b2ea8cf..ab3fd701 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "Zadnjih 6 ur", "appSettings_last24Hours": "Zadnjih 24 ur", "appSettings_lastWeek": "Prejšnji teden", + "appSettings_rasterTileSource": "Vir rastrskih ploščic", + "appSettings_stadiaEndpoint": "Končna točka Stadia", + "appSettings_stadiaApiKey": "Ključ API Stadia", + "appSettings_stadiaApiKeyRequired": "Obvezno za uporabo Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Nastavljeno: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.", "appSettings_offlineMapCache": "Shramba zemljevidov brez povezave", "appSettings_noAreaSelected": "Območje ni izbrano", "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", @@ -771,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index a25b9ec0..51ac5d24 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -242,6 +242,19 @@ "appSettings_last6Hours": "De senaste 6 timmarna", "appSettings_last24Hours": "De senaste 24 timmarna", "appSettings_lastWeek": "Förra veckan", + "appSettings_rasterTileSource": "Källa för rasterplattor", + "appSettings_stadiaEndpoint": "Stadia-slutpunkt", + "appSettings_stadiaApiKey": "Stadia API-nyckel", + "appSettings_stadiaApiKeyRequired": "Krävs för att använda Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Konfigurerad: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.", "appSettings_offlineMapCache": "Offline Kartcache", "appSettings_noAreaSelected": "Ingen area markerad", "appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})", @@ -771,6 +784,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 0de853a7..df3195a6 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -244,6 +244,19 @@ "appSettings_last6Hours": "Останні 6 годин", "appSettings_last24Hours": "Останні 24 години", "appSettings_lastWeek": "Минулий тиждень", + "appSettings_rasterTileSource": "Джерело растрових тайлів", + "appSettings_stadiaEndpoint": "Кінцева точка Stadia", + "appSettings_stadiaApiKey": "Ключ API Stadia", + "appSettings_stadiaApiKeyRequired": "Потрібно для використання Stadia Maps", + "appSettings_stadiaApiKeyConfigured": "Налаштовано: {maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.", "appSettings_offlineMapCache": "Офлайн-кеш карти", "appSettings_noAreaSelected": "Область не вибрано", "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", @@ -781,6 +794,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 3f3a9d51..1becea8c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -256,6 +256,19 @@ "appSettings_last6Hours": "过去6小时", "appSettings_last24Hours": "过去24小时", "appSettings_lastWeek": "上周", + "appSettings_rasterTileSource": "栅格瓦片源", + "appSettings_stadiaEndpoint": "Stadia 端点", + "appSettings_stadiaApiKey": "Stadia API 密钥", + "appSettings_stadiaApiKeyRequired": "使用 Stadia Maps 时必需", + "appSettings_stadiaApiKeyConfigured": "已配置:{maskedKey}", + "@appSettings_stadiaApiKeyConfigured": { + "placeholders": { + "maskedKey": { + "type": "String" + } + } + }, + "appSettings_stadiaApiKeyDialogDescription": "请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。", "appSettings_offlineMapCache": "离线地图缓存", "appSettings_noAreaSelected": "未选择任何区域", "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", @@ -798,6 +811,56 @@ } } }, + "mapCache_cachedTilesLabel": "Cached tiles", + "mapCache_cachedTileSummaryLabel": "Cached tile summary", + "mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.", + "@mapCache_bulkDownloadDisabledForSource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.", + "@mapCache_bulkDownloadDisabledInConfig": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summarySource": "Source: {source}", + "@mapCache_summarySource": { + "placeholders": { + "source": { + "type": "String" + } + } + }, + "mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}", + "@mapCache_summaryCachedTilesForSource": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}", + "@mapCache_summaryCachedInSelection": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_summaryApproxCacheSize": "Approx cache size: {size}", + "@mapCache_summaryApproxCacheSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "@mapCache_boundsLabel": { "placeholders": { diff --git a/lib/main.dart b/lib/main.dart index 1445bdde..7cabda17 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -51,7 +51,9 @@ void main() async { final bleDebugLogService = BleDebugLogService(); final appDebugLogService = AppDebugLogService(); final backgroundService = BackgroundService(); - final mapTileCacheService = MapTileCacheService(); + final mapTileCacheService = MapTileCacheService( + appSettingsService: appSettingsService, + ); final chatTextScaleService = ChatTextScaleService(); final translationService = TranslationService(appSettingsService); final uiViewStateService = UiViewStateService(); @@ -183,7 +185,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: translationService), ChangeNotifierProvider.value(value: uiViewStateService), Provider.value(value: storage), - Provider.value(value: mapTileCacheService), + ChangeNotifierProvider.value(value: mapTileCacheService), ChangeNotifierProvider.value(value: timeoutPredictionService), ], child: Consumer( diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index aa435f13..a6f82100 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -76,6 +76,8 @@ class Cyr2LatProfile { class AppSettings { static const Object _unset = Object(); + static const String stadiaDemo = + '51bd0381-4685-4666-bae8-48940f6d77c0'; final bool clearPathOnMaxRetry; final bool mapShowRepeaters; @@ -91,6 +93,9 @@ class AppSettings { final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; + final String mapRasterSourceId; + final String mapTileEndpointId; + final String? mapTileApiKey; final bool notificationsEnabled; final bool notifyOnNewMessage; final bool notifyOnNewChannelMessage; @@ -124,6 +129,17 @@ class AppSettings { final List cyr2latProfiles; final String selectedCyr2latProfileId; + String get effectiveMapTileApiKey { + final apiKey = mapTileApiKey?.trim(); + if (apiKey == null || apiKey.isEmpty) { + return stadiaDemo; + } + return apiKey; + } + + bool get usesstadiaDemo => + effectiveMapTileApiKey == stadiaDemo; + Map get cyr2latCharMap { final profile = cyr2latProfiles.firstWhere( (p) => p.id == selectedCyr2latProfileId, @@ -147,6 +163,9 @@ class AppSettings { this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, + this.mapRasterSourceId = 'osm_auto', + this.mapTileEndpointId = 'standard_2x', + this.mapTileApiKey, this.notificationsEnabled = true, this.notifyOnNewMessage = true, this.notifyOnNewChannelMessage = true, @@ -210,6 +229,9 @@ class AppSettings { 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, + 'map_raster_source_id': mapRasterSourceId, + 'map_tile_endpoint_id': mapTileEndpointId, + 'map_tile_api_key': mapTileApiKey, 'notifications_enabled': notificationsEnabled, 'notify_on_new_message': notifyOnNewMessage, 'notify_on_new_channel_message': notifyOnNewChannelMessage, @@ -277,6 +299,9 @@ class AppSettings { ), mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10, mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, + mapRasterSourceId: json['map_raster_source_id'] as String? ?? 'osm_auto', + mapTileEndpointId: json['map_tile_endpoint_id'] as String? ?? 'standard', + mapTileApiKey: json['map_tile_api_key'] as String?, notificationsEnabled: json['notifications_enabled'] as bool? ?? true, notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true, notifyOnNewChannelMessage: @@ -390,6 +415,9 @@ class AppSettings { Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, + String? mapRasterSourceId, + String? mapTileEndpointId, + Object? mapTileApiKey = _unset, bool? notificationsEnabled, bool? notifyOnNewMessage, bool? notifyOnNewChannelMessage, @@ -441,6 +469,11 @@ class AppSettings { : mapCacheBounds as Map?, mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, + mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId, + mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId, + mapTileApiKey: mapTileApiKey == _unset + ? this.mapTileApiKey + : mapTileApiKey as String?, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage, notifyOnNewChannelMessage: diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index e54687c9..90a0a132 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -8,6 +8,7 @@ import '../l10n/l10n.dart'; import '../models/app_settings.dart'; import '../models/translation_support.dart'; import '../services/app_settings_service.dart'; +import '../services/map_tile_cache_service.dart'; import '../services/notification_service.dart'; import '../services/translation_service.dart'; import '../theme/mesh_theme.dart'; @@ -699,52 +700,229 @@ class AppSettingsScreen extends StatelessWidget { ) { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; - return Column( - children: [ - SwitchListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, + final children = [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.router_outlined, size: 20), + title: Text(context.l10n.appSettings_showRepeaters), + subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle), + value: settingsService.settings.mapShowRepeaters, + onChanged: (value) => settingsService.setMapShowRepeaters(value), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.chat_outlined, size: 20), + title: Text(context.l10n.appSettings_showChatNodes), + subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle), + value: settingsService.settings.mapShowChatNodes, + onChanged: (value) => settingsService.setMapShowChatNodes(value), + ), + const Divider(height: 1, indent: 16), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + secondary: const Icon(Icons.people_outline, size: 20), + title: Text(context.l10n.appSettings_showOtherNodes), + subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle), + value: settingsService.settings.mapShowOtherNodes, + onChanged: (value) => settingsService.setMapShowOtherNodes(value), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showTimeFilterSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.timer_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_timeFilter, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.mapTimeFilterHours == 0 + ? context.l10n.appSettings_timeFilterShowAll + : context.l10n.appSettings_timeFilterShowLast( + settingsService.settings.mapTimeFilterHours + .toInt(), + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - secondary: const Icon(Icons.router_outlined, size: 20), - title: Text(context.l10n.appSettings_showRepeaters), - subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle), - value: settingsService.settings.mapShowRepeaters, - onChanged: (value) => settingsService.setMapShowRepeaters(value), ), - const Divider(height: 1, indent: 16), - SwitchListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showUnitsSheet(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(Icons.straighten, size: 20, color: scheme.onSurfaceVariant), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_unitsTitle, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.unitSystem == UnitSystem.imperial + ? context.l10n.appSettings_unitsImperial + : context.l10n.appSettings_unitsMetric, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - secondary: const Icon(Icons.chat_outlined, size: 20), - title: Text(context.l10n.appSettings_showChatNodes), - subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle), - value: settingsService.settings.mapShowChatNodes, - onChanged: (value) => settingsService.setMapShowChatNodes(value), ), - const Divider(height: 1, indent: 16), - SwitchListTile( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MapCacheScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.download_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_offlineMapCache, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + settingsService.settings.mapCacheBounds == null + ? context.l10n.appSettings_noAreaSelected + : context.l10n.appSettings_areaSelectedZoom( + settingsService.settings.mapCacheMinZoom, + settingsService.settings.mapCacheMaxZoom, + ), + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], ), - secondary: const Icon(Icons.people_outline, size: 20), - title: Text(context.l10n.appSettings_showOtherNodes), - subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle), - value: settingsService.settings.mapShowOtherNodes, - onChanged: (value) => settingsService.setMapShowOtherNodes(value), ), + ), + const Divider(height: 1, indent: 16), + InkWell( + onTap: () => _showMapRasterSourceDialog(context, settingsService), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon( + Icons.layers_outlined, + size: 20, + color: scheme.onSurfaceVariant, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.appSettings_rasterTileSource, + style: textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + _mapRasterSourceSummary(settingsService.settings), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall?.copyWith( + color: scheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: scheme.onSurfaceVariant, + size: 16, + ), + ], + ), + ), + ), + ]; + + if (_isStadiaSource(settingsService.settings)) { + children.addAll([ const Divider(height: 1, indent: 16), InkWell( - onTap: () => _showTimeFilterSheet(context, settingsService), + onTap: () => _showMapRasterEndpointDialog(context, settingsService), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Icon( - Icons.timer_outlined, + Icons.public_outlined, size: 20, color: scheme.onSurfaceVariant, ), @@ -754,19 +932,14 @@ class AppSettingsScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.appSettings_timeFilter, + context.l10n.appSettings_stadiaEndpoint, style: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( - settingsService.settings.mapTimeFilterHours == 0 - ? context.l10n.appSettings_timeFilterShowAll - : context.l10n.appSettings_timeFilterShowLast( - settingsService.settings.mapTimeFilterHours - .toInt(), - ), + _mapRasterEndpointSummary(settingsService.settings), style: textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), @@ -785,13 +958,13 @@ class AppSettingsScreen extends StatelessWidget { ), const Divider(height: 1, indent: 16), InkWell( - onTap: () => _showUnitsSheet(context, settingsService), + onTap: () => _showMapApiKeyDialog(context, settingsService), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Icon( - Icons.straighten, + Icons.key_outlined, size: 20, color: scheme.onSurfaceVariant, ), @@ -801,17 +974,14 @@ class AppSettingsScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.appSettings_unitsTitle, + context.l10n.appSettings_stadiaApiKey, style: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( - settingsService.settings.unitSystem == - UnitSystem.imperial - ? context.l10n.appSettings_unitsImperial - : context.l10n.appSettings_unitsMetric, + _mapApiKeySummary(context, settingsService.settings), style: textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), @@ -828,59 +998,210 @@ class AppSettingsScreen extends StatelessWidget { ), ), ), - const Divider(height: 1, indent: 16), - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const MapCacheScreen()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Icon( - Icons.download_outlined, - size: 20, - color: scheme.onSurfaceVariant, - ), - const SizedBox(width: 12), - Expanded( + ]); + } + + return Column(children: children); + } + + String _mapRasterSourceSummary(AppSettings settings) { + final source = MapRasterSourceCatalog.fromSettings(settings); + return '${source.label} - ${source.description}'; + } + + bool _isStadiaSource(AppSettings settings) { + return MapRasterSourceCatalog.fromSettings(settings).isStadia; + } + + String _mapRasterEndpointSummary(AppSettings settings) { + final endpoint = MapRasterEndpointCatalog.fromSettings(settings); + return '${endpoint.label} - ${endpoint.description}'; + } + + String _mapApiKeySummary(BuildContext context, AppSettings settings) { + return context.l10n.appSettings_stadiaApiKeyConfigured( + _maskApiKey(settings.effectiveMapTileApiKey), + ); + } + + String _maskApiKey(String value) { + if (value.length <= 8) return '********'; + return '${value.substring(0, 4)}...${value.substring(value.length - 4)}'; + } + + void _showMapRasterSourceDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + String selectedId = settingsService.settings.mapRasterSourceId; + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setState) => AlertDialog( + title: Text(context.l10n.appSettings_rasterTileSource), + content: SizedBox( + width: 360, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(dialogContext).size.height * 0.6, + ), + child: SingleChildScrollView( + child: RadioGroup( + groupValue: selectedId, + onChanged: (value) { + if (value == null) return; + setState(() { + selectedId = value; + }); + }, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Text( - context.l10n.appSettings_offlineMapCache, - style: textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, + for (final preset in MapRasterSourcePreset.values) + Builder( + builder: (context) { + final option = MapRasterSourceCatalog.fromPreset( + preset, + ); + return RadioListTile( + value: preset.id, + title: Text(option.label), + subtitle: Text(option.description), + ); + }, ), - ), - const SizedBox(height: 2), - Text( - settingsService.settings.mapCacheBounds == null - ? context.l10n.appSettings_noAreaSelected - : context.l10n.appSettings_areaSelectedZoom( - settingsService.settings.mapCacheMinZoom, - settingsService.settings.mapCacheMaxZoom, - ), - style: textTheme.bodySmall?.copyWith( - color: scheme.onSurfaceVariant, - ), - ), ], ), ), - Icon( - Icons.chevron_right, - color: scheme.onSurfaceVariant, - size: 16, - ), - ], + ), ), ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + await settingsService.setMapRasterSourceId(selectedId); + if (!dialogContext.mounted) return; + Navigator.pop(dialogContext); + }, + child: Text(context.l10n.common_save), + ), + ], ), - ], + ), + ); + } + + void _showMapRasterEndpointDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + String selectedId = settingsService.settings.mapTileEndpointId; + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setState) => AlertDialog( + title: Text(context.l10n.appSettings_stadiaEndpoint), + content: SizedBox( + width: 360, + child: RadioGroup( + groupValue: selectedId, + onChanged: (value) { + if (value == null) return; + setState(() { + selectedId = value; + }); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final option in MapRasterEndpointCatalog.presets) + RadioListTile( + value: option.id, + title: Text(option.label), + subtitle: Text(option.description), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + await settingsService.setMapTileEndpointId(selectedId); + if (!dialogContext.mounted) return; + Navigator.pop(dialogContext); + }, + child: Text(context.l10n.common_save), + ), + ], + ), + ), + ); + } + + void _showMapApiKeyDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + final currentApiKey = settingsService.settings.mapTileApiKey?.trim() ?? ''; + final maskedApiKey = _maskApiKey( + currentApiKey.isEmpty + ? AppSettings.stadiaDemo + : currentApiKey, + ); + final controller = TextEditingController(text: maskedApiKey); + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.appSettings_stadiaApiKey), + content: SizedBox( + width: 420, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_stadiaApiKeyDialogDescription), + const SizedBox(height: 12), + TextField( + controller: controller, + autofocus: true, + autocorrect: false, + enableSuggestions: false, + autofillHints: const [AutofillHints.password], + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: '4e1bf343-3d91-4d9c-a8e1-1234567890ab', + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + final apiKey = controller.text.trim(); + await settingsService.setMapTileApiKey( + apiKey == maskedApiKey ? currentApiKey : apiKey, + ); + if (!dialogContext.mounted) return; + Navigator.pop(dialogContext); + }, + child: Text(context.l10n.common_save), + ), + ], + ), ); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index bc2ca1bd..3292e76f 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -22,7 +22,6 @@ import '../theme/mesh_theme.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/mesh_ui.dart'; import '../widgets/path_map_ui.dart'; -import '../widgets/themed_map_tile_layer.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; @@ -967,7 +966,7 @@ class _ChannelMessagePathMapScreenState }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), AnimatedBuilder( animation: _playback, builder: (context, _) { diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 2de232c6..d9846db3 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -282,8 +282,6 @@ class _ChannelsScreenState extends State buildDefaultDragHandles: false, itemCount: filteredChannels.length, onReorderItem: (oldIndex, newIndex) { - // onReorderItem already adjusts newIndex after the - // removed item, unlike the deprecated onReorder. final reordered = List.from( filteredChannels, ); diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index fe935e43..7117affa 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -20,7 +20,6 @@ import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; import '../icons/los_icon.dart'; import '../theme/mesh_theme.dart'; -import '../widgets/themed_map_tile_layer.dart'; class LineOfSightEndpoint { final String label; @@ -357,7 +356,7 @@ class _LineOfSightMapScreenState extends State { Widget build(BuildContext context) { final settings = context.watch().settings; final isImperial = settings.unitSystem == UnitSystem.imperial; - final tileCache = context.read(); + final tileCache = context.watch(); final endpoints = _visibleEndpoints(); final mapPoints = [ if (_start != null) _start!.point, @@ -443,8 +442,8 @@ class _LineOfSightMapScreenState extends State { }, ), children: [ - ThemedMapTileLayer( - tileCache: tileCache, + tileCache.buildTileLayer( + context, opacity: _showTerrainLayer ? 1 : 0.72, ), if (_result != null && _result!.segments.isNotEmpty) diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 6455c15c..9cb45d36 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -12,7 +12,6 @@ import '../widgets/adaptive_app_bar_title.dart'; import '../helpers/snack_bar_builder.dart'; import '../theme/mesh_theme.dart'; import '../widgets/mesh_ui.dart'; -import '../widgets/themed_map_tile_layer.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -34,6 +33,11 @@ class _MapCacheScreenState extends State { bool _isDownloading = false; int _completedTiles = 0; int _failedTiles = 0; + List _cachedTiles = const []; + int _cachedTileBytes = 0; + bool _isLoadingCachedTiles = false; + double _overlayZoom = 2.0; + LatLngBounds? _visibleBounds; @override void initState() { @@ -123,8 +127,10 @@ class _MapCacheScreenState extends State { _minZoom = safeMin; _maxZoom = safeMax; _selectedBounds = bounds; + _visibleBounds = bounds; }); _updateEstimate(); + _refreshCachedTiles(); if (bounds != null) { _mapController.fitCamera( CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)), @@ -181,6 +187,7 @@ class _MapCacheScreenState extends State { Future _startDownload() async { final bounds = _selectedBounds; + final cacheService = context.read(); if (bounds == null) { showDismissibleSnackBar( context, @@ -197,6 +204,18 @@ class _MapCacheScreenState extends State { return; } + if (!cacheService.source.allowsBulkDownload) { + showDismissibleSnackBar( + context, + content: Text( + context.l10n.mapCache_bulkDownloadDisabledInConfig( + cacheService.source.label, + ), + ), + ); + return; + } + final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -219,8 +238,6 @@ class _MapCacheScreenState extends State { if (confirmed != true || !mounted) return; - final cacheService = context.read(); - setState(() { _isDownloading = true; _completedTiles = 0; @@ -254,6 +271,8 @@ class _MapCacheScreenState extends State { result.failed, ) : context.l10n.mapCache_cachedTiles(result.downloaded); + await _refreshCachedTiles(); + if (!mounted) return; showDismissibleSnackBar(context, content: Text(message)); } @@ -280,16 +299,61 @@ class _MapCacheScreenState extends State { final cacheService = context.read(); await cacheService.clearCache(); if (!mounted) return; + await _refreshCachedTiles(); + if (!mounted) return; showDismissibleSnackBar( context, content: Text(context.l10n.mapCache_offlineCacheCleared), ); } + Future _refreshCachedTiles() async { + if (!mounted) return; + setState(() { + _isLoadingCachedTiles = true; + }); + final cacheService = context.read(); + final inventory = await cacheService.getCachedTileInventory(); + if (!mounted) return; + setState(() { + _cachedTiles = inventory.tiles; + _cachedTileBytes = inventory.totalBytes; + _isLoadingCachedTiles = false; + }); + } + + String _formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) { + return '${(bytes / 1024).toStringAsFixed(1)} KB'; + } + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } + @override Widget build(BuildContext context) { - final tileCache = context.read(); + final tileCache = context.watch(); + final source = tileCache.source; final selectedBounds = _selectedBounds; + final activeCachedTiles = tileCache.filterTilesForActiveSource( + _cachedTiles, + ); + final cachedInSelection = tileCache.countTilesForBounds( + activeCachedTiles, + bounds: selectedBounds, + minZoom: _minZoom, + maxZoom: _maxZoom, + ); + final overlayPolygons = tileCache.buildCachedTilePolygons( + activeCachedTiles, + zoom: _overlayZoom.round().clamp(0, 19).toInt(), + visibleBounds: _visibleBounds, + ); + final cacheSummary = + '${context.l10n.mapCache_summarySource(source.label)}\n' + '${context.l10n.mapCache_summaryCachedTilesForSource(activeCachedTiles.length)}\n' + '${context.l10n.mapCache_summaryCachedInSelection(cachedInSelection)}\n' + '${context.l10n.mapCache_summaryApproxCacheSize(_formatBytes(_cachedTileBytes))}'; final l10n = context.l10n; final scheme = Theme.of(context).colorScheme; final isDesktop = _isDesktopPlatform(defaultTargetPlatform); @@ -327,9 +391,23 @@ class _MapCacheScreenState extends State { ) : const KeyboardOptions.disabled(), ), + onPositionChanged: (camera, hasGesture) { + final nextZoom = camera.zoom; + final nextBounds = camera.visibleBounds; + if (_overlayZoom != nextZoom || + _visibleBounds?.north != nextBounds.north || + _visibleBounds?.south != nextBounds.south || + _visibleBounds?.east != nextBounds.east || + _visibleBounds?.west != nextBounds.west) { + setState(() { + _overlayZoom = nextZoom; + _visibleBounds = nextBounds; + }); + } + }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), if (selectedBounds != null) PolygonLayer( polygons: [ @@ -341,9 +419,24 @@ class _MapCacheScreenState extends State { ), ], ), + if (overlayPolygons.isNotEmpty) + PolygonLayer(polygons: overlayPolygons), ], ), if (isDesktop) _buildDesktopMapControls(), + Positioned( + top: 12, + left: isDesktop ? 84 : 12, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + 'Z: ${_overlayZoom.round()}:', + style: const TextStyle(fontSize: 12), + ), + ), + ), + ), Positioned( top: 12, right: 12, @@ -445,6 +538,34 @@ class _MapCacheScreenState extends State { color: scheme.onSurfaceVariant, ), ), + const SizedBox(height: 12), + TextFormField( + key: ValueKey( + '$cacheSummary|${activeCachedTiles.length}|$cachedInSelection|$_cachedTileBytes', + ), + initialValue: cacheSummary, + readOnly: true, + minLines: 4, + maxLines: 4, + decoration: InputDecoration( + labelText: _isLoadingCachedTiles + ? l10n.mapCache_cachedTilesLabel + : l10n.mapCache_cachedTileSummaryLabel, + border: const OutlineInputBorder(), + ), + ), + if (!source.allowsBulkDownload) ...[ + const SizedBox(height: 8), + Text( + l10n.mapCache_bulkDownloadDisabledForSource( + source.label, + ), + style: MeshTheme.mono( + fontSize: 12, + color: MeshPalette.alert, + ), + ), + ], if (_isDownloading) ...[ const SizedBox(height: 8), LinearProgressIndicator( @@ -471,7 +592,10 @@ class _MapCacheScreenState extends State { child: ElevatedButton.icon( icon: const Icon(Icons.download), label: Text(l10n.mapCache_downloadTilesButton), - onPressed: _isDownloading || selectedBounds == null + onPressed: + _isDownloading || + selectedBounds == null || + !source.allowsBulkDownload ? null : _startDownload, ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 18e3ba9d..ec9e96db 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -25,7 +25,6 @@ import '../utils/battery_utils.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/sync_progress_overlay.dart'; -import '../widgets/themed_map_tile_layer.dart'; import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; @@ -107,6 +106,31 @@ class _MapScreenState extends State { super.dispose(); } + ColorScheme get _overlayScheme => Theme.of(context).colorScheme; + + bool get _useDarkOverlay => Theme.of(context).brightness == Brightness.dark; + + Color get _overlayPanelColor => _useDarkOverlay + ? MapPalette.panelDark + : _overlayScheme.surfaceContainerLow.withValues(alpha: 0.96); + + Color get _overlayPrimaryTextColor => + _useDarkOverlay ? MapPalette.textPrimary : _overlayScheme.onSurface; + + Color get _overlaySecondaryTextColor => _useDarkOverlay + ? MapPalette.textSecondary + : _overlayScheme.onSurfaceVariant; + + Color get _overlayMutedTextColor => + _useDarkOverlay ? MapPalette.textMuted : _overlayScheme.onSurfaceVariant; + + Color get _overlayBorderColor => + _useDarkOverlay ? MapPalette.border : _overlayScheme.outlineVariant; + + Color get _overlayShadowColor => _useDarkOverlay + ? MapPalette.markerShadow + : Colors.black.withValues(alpha: 0.18); + _NodeAge _ageOf(Contact contact) { final d = DateTime.now().difference(contact.lastSeen); if (d.inMinutes <= 60) return _NodeAge.online; @@ -233,12 +257,12 @@ class _MapScreenState extends State { bottom: 96, child: DecoratedBox( decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 8, offset: Offset(0, 3), ), @@ -250,20 +274,20 @@ class _MapScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.add), visualDensity: VisualDensity.standard, tooltip: context.l10n.map_zoomIn, onPressed: () => _zoomMapBy(1), ), IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.remove), tooltip: context.l10n.map_zoomOut, onPressed: () => _zoomMapBy(-1), ), IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.crop_free), tooltip: context.l10n.map_centerMap, onPressed: () => _mapController.move(center, zoom), @@ -328,6 +352,7 @@ class _MapScreenState extends State { final settingsService = context.read(); final pathHistory = context.read(); final tileCache = context.read(); + final scheme = Theme.of(context).colorScheme; final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final allContacts = connector.allContacts; @@ -579,8 +604,8 @@ class _MapScreenState extends State { canPop: allowBack, child: Scaffold( appBar: AppBar( - backgroundColor: MapPalette.panelDark, - foregroundColor: MapPalette.textPrimary, + backgroundColor: scheme.surface, + foregroundColor: scheme.onSurface, title: AppBarTitle(context.l10n.map_title), centerTitle: true, automaticallyImplyLeading: false, @@ -753,7 +778,7 @@ class _MapScreenState extends State { }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), if (_polylines.isNotEmpty && _isBuildingPathTrace) PolylineLayer(polylines: _polylines), if (sharedMarkerPolylines.isNotEmpty) @@ -906,7 +931,7 @@ class _MapScreenState extends State { _handleQuickSwitch(index, context), contactsUnreadCount: connector.getTotalContactsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - highContrast: true, + highContrast: _useDarkOverlay, ), ), floatingActionButton: @@ -1207,14 +1232,14 @@ class _MapScreenState extends State { height: 36, decoration: BoxDecoration( shape: BoxShape.circle, - color: MapPalette.panelDark, + color: _overlayPanelColor, border: Border.all( - color: guess.highConfidence ? color : MapPalette.textMuted, + color: guess.highConfidence ? color : _overlayMutedTextColor, width: guess.highConfidence ? 2.5 : 2, ), - boxShadow: const [ + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 7, offset: Offset(0, 2), ), @@ -1223,7 +1248,7 @@ class _MapScreenState extends State { alignment: Alignment.center, child: Icon( Icons.not_listed_location, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, size: 19, ), ), @@ -1541,12 +1566,12 @@ class _MapScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.xs), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 4, offset: Offset(0, 1), ), @@ -1560,7 +1585,7 @@ class _MapScreenState extends State { style: MeshTheme.mono( fontSize: 10, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), ), @@ -1681,7 +1706,7 @@ class _MapScreenState extends State { decoration: BoxDecoration( shape: BoxShape.circle, color: statusColor, - border: Border.all(color: MapPalette.panelDark, width: 2), + border: Border.all(color: _overlayPanelColor, width: 2), ), alignment: Alignment.center, child: batteryLow @@ -1704,10 +1729,10 @@ class _MapScreenState extends State { Expanded( child: Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -1765,9 +1790,9 @@ class _MapScreenState extends State { children: [ Expanded( child: Material( - color: MapPalette.panelDark, + color: _overlayPanelColor, shape: StadiumBorder( - side: const BorderSide(color: MapPalette.border), + side: BorderSide(color: _overlayBorderColor), ), clipBehavior: Clip.antiAlias, child: TextField( @@ -1775,17 +1800,15 @@ class _MapScreenState extends State { focusNode: _searchFocus, decoration: InputDecoration( hintText: context.l10n.map_searchHint, - hintStyle: const TextStyle( - color: MapPalette.textSecondary, - ), - prefixIcon: const Icon( + hintStyle: TextStyle(color: _overlaySecondaryTextColor), + prefixIcon: Icon( Icons.search, size: 20, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), suffixIcon: hasQuery ? IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.close, size: 18), onPressed: () { setState(() { @@ -1805,9 +1828,9 @@ class _MapScreenState extends State { vertical: 12, ), ), - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, fontWeight: FontWeight.w600, ), cursorColor: MapPalette.selected, @@ -1819,9 +1842,9 @@ class _MapScreenState extends State { ), const SizedBox(width: 8), Material( - color: MapPalette.panelDark, + color: _overlayPanelColor, shape: StadiumBorder( - side: const BorderSide(color: MapPalette.border), + side: BorderSide(color: _overlayBorderColor), ), clipBehavior: Clip.antiAlias, child: InkWell( @@ -1845,17 +1868,17 @@ class _MapScreenState extends State { style: MeshTheme.mono( fontSize: 13, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), const SizedBox(width: 2), AnimatedRotation( turns: _statsExpanded ? 0.5 : 0, duration: const Duration(milliseconds: 200), - child: const Icon( + child: Icon( Icons.expand_more, size: 16, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), ], @@ -1954,12 +1977,12 @@ class _MapScreenState extends State { color: selected ? Color.alphaBlend( accent.withValues(alpha: 0.34), - MapPalette.panelDark, + _overlayPanelColor, ) - : MapPalette.panelDark, + : _overlayPanelColor, shape: StadiumBorder( side: BorderSide( - color: selected ? accent : MapPalette.border, + color: selected ? accent : _overlayBorderColor, width: selected ? 1.5 : 1, ), ), @@ -1975,11 +1998,7 @@ class _MapScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ if (selected) ...[ - const Icon( - Icons.check, - size: 13, - color: MapPalette.textPrimary, - ), + Icon(Icons.check, size: 13, color: _overlayPrimaryTextColor), const SizedBox(width: 4), ], Text( @@ -1988,8 +2007,8 @@ class _MapScreenState extends State { fontSize: 12.5, fontWeight: FontWeight.w600, color: selected - ? MapPalette.textPrimary - : MapPalette.textSecondary, + ? _overlayPrimaryTextColor + : _overlaySecondaryTextColor, ), ), ], @@ -2019,14 +2038,14 @@ class _MapScreenState extends State { margin: const EdgeInsets.only(top: 6), constraints: const BoxConstraints(maxHeight: 300), decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 10, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -2035,8 +2054,8 @@ class _MapScreenState extends State { padding: const EdgeInsets.all(16), child: Text( context.l10n.map_noResults, - style: const TextStyle( - color: MapPalette.textSecondary, + style: TextStyle( + color: _overlaySecondaryTextColor, fontSize: 13, ), ), @@ -2046,7 +2065,7 @@ class _MapScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 4), itemCount: results.length, separatorBuilder: (_, _) => - const Divider(height: 1, color: MapPalette.border), + Divider(height: 1, color: _overlayBorderColor), itemBuilder: (context, index) { final c = results[index]; final color = _getNodeColor(c.type); @@ -2067,10 +2086,10 @@ class _MapScreenState extends State { children: [ Text( c.name, - style: const TextStyle( + style: TextStyle( fontSize: 13.5, fontWeight: FontWeight.w600, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -2078,7 +2097,7 @@ class _MapScreenState extends State { c.publicKeyHex.substring(0, 12), style: MeshTheme.mono( fontSize: 10.5, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), ), ], @@ -2088,13 +2107,13 @@ class _MapScreenState extends State { Icon( Icons.chevron_right, size: 18, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ) else Text( context.l10n.map_noGps.toUpperCase(), style: MeshTheme.accentLabel( - color: MapPalette.textMuted, + color: _overlayMutedTextColor, fontSize: 8.5, ), ), @@ -2154,14 +2173,14 @@ class _MapScreenState extends State { width: 230, padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 10, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -2178,7 +2197,7 @@ class _MapScreenState extends State { ), _statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline), _statRow(context.l10n.map_markers, pinCount, MapPalette.shared), - const Divider(height: 16, color: MapPalette.border), + Divider(height: 16, color: _overlayBorderColor), _buildLegendItem( Icons.person, context.l10n.map_chat, @@ -2208,7 +2227,7 @@ class _MapScreenState extends State { _buildLegendItem( Icons.not_listed_location, context.l10n.map_guessedLocation, - MapPalette.textMuted, + _overlayMutedTextColor, ), ], ), @@ -2229,7 +2248,10 @@ class _MapScreenState extends State { Expanded( child: Text( label, - style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary), + style: TextStyle( + fontSize: 12.5, + color: _overlaySecondaryTextColor, + ), overflow: TextOverflow.ellipsis, ), ), @@ -2238,7 +2260,7 @@ class _MapScreenState extends State { style: MeshTheme.mono( fontSize: 13, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), ), ], @@ -2271,8 +2293,8 @@ class _MapScreenState extends State { child: MeshCard( margin: EdgeInsets.zero, padding: const EdgeInsets.fromLTRB(14, 12, 8, 12), - color: MapPalette.panelDark, - borderColor: MapPalette.border, + color: _overlayPanelColor, + borderColor: _overlayBorderColor, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -2295,10 +2317,10 @@ class _MapScreenState extends State { Flexible( child: Text( contact.name, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -2326,9 +2348,9 @@ class _MapScreenState extends State { Flexible( child: Text( contact.typeLabel(context.l10n), - style: const TextStyle( + style: TextStyle( fontSize: 11.5, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), overflow: TextOverflow.ellipsis, ), @@ -2340,13 +2362,13 @@ class _MapScreenState extends State { ), if (pos != null) IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.center_focus_strong, size: 20), tooltip: context.l10n.map_centerOnNode, onPressed: () => _mapController.move(pos, max(_zoom, 15)), ), IconButton( - color: MapPalette.textPrimary, + color: _overlayPrimaryTextColor, icon: const Icon(Icons.close, size: 20), onPressed: _clearSelection, ), @@ -2407,14 +2429,17 @@ class _MapScreenState extends State { Text( label.toUpperCase(), style: MeshTheme.accentLabel( - color: MapPalette.textMuted, + color: _overlayMutedTextColor, fontSize: 8, ), ), const SizedBox(height: 1), Text( value, - style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary), + style: MeshTheme.mono( + fontSize: 11.5, + color: _overlayPrimaryTextColor, + ), ), ], ); @@ -3555,14 +3580,14 @@ class _MapScreenState extends State { right: 16, child: DecoratedBox( decoration: BoxDecoration( - color: MapPalette.panelDark, + color: _overlayPanelColor, borderRadius: BorderRadius.circular(MeshRadii.md), - border: Border.all(color: MapPalette.border), - boxShadow: const [ + border: Border.all(color: _overlayBorderColor), + boxShadow: [ BoxShadow( - color: MapPalette.markerShadow, + color: _overlayShadowColor, blurRadius: 10, - offset: Offset(0, 4), + offset: const Offset(0, 4), ), ], ), @@ -3575,18 +3600,27 @@ class _MapScreenState extends State { children: [ Text( l10n.contacts_pathTrace, - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _overlayPrimaryTextColor, + ), ), if (_pathTrace.isEmpty) const SizedBox(height: 8), if (_pathTrace.isEmpty) - Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), + Text( + l10n.map_tapToAdd, + style: TextStyle( + fontSize: 12, + color: _overlaySecondaryTextColor, + ), + ), const SizedBox(height: 6), if (_pathTrace.isNotEmpty) Text( "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", style: MeshTheme.mono( fontSize: 12, - color: MapPalette.textSecondary, + color: _overlaySecondaryTextColor, ), ), SelectableText( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 2eebdc71..463c53a0 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -19,7 +19,6 @@ import 'package:meshcore_open/services/path_history_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/path_map_ui.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; -import 'package:meshcore_open/widgets/themed_map_tile_layer.dart'; import 'package:provider/provider.dart'; import '../theme/mesh_theme.dart'; @@ -1399,7 +1398,7 @@ class _PathTraceMapScreenState extends State }, ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), AnimatedBuilder( animation: _playback, builder: (context, _) { diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index da1d5c6b..e695ba99 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -120,6 +120,25 @@ class AppSettingsService extends ChangeNotifier { ); } + Future setMapRasterSourceId(String value) async { + await updateSettings(_settings.copyWith(mapRasterSourceId: value)); + } + + Future setMapTileEndpointId(String value) async { + await updateSettings(_settings.copyWith(mapTileEndpointId: value)); + } + + Future setMapTileApiKey(String? value) async { + final normalized = value?.trim(); + await updateSettings( + _settings.copyWith( + mapTileApiKey: (normalized == null || normalized.isEmpty) + ? null + : normalized, + ), + ); + } + Future setNotificationsEnabled(bool value) async { await updateSettings(_settings.copyWith(notificationsEnabled: value)); } diff --git a/lib/services/map_tile_cache_service.dart b/lib/services/map_tile_cache_service.dart index e0d4e573..07584132 100644 --- a/lib/services/map_tile_cache_service.dart +++ b/lib/services/map_tile_cache_service.dart @@ -1,12 +1,232 @@ import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_map/flutter_map.dart'; -const String kMapTileUrlTemplate = - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; +import '../models/app_settings.dart'; +import 'app_settings_service.dart'; + +enum MapRasterSourcePreset { + osmAuto('osm_auto'), + osmStandard('osm_standard'), + osmDark('osm_dark'), + stamenTerrain('stamen_terrain'), + alidadeSmoothDark('alidade_smooth_dark'), + outdoors('outdoors'), + osmBright('osm_bright'), + outdoorsDark('outdoors_dark'), + osmBrightDark('osm_bright_dark'); + + const MapRasterSourcePreset(this.id); + + final String id; + + static MapRasterSourcePreset fromId(String id) { + for (final value in values) { + if (value.id == id) return value; + } + return MapRasterSourcePreset.osmAuto; + } +} + +enum MapRasterEndpointPreset { + standard('standard'), + standard2x('standard_2x'), + eu('eu'), + eu2x('eu_2x'); + + const MapRasterEndpointPreset(this.id); + + final String id; + + static MapRasterEndpointPreset fromId(String id) { + for (final value in values) { + if (value.id == id) return value; + } + return MapRasterEndpointPreset.standard; + } +} + +@immutable +class MapRasterSourceDefinition { + const MapRasterSourceDefinition({ + required this.id, + required this.label, + required this.description, + this.isStadia = false, + this.allowsBulkDownload = false, + }); + + final String id; + final String label; + final String description; + final bool isStadia; + final bool allowsBulkDownload; +} + +@immutable +class MapRasterEndpointDefinition { + const MapRasterEndpointDefinition({ + required this.id, + required this.label, + required this.description, + required this.host, + this.scaleSuffix = '', + }); + + final String id; + final String label; + final String description; + final String host; + final String scaleSuffix; +} + +class MapRasterSourceCatalog { + static const MapRasterSourceDefinition osmAuto = MapRasterSourceDefinition( + id: 'osm_auto', + label: 'OpenStreetMap Auto', + description: + 'Automatically uses OpenStreetMap Standard or Dark from the app theme', + ); + static const MapRasterSourceDefinition osmStandard = + MapRasterSourceDefinition( + id: 'osm_standard', + label: 'OpenStreetMap Standard', + description: 'Direct tiles from tile.openstreetmap.org', + ); + static const MapRasterSourceDefinition osmDark = MapRasterSourceDefinition( + id: 'osm_dark', + label: 'OpenStreetMap Dark', + description: 'Standard OpenStreetMap tiles with an inverted dark filter', + ); + static const MapRasterSourceDefinition stamenTerrain = + MapRasterSourceDefinition( + id: 'stamen_terrain', + label: 'Stamen Terrain', + description: 'Terrain-focused style with hill shading', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition alidadeSmoothDark = + MapRasterSourceDefinition( + id: 'alidade_smooth_dark', + label: 'Alidade Smooth Dark', + description: 'Dark basemap with smooth contrast', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition outdoors = MapRasterSourceDefinition( + id: 'outdoors', + label: 'Outdoors', + description: 'Outdoor-focused map with trails and terrain context', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition osmBright = MapRasterSourceDefinition( + id: 'osm_bright', + label: 'OSM Bright', + description: 'Bright general-purpose OpenStreetMap style', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition outdoorsDark = + MapRasterSourceDefinition( + id: 'outdoors', + label: 'Outdoors Dark', + description: 'Dark version of the Outdoors map style', + isStadia: true, + allowsBulkDownload: true, + ); + static const MapRasterSourceDefinition osmBrightDark = + MapRasterSourceDefinition( + id: 'osm_bright', + label: 'OSM Bright Dark', + description: 'Dark version of the OSM Bright map style', + isStadia: true, + allowsBulkDownload: true, + ); + + static MapRasterSourceDefinition fromPreset(MapRasterSourcePreset preset) { + switch (preset) { + case MapRasterSourcePreset.osmAuto: + return osmAuto; + case MapRasterSourcePreset.osmStandard: + return osmStandard; + case MapRasterSourcePreset.osmDark: + return osmDark; + case MapRasterSourcePreset.alidadeSmoothDark: + return alidadeSmoothDark; + case MapRasterSourcePreset.outdoors: + return outdoors; + case MapRasterSourcePreset.outdoorsDark: + return outdoorsDark; + case MapRasterSourcePreset.osmBright: + return osmBright; + case MapRasterSourcePreset.osmBrightDark: + return osmBrightDark; + case MapRasterSourcePreset.stamenTerrain: + return stamenTerrain; + } + } + + static MapRasterSourceDefinition fromSettings(AppSettings settings) { + return fromPreset(MapRasterSourcePreset.fromId(settings.mapRasterSourceId)); + } +} + +class MapRasterEndpointCatalog { + static const MapRasterEndpointDefinition standard = + MapRasterEndpointDefinition( + id: 'standard', + label: 'Standard Endpoint', + description: 'Global CDN routing to the fastest Stadia server', + host: 'tiles.stadiamaps.com', + ); + static const MapRasterEndpointDefinition standard2x = + MapRasterEndpointDefinition( + id: 'standard_2x', + label: 'Standard Endpoint (@2x)', + description: 'Global Stadia endpoint with HiDPI raster tiles', + host: 'tiles.stadiamaps.com', + scaleSuffix: '@2x', + ); + static const MapRasterEndpointDefinition eu = MapRasterEndpointDefinition( + id: 'eu', + label: 'EU Endpoint', + description: 'Route tile requests to Stadia EU servers', + host: 'tiles-eu.stadiamaps.com', + ); + static const MapRasterEndpointDefinition eu2x = MapRasterEndpointDefinition( + id: 'eu_2x', + label: 'EU Endpoint (@2x)', + description: 'EU Stadia endpoint with HiDPI raster tiles', + host: 'tiles-eu.stadiamaps.com', + scaleSuffix: '@2x', + ); + + static const List presets = [ + standard, + standard2x, + eu, + eu2x, + ]; + + static MapRasterEndpointDefinition fromSettings(AppSettings settings) { + final preset = MapRasterEndpointPreset.fromId(settings.mapTileEndpointId); + switch (preset) { + case MapRasterEndpointPreset.standard2x: + return standard2x; + case MapRasterEndpointPreset.eu: + return eu; + case MapRasterEndpointPreset.eu2x: + return eu2x; + case MapRasterEndpointPreset.standard: + return standard; + } + } +} class MapTileCacheProgress { final int completed; @@ -32,36 +252,253 @@ class MapTileCacheResult { }); } -class MapTileCacheService { +class CachedTileInfo { + final String key; + final String host; + final String sourceId; + final int zoom; + final int x; + final int y; + final int length; + + const CachedTileInfo({ + required this.key, + required this.host, + required this.sourceId, + required this.zoom, + required this.x, + required this.y, + required this.length, + }); +} + +class CachedTileInventory { + final List tiles; + final int totalBytes; + + const CachedTileInventory({required this.tiles, required this.totalBytes}); +} + +class MapTileCacheService extends ChangeNotifier { static const String cacheKey = 'map_tile_cache'; static const String userAgentPackageName = 'com.meshcore.open'; static const int defaultMinZoom = 10; static const int defaultMaxZoom = 15; + final AppSettingsService appSettingsService; final BaseCacheManager cacheManager; late final TileProvider tileProvider; - MapTileCacheService({BaseCacheManager? cacheManager}) - : cacheManager = - cacheManager ?? - CacheManager( - Config( - cacheKey, - stalePeriod: const Duration(days: 365), - maxNrOfCacheObjects: 200000, - ), - ) { + MapTileCacheService({ + required this.appSettingsService, + BaseCacheManager? cacheManager, + }) : cacheManager = + cacheManager ?? + CacheManager( + Config( + cacheKey, + stalePeriod: const Duration(days: 365), + maxNrOfCacheObjects: 200000, + ), + ) { tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager); + appSettingsService.addListener(_handleSettingsChanged); } + MapRasterSourceDefinition get source { + final selectedSource = MapRasterSourceCatalog.fromSettings( + appSettingsService.settings, + ); + if (!selectedSource.allowsBulkDownload || + !appSettingsService.settings.usesstadiaDemo) { + return selectedSource; + } + return MapRasterSourceDefinition( + id: selectedSource.id, + label: selectedSource.label, + description: selectedSource.description, + isStadia: selectedSource.isStadia, + allowsBulkDownload: false, // Explicitly disable bulk download for demo. + ); + } + + MapRasterEndpointDefinition get endpoint => + MapRasterEndpointCatalog.fromSettings(appSettingsService.settings); + + String get urlTemplate => _buildUrlTemplate(appSettingsService.settings); + + TileBuilder? get tileBuilder => null; + + static bool shouldApplyDarkFilterForSettings( + AppSettings settings, + Brightness brightness, + ) { + switch (MapRasterSourcePreset.fromId(settings.mapRasterSourceId)) { + case MapRasterSourcePreset.osmDark: + case MapRasterSourcePreset.outdoorsDark: + case MapRasterSourcePreset.osmBrightDark: + return true; + case MapRasterSourcePreset.osmAuto: + return brightness == Brightness.dark; + case MapRasterSourcePreset.osmStandard: + case MapRasterSourcePreset.stamenTerrain: + case MapRasterSourcePreset.alidadeSmoothDark: + case MapRasterSourcePreset.outdoors: + case MapRasterSourcePreset.osmBright: + return false; + } + } + + static const ColorFilter _darkMapFilter = ColorFilter.matrix([ + -0.0850, + -0.2861, + -0.0289, + 0, + 120, + -0.0957, + -0.3218, + -0.0325, + 0, + 140, + -0.1169, + -0.3934, + -0.0397, + 0, + 170, + 0, + 0, + 0, + 1, + 0, + ]); + + CacheManager get _concreteCacheManager => cacheManager as CacheManager; + Map get defaultHeaders => { 'User-Agent': 'flutter_map ($userAgentPackageName)', }; + Widget buildTileLayer(BuildContext context, {double opacity = 1}) { + Widget layer = TileLayer( + urlTemplate: urlTemplate, + tileProvider: tileProvider, + tileBuilder: tileBuilder, + userAgentPackageName: userAgentPackageName, + maxZoom: 19, + ); + + final shouldApplyDarkFilter = shouldApplyDarkFilterForSettings( + appSettingsService.settings, + Theme.of(context).brightness, + ); + + if (shouldApplyDarkFilter) { + layer = ColorFiltered(colorFilter: _darkMapFilter, child: layer); + } + + if (opacity < 1) { + layer = Opacity(opacity: opacity, child: layer); + } + + return layer; + } + Future clearCache() async { await cacheManager.emptyCache(); } + Future getCachedTileInventory() async { + final repo = _concreteCacheManager.config.repo; + await repo.open(); + final objects = await repo.getAllObjects(); + final tiles = []; + int totalBytes = 0; + + for (final object in objects) { + totalBytes += object.length ?? 0; + final tile = _parseCachedTile(object); + if (tile != null) { + tiles.add(tile); + } + } + + return CachedTileInventory(tiles: tiles, totalBytes: totalBytes); + } + + List filterTilesForActiveSource( + Iterable tiles, + ) { + final activeSource = source; + if (!activeSource.isStadia) { + return tiles + .where( + (tile) => + tile.sourceId == MapRasterSourceCatalog.osmStandard.id && + tile.host == 'tile.openstreetmap.org', + ) + .toList(); + } + + final activeEndpoint = endpoint; + return tiles + .where( + (tile) => + tile.sourceId == activeSource.id && + tile.host == activeEndpoint.host, + ) + .toList(); + } + + int countTilesForBounds( + Iterable tiles, { + LatLngBounds? bounds, + required int minZoom, + required int maxZoom, + }) { + if (bounds == null) return 0; + final safeMin = math.min(minZoom, maxZoom); + final safeMax = math.max(minZoom, maxZoom); + return tiles.where((tile) { + if (tile.zoom < safeMin || tile.zoom > safeMax) { + return false; + } + final tileBounds = _tileBoundsForTile(tile.x, tile.y, tile.zoom); + return _boundsIntersect(bounds, tileBounds); + }).length; + } + + List buildCachedTilePolygons( + Iterable tiles, { + required int zoom, + LatLngBounds? visibleBounds, + int limit = 250, + }) { + final polygons = []; + for (final tile in tiles) { + if (tile.zoom != zoom) continue; + final tileBounds = _tileBoundsForTile(tile.x, tile.y, tile.zoom); + if (visibleBounds != null && + !_boundsIntersect(visibleBounds, tileBounds)) { + continue; + } + polygons.add( + Polygon( + points: [ + tileBounds.northWest, + tileBounds.northEast, + tileBounds.southEast, + tileBounds.southWest, + ], + borderStrokeWidth: 0.6, + color: const Color(0x5532A852), + borderColor: const Color(0xCC2F8F46), + ), + ); + if (polygons.length >= limit) break; + } + return polygons; + } + int estimateTileCount(LatLngBounds bounds, int minZoom, int maxZoom) { final safeMin = math.min(minZoom, maxZoom); final safeMax = math.max(minZoom, maxZoom); @@ -89,6 +526,7 @@ class MapTileCacheService { final total = estimateTileCount(bounds, safeMin, safeMax); final authHeaders = headers ?? defaultHeaders; final safeConcurrency = math.max(1, concurrentDownloads); + final currentTemplate = urlTemplate; int completed = 0; int failed = 0; @@ -124,7 +562,7 @@ class MapTileCacheService { final tileBounds = _tileBoundsForBounds(bounds, zoom); for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) { for (int y = tileBounds.minY; y <= tileBounds.maxY; y++) { - final url = _buildTileUrl(x, y, zoom); + final url = _buildTileUrl(x, y, zoom, urlTemplate: currentTemplate); await queueDownload(url); } } @@ -167,6 +605,16 @@ class MapTileCacheService { ); } + void _handleSettingsChanged() { + notifyListeners(); + } + + @override + void dispose() { + appSettingsService.removeListener(_handleSettingsChanged); + super.dispose(); + } + _TileBounds _tileBoundsForBounds(LatLngBounds bounds, int zoom) { final north = _clampLatitude(bounds.north); final south = _clampLatitude(bounds.south); @@ -185,6 +633,56 @@ class MapTileCacheService { ); } + String _buildUrlTemplate(AppSettings settings) { + final source = MapRasterSourceCatalog.fromSettings(settings); + if (!source.isStadia) { + return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + } + final endpoint = MapRasterEndpointCatalog.fromSettings(settings); + final apiKey = settings.effectiveMapTileApiKey; + final base = + 'https://${endpoint.host}/tiles/${source.id}/{z}/{x}/{y}${endpoint.scaleSuffix}.png'; + final query = Uri(queryParameters: {'api_key': apiKey}).query; + return '$base?$query'; + } + + CachedTileInfo? _parseCachedTile(CacheObject object) { + final uri = Uri.tryParse(object.key); + if (uri == null) return null; + final segments = uri.pathSegments; + + if (segments.length >= 3 && + segments[segments.length - 3].isNotEmpty && + segments[segments.length - 2].isNotEmpty) { + final zoom = int.tryParse(segments[segments.length - 3]); + final x = int.tryParse(segments[segments.length - 2]); + final ySegment = segments.last; + final yString = ySegment.split('.').first.replaceAll('@2x', ''); + final y = int.tryParse(yString); + + if (zoom == null || x == null || y == null) { + return null; + } + + var sourceId = MapRasterSourceCatalog.osmStandard.id; + if (segments.length >= 5 && segments[0] == 'tiles') { + sourceId = segments[1]; + } + + return CachedTileInfo( + key: object.key, + host: uri.host, + sourceId: sourceId, + zoom: zoom, + x: x, + y: y, + length: object.length ?? 0, + ); + } + + return null; + } + int _lonToTileX(double lon, int zoom, int maxIndex) { final n = 1 << zoom; final value = ((lon + 180.0) / 360.0 * n).floor(); @@ -205,8 +703,34 @@ class MapTileCacheService { return lat.clamp(-maxLat, maxLat); } - String _buildTileUrl(int x, int y, int zoom) { - return kMapTileUrlTemplate + LatLngBounds _tileBoundsForTile(int x, int y, int zoom) { + return LatLngBounds.unsafe( + north: _tileYToLat(y, zoom), + south: _tileYToLat(y + 1, zoom), + east: _tileXToLon(x + 1, zoom), + west: _tileXToLon(x, zoom), + ); + } + + double _tileXToLon(int x, int zoom) { + final n = 1 << zoom; + return x / n * 360.0 - 180.0; + } + + double _tileYToLat(int y, int zoom) { + final n = math.pi - (2.0 * math.pi * y) / (1 << zoom); + return 180.0 / math.pi * math.atan(0.5 * (math.exp(n) - math.exp(-n))); + } + + bool _boundsIntersect(LatLngBounds a, LatLngBounds b) { + return a.west <= b.east && + a.east >= b.west && + a.south <= b.north && + a.north >= b.south; + } + + String _buildTileUrl(int x, int y, int zoom, {required String urlTemplate}) { + return urlTemplate .replaceAll('{z}', zoom.toString()) .replaceAll('{x}', x.toString()) .replaceAll('{y}', y.toString()); diff --git a/lib/theme/mesh_theme.dart b/lib/theme/mesh_theme.dart index fcb15b1b..883ea2a2 100644 --- a/lib/theme/mesh_theme.dart +++ b/lib/theme/mesh_theme.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart' show CupertinoPageTransitionsBuilder; import 'package:flutter/material.dart'; /// MeshCore palette — high-contrast slate surfaces with sky-blue accents. @@ -448,7 +447,7 @@ class MeshTheme { pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: FadeForwardsPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: FadeForwardsPageTransitionsBuilder(), TargetPlatform.linux: FadeForwardsPageTransitionsBuilder(), TargetPlatform.macOS: FadeForwardsPageTransitionsBuilder(), TargetPlatform.windows: FadeForwardsPageTransitionsBuilder(), diff --git a/lib/widgets/telemetry_location_map.dart b/lib/widgets/telemetry_location_map.dart index f8983c04..537867c5 100644 --- a/lib/widgets/telemetry_location_map.dart +++ b/lib/widgets/telemetry_location_map.dart @@ -11,7 +11,6 @@ import '../models/app_settings.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; -import 'themed_map_tile_layer.dart'; class TelemetryLocationMap extends StatefulWidget { final double latitude; @@ -115,7 +114,7 @@ class _TelemetryLocationMapState extends State { ), ), children: [ - ThemedMapTileLayer(tileCache: tileCache), + tileCache.buildTileLayer(context), MarkerLayer( markers: [ ...contacts.map(_buildContactMarker), diff --git a/lib/widgets/themed_map_tile_layer.dart b/lib/widgets/themed_map_tile_layer.dart deleted file mode 100644 index c6d2eaad..00000000 --- a/lib/widgets/themed_map_tile_layer.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; - -import '../services/map_tile_cache_service.dart'; - -/// Shared cached map tiles with an automatic dark-mode treatment. -/// -/// The dark style transforms the existing OpenStreetMap raster tiles, so light -/// and dark maps share the same offline cache and network requests. -class ThemedMapTileLayer extends StatelessWidget { - final MapTileCacheService tileCache; - final double opacity; - - const ThemedMapTileLayer({ - super.key, - required this.tileCache, - this.opacity = 1, - }); - - static const ColorFilter _darkMapFilter = ColorFilter.matrix([ - -0.0850, - -0.2861, - -0.0289, - 0, - 120, - -0.0957, - -0.3218, - -0.0325, - 0, - 140, - -0.1169, - -0.3934, - -0.0397, - 0, - 170, - 0, - 0, - 0, - 1, - 0, - ]); - - @override - Widget build(BuildContext context) { - Widget layer = TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ); - - if (Theme.of(context).brightness == Brightness.dark) { - layer = ColorFiltered(colorFilter: _darkMapFilter, child: layer); - } - if (opacity < 1) { - layer = Opacity(opacity: opacity, child: layer); - } - return layer; - } -} diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36fa..93e46829 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f4..533a1712 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES)