diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 91111a127..b7ea19246 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -101,7 +101,7 @@ jobs: EOF - name: Build Release APK - run: flutter build apk --release --target-platform android-arm64,android-x64 + run: flutter build apk --release - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index f51f42ace..5f5b3781f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,8 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/build/ +/android/app/build/ # Signing android/key.properties diff --git a/.metadata b/.metadata index 2059ed534..b15f453e2 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "e1fd963c6f6922bd32afde2e9698a363cd0406d2" + revision: "4cf24164269a5ebf0c16a028a00727d0e77bbb05" channel: "stable" project_type: app @@ -13,14 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2 - base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2 - - platform: android - create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2 - base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2 - - platform: ios - create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2 - base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2 + create_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05 + base_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05 + - platform: macos + create_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05 + base_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05 # User provided section diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a4cdd0c1d..dcf1f81bf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -9,30 +9,40 @@ concerns live in exactly one place. ``` lib/ ├── main.dart # entry point → bootstrap() -├── bootstrap.dart # init platform services (Firebase, DI) then runApp +├── bootstrap.dart # init platform services (Firebase, prefs, SNTP, DI) then runApp ├── app/ # app shell: wiring, not features │ ├── app.dart # root MaterialApp.router + providers │ ├── router/ # go_router route table (central AppRoutes registry) │ ├── shell/ # bottom-nav shell (StatefulShellRoute) -│ └── theme/ # Material 3 theming + design tokens (spacing/radius/…) +│ └── theme/ # Material 3 tokens (spacing/radius/motion/colour/glass) ├── core/ # cross-cutting, feature-agnostic building blocks +│ ├── di/ # SharedDeps assembly + coreProviders (provider wiring) │ ├── error/ # Result + Failure hierarchy -│ ├── geo/ # geometry helpers (point-in-polygon) +│ ├── geo/ # location services, township directory/boundaries │ ├── logging/ # the Log facade +│ ├── meshtastic/ # LoRa mesh: BLE transport, link keeper, DPIP data plane │ ├── models/ # shared value types (LatLng, …) -│ ├── network/ # Dio ApiClient, region selection, error→Failure +│ ├── network/ # ApiClient + ApiTier + ApiPaths, region failover, +│ │ # SSE, ETag cache, meteor delta decode │ ├── notifications/ # FCM/APNs + awesome channels, tap routing seam -│ ├── platform/ # native-first device_info / compass -│ ├── realtime/ # polling spine (channel/state/clock/staleness) -│ └── settings/ # app-wide persisted/ephemeral state (provider) +│ ├── platform/ # native-first device_info / compass / battery / tier +│ ├── realtime/ # polling spine (channel/state/clock/staleness/SSE) +│ ├── settings/ # typed Prefs facade + persisted/ephemeral controllers +│ ├── storage/ # SQLite stores (ETag cache, network usage) +│ └── weather/ # weather-condition / icon mapping ├── features/ # one folder per feature, each self-contained +│ │ # changelog · disaster_map · earthquake · events · +│ │ # home · location · log · map · meshtastic · more · +│ │ # notification · onboarding · settings · sponsor · +│ │ # typhoon · weather │ └── / │ ├── data/ # datasources, repository impls, JSON→model mapping │ ├── domain/ # @freezed entities, repository interfaces (pure Dart) │ └── presentation/ # pages/, widgets/, controllers (state) └── shared/ # reused across ≥2 features - ├── map/ # the reusable MapLibre surface (layers, timeline) + ├── map/ # the reusable MapLibre surface (BaseMap, layers, timeline) ├── navigation/ # AppRoutes (route names/paths) + ├── seismic/ # intensity/report colour scales └── widgets/ # AsyncView/RealtimeView, region bar, … ``` @@ -70,6 +80,7 @@ lib/ ## Native config Platform integration (Firebase, APNs critical-alerts, signing, permissions) is -restored per-need in `android/` and `ios/`. Firebase reads native -`google-services.json` / `GoogleService-Info.plist`; `Firebase.initializeApp()` -in `bootstrap.dart` needs no generated options file. +restored per-need in `android/` and `ios/`. Firebase initializes from the +generated `lib/firebase_options.dart` (`DefaultFirebaseOptions`) — not the +native plists — so init never depends on a bundle resource; Android also keeps +`google-services.json` for the GMS plugin. diff --git a/CLAUDE.md b/CLAUDE.md index fd3d8b8fd..76104c97c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,8 +4,8 @@ Guidance for working in this repository. See `ARCHITECTURE.md` for the folder structure, `api.md` for the API/region map, and `DESIGN.md` for the design system (colours, spacing, radius, motion, typography, shared components). -DPIP is a Taiwan disaster-prevention app, mid-rewrite on the `rewrite` branch -(clean Flutter 3.44 baseline, feature-first architecture). +DPIP is a Taiwan disaster-prevention app, mid-rewrite (clean Flutter 3.47 +baseline, feature-first architecture). ## Toolchain @@ -58,14 +58,20 @@ DPIP is a Taiwan disaster-prevention app, mid-rewrite on the `rewrite` branch (`AppSpacing` / `AppRadius` / `AppMotion`), `ColorScheme` roles, and shared components (`shared/widgets/`). Never hardcode spacing, radius, duration, or colour where a token/role exists. Full reference: `DESIGN.md`. -- **State management:** `provider`. App-wide services are provided in - `app/app.dart`; feature state lives in the feature's `presentation`. -- **Networking:** never call hosts directly — use the region-aware API surface - (`api/redundant_api.dart`, `api/exclusive_api.dart`, `api/external_api.dart`). - No DNS-balanced bare hosts. See `api.md`. `ApiClient` fails over to the next - region **only** on transient/server faults (connection drop, timeout, 5xx) and - logs each failover; a 4xx or a cancellation throws immediately (it would recur - on every region). Pass a `CancelToken` to abort a superseded request. Fatal and +- **State management:** `provider`. `bootstrap()` assembles the shared + infrastructure into a `SharedDeps` (`core/di/shared_deps.dart`) and hands it + to each feature's `*Providers(deps)` aggregate (wired through + `core/di/core_providers.dart`); feature state lives in the feature's + `presentation`. +- **Networking:** never call hosts directly — use the region-aware `ApiClient` + (`core/network/api_client.dart`) with an `ApiTier` from + `core/network/api_region.dart` (LB vs Core, exclusive vs multi-active) and + path constants from `core/network/api_paths.dart`. No DNS-balanced bare + hosts. See `api.md`. `ApiClient` fails over to the next region **only** on + transient/server faults (connection drop, timeout, 5xx) and logs each + failover; a 4xx or a cancellation throws immediately (it would recur on every + region). Pass a `CancelToken` to abort a superseded request. SSE streams go + through `ApiClient.openStream` + `core/network/sse_client.dart`. Fatal and handled errors forward to an optional `CrashSink` set on `Log` (Crashlytics wire-up point). - **Data & errors (contract):** models are `@freezed` value types with generated @@ -127,9 +133,29 @@ DPIP is a Taiwan disaster-prevention app, mid-rewrite on the `rewrite` branch key; `app/` maps it to an `AppRoutes` tab). **External, not code:** upload an APNs auth key to the Firebase console for iOS; push only works on a physical device; permission is requested after the first frame for now (move to - onboarding); backend token registration (`/v2/location`) needs the not-yet- - ported location feature — the token is stored (`NotificationService.token`) - meanwhile. + onboarding). The push token registers through the location feature + (`/v2/location`, `DeviceLocationReporter` in `core/geo/`) on meaningful moves. +- **LoRa mesh (`core/meshtastic/`):** the off-grid path. Three layers, each with + one job. `MeshtasticService` (domain) is the **transport** — connect/scan, + packet streams, `sendData`, and the radio's channel/region config; its BLE + impl lives in `data/` over the vendored `third_party/meshtastic_flutter` + (locally forked — see its CHANGELOG). `MeshLink` (created in `bootstrap`, not + by a page) owns the **session**: the chosen radio is persisted and *is* the + intent to stay connected, so it survives page changes, drops and app + restarts; only `detach()` stops it. It also provisions the radio after every + connect. `DpipMeshGateway` is the **data plane**: DPIP disaster payloads ride + `PRIVATE_APP` (256) inside a 5-byte versioned envelope (`dpip_mesh.dart`) on + the fixed `DPIP` channel (PSK `AQ==`, region `TW`) — never on + `TEXT_MESSAGE_APP`, which belongs to the user's chat. A feed broadcasts by + handing over a `DpipMeshPacket` and receives by listening to `inbound`; it + never sees Bluetooth. Wire codes and the envelope layout are pinned by tests + — changing one is a protocol break, so bump the version instead. Mesh + delivery is **best-effort** (lossy, duty-cycle limited, unacknowledged): a + safety-critical feed may add it as a path, never rely on it as the only one. + Radio writes go through local admin messages (`from == 0` exempts them from + the remote-admin session key); a channel write is read back before it counts, + and a region change is confirmed by the user because the firmware reboots the + radio and takes every other channel with it. - **Native-first:** prefer platform channels / built-ins over third-party plugins where practical (e.g. `core/platform/` device_info, compass). - **Icons:** use Flutter's built-in Material `Icons` only — no third-party icon @@ -140,18 +166,20 @@ DPIP is a Taiwan disaster-prevention app, mid-rewrite on the `rewrite` branch - **Localization (i18n):** every user-facing string goes through `AppLocalizations` (`AppLocalizations.of(context).`) — never hardcode display text. ARB sources live in `lib/l10n/` (`app_en.arb` is the template, - `app_zh.arb` is Traditional Chinese, the Taiwan default; `zh_Hant_HK` / - `zh_Hans` cover HK/Simplified); generated code is in `lib/l10n/gen/`. Each ARB - **self-describes** with a `languageName` key (the locale's own name), and the - language picker (`shared/widgets/language_picker.dart`) is built from + `app_zh.arb` is Traditional Chinese, the Taiwan default; `zh_TW`, + `zh_Hant_HK` / `zh_Hans` cover HK/Simplified, plus ja/ko/th/vi/fil/id); + generated code is in `lib/l10n/gen/`. Each ARB **self-describes** with a + `languageName` key (the locale's own name), and the language picker + (`shared/widgets/language_picker.dart`) is built from `AppLocalizations.supportedLocales` + that key — never a hardcoded list. So a language is added by just dropping in `app_.arb` (with `languageName`); - the home/fallback locale is the one constant in `core/settings/locale_config.dart`. - Enforced by `tool/check_l10n.sh` (a CI gate, no packages): ARB key-parity with - the template + no hardcoded CJK/kana/Hangul/Thai string literals in - `features/*/presentation/**` or `shared/widgets/**`. A genuinely non-display or - throwaway literal is exempted with `// l10n-ignore: ` (that line/the one - above) or `l10n-ignore-file` in a file's header doc. Config in `l10n.yaml`. + the home/fallback locale is the one constant in + `core/settings/locale_config.dart`. Enforced by `tool/check_l10n.sh` (a CI + gate, no packages): ARB key-parity with the template + no hardcoded + CJK/kana/Hangul/Thai string literals in `features/*/presentation/**` or + `shared/widgets/**`. A genuinely non-display or throwaway literal is exempted + with `// l10n-ignore: ` (that line/the one above) or + `l10n-ignore-file` in a file's header doc. Config in `l10n.yaml`. - **Persistence keys (contract):** all `SharedPreferences` access goes through the typed `Prefs` facade (`core/settings/prefs.dart`), keyed by a `PrefKey` from the `PreferenceKeys` registry (`core/settings/preference_keys.dart`) — @@ -170,12 +198,14 @@ DPIP is a Taiwan disaster-prevention app, mid-rewrite on the `rewrite` branch `.github/workflows/ci.yml` runs on every push/PR and must stay green. It uses the mise-pinned toolchain and runs, in order: the layering gate (`tool/check_layering.sh`), the localization gate (`tool/check_l10n.sh`), the -prefs gate (`tool/check_prefs.sh`), `dart format --set-exit-if-changed`, a -codegen-drift check (`build_runner` + `git diff --exit-code` — committed -`*.g.dart` / `*.freezed.dart` must match a fresh build), `flutter analyze`, and -`flutter test`. The three bash gates need only bash + python3 (no toolchain), so -they fail fast. Run these locally before pushing. Safety-critical seismic math -is pinned by golden tests (`test/features/earthquake/eew_estimator_test.dart`); +prefs gate (`tool/check_prefs.sh`), the lockfile gate (`tool/check_pubspec_lock.sh`), +`dart format --set-exit-if-changed`, a codegen-drift check (`build_runner` + +`git diff --exit-code` — committed `*.g.dart` / `*.freezed.dart` must match a +fresh build), `flutter analyze`, and `flutter test`. The four bash gates need +only bash + python3 (no toolchain), so they fail fast. `android.yml` / +`ios.yml` build release artifacts, and `review.yml` adds an automated PR +review. Run these locally before pushing. Safety-critical seismic math is +pinned by golden tests (`test/features/earthquake/eew_estimator_test.dart`); if you change the EEW estimator, update those goldens deliberately. ## Commits diff --git a/DESIGN.md b/DESIGN.md index b40c7b9ef..104cfaf0e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -36,7 +36,7 @@ logging and localization — is covered at the end. outline `#a9b4bc` / town `#6A6B72`. Light: bg `#E0E0E0` / fill `#ADADAD` / outline `#6B6B6B` / town `#9A9A9A`. - Literal colours are also allowed where no theme role applies: shader - fallback/mood colours (`weather_sky.frag` / `weather_sky_background.dart`) and + fallback/mood colours (`weather_sky_background.dart`, `_fallbackColour`) and the one sheet shadow. ## Spacing — `AppSpacing` @@ -85,25 +85,36 @@ leading icon. Prefer Material `elevation` / tonal surfaces. The home sheet's custom top-edge shadow is the one bespoke shadow; add more only with a clear reason. +## Glass over the weather backdrop + +Content layered over the weather sky tints through `lib/app/theme/app_glass.dart` +(`glassSurface` / `glassOnSurface` / `inkOverWeather` …), driven by a `reveal` +dial: at rest a translucent theme surface, once revealed a pane of the sky +itself (sky colour at 20 % alpha, HSL-lightness shifted by time of day — see +`skyCardTint`). Ink follows the sky, not the theme. Shared surfaces built from +it: `shared/widgets/frosted_surface.dart`, `sheet_surface.dart`. + ## Shared components — `lib/shared/widgets/` - `SectionHeader(title)` — the small primary-tinted header above a settings/menu group. Use it for every section instead of re-styling a `Text`. -Map foundations live in `lib/shared/map/` (`BaseMap`, `map_style`, -`map_snapshot`); see `api.md` for the tile/radar endpoints. +Map foundations live in `lib/shared/map/` (`BaseMap` in `base_map.dart`, +`map_style.dart` with `MapColors`/`MapPalette`, `map_tile_cache.dart`); see +`api.md` for the tile/radar endpoints. ## Logging (infrastructure) Always log through `Log` (`lib/core/logging/log.dart`): `Log.debug / info / warning / error / handle`. **Never** `print` / `debugPrint` -(`avoid_print` fails analysis). In-app viewer: More → 實驗性功能 is above it; the -log page is backed by the same `Log` history. +(`avoid_print` fails analysis). In-app viewer: the **App 日誌** page under the +More tab (`LogPage`), backed by the same `Log` history. ## Localization (infrastructure) Every user-facing string goes through `AppLocalizations` (`AppLocalizations.of(context).`). ARB sources in `lib/l10n/` -(`app_en.arb` template, `app_zh.arb` Traditional Chinese default); generated code -in `lib/l10n/gen/`. Add a language by dropping in `app_.arb`. Never -hardcode display text. +(`app_en.arb` template, `app_zh.arb` Traditional Chinese default, plus +`zh_TW` / `zh_Hant_HK` / `zh_Hans` and ja/ko/th/vi/fil/id); generated code +in `lib/l10n/gen/`. Add a language by dropping in `app_.arb` (with a +`languageName` key) — never hardcode display text. diff --git a/README.md b/README.md index c596eb5a0..8bbe2d968 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ -[![splash](/.github/assets/splash.png)](#下載) -
-status -Release -Android Build Status -iOS Build Status +status +Release +Android Build Status +iOS Build Status Crowdin Localization Greater Good -GitHub License +GitHub License website TREM Discord
@@ -63,7 +61,7 @@ TREM-Net 是一個自 2022 年 6 月起開始在全臺各地部署的觀測網 你可以在 [Play Store](https://play.google.com/store/apps/details?id=com.exptech.dpip) 和 [App Store](https://apps.apple.com/tw/app/dpip-%E7%81%BD%E5%AE%B3%E5%A4%A9%E6%B0%A3%E8%88%87%E5%9C%B0%E9%9C%87%E9%80%9F%E5%A0%B1/id6468026362) 上取得 DPIP。 -你也可以從我們的 [Release 頁面](https://github.com/ExpTechTW/DPIP-Pocket/releases/latest)上取得 DPIP 的安裝包進行手動安裝。 +你也可以從我們的 [Release 頁面](https://github.com/ExpTechTW/DPIP/releases/latest)上取得 DPIP 的安裝包進行手動安裝。 ## 翻譯 @@ -71,7 +69,7 @@ DPIP 支援多語言,我們正在 Crowdin 平台上進行翻譯。如果你願 你可以[點擊這裡前往我們的 Crowdin 專案頁面](https://crowdin.com/project/dpip),選擇你熟悉的語言並開始翻譯。每一份貢獻都將幫助我們將防災資訊傳遞給更多的人! -如果你沒有看到你熟悉的語言,歡迎在我們的 [Issue](https://github.com/ExpTechTW/DPIP-Pocket/issues) 中提出新的語言請求,我們會盡快為你開啟。 +如果你沒有看到你熟悉的語言,歡迎在我們的 [Issue](https://github.com/ExpTechTW/DPIP/issues) 中提出新的語言請求,我們會盡快為你開啟。 ## 從原始碼建置 @@ -79,19 +77,11 @@ DPIP 支援多語言,我們正在 Crowdin 平台上進行翻譯。如果你願 在開始建置之前,請確保你的開發環境已安裝並配置以下軟體: -- **Flutter SDK**: [安裝指引](https://docs.flutter.dev/get-started/install) -- **Dart SDK**: 已包含在 Flutter SDK 中 +- **Flutter SDK**: 由 [mise](https://mise.jdx.dev/) 管理(`mise.toml` 固定版本,請先 `mise install`) - [**Android Studio**](https://developer.android.com/studio?hl=ja) 或 [**Xcode**](https://developer.apple.com/jp/xcode/)(iOS 開發用) - 也可以使用 [VSCode](https://code.visualstudio.com/) 或其他你喜歡的 IDE - _\*可選\*_ [**Git**](https://git-scm.com/): 用於複製存儲庫 -```console -Flutter 3.35.1 • channel stable • https://github.com/flutter/flutter.git -Framework • revision 20f8274939 • 2025-08-14 10:53:09 -0700 -Engine • hash 6cd51c08a88e7bbe848a762c20ad3ecb8b063c0e • 2025-08-13 23:35:25.000Z -Tools • Dart 3.9.0 • DevTools 2.48.0 -``` - ### 建置步驟 1. 取得原始碼 @@ -100,14 +90,12 @@ Tools • Dart 3.9.0 • DevTools 2.48.0 你可以直接在 Github 上下載存儲庫壓縮檔 - ![Download Source ZIP](/.github/assets/download_source.png) - - **使用 Git** 使用以下指令複製專案: ```bash - git clone https://github.com/ExpTechTW/DPIP-Pocket.git + git clone https://github.com/ExpTechTW/DPIP.git ``` 2. 進入專案目錄 @@ -119,13 +107,15 @@ Tools • Dart 3.9.0 • DevTools 2.48.0 3. 安裝相依套件 ```bash - flutter pub get --no-example + mise exec -- flutter pub get ``` + > 若卡在 *Downloading packages*,改用本機快取:`mise exec -- flutter pub get --offline` + 4. 產生建置檔案 ```bash - dart run build_runner build + mise exec -- dart run build_runner build --delete-conflicting-outputs ``` 5. 建置應用程式 @@ -133,37 +123,37 @@ Tools • Dart 3.9.0 • DevTools 2.48.0 - **Android APK** ```bash - flutter build apk --release + mise exec -- flutter build apk --release ``` - **iOS** ```bash - flutter build ios --release + mise exec -- flutter build ios --release ``` ## 如何貢獻 我們歡迎各種形式的貢獻!你可以透過以下方式參與專案: -- 回報問題或提出新功能建議:請在 [Issues](https://github.com/ExpTechTW/DPIP-Pocket/issues) 中提出 -- 提交程式碼:請 [Fork](https://github.com/ExpTechTW/DPIP-Pocket/fork) 此倉庫,建立新分支進行修改,然後提交 [Pull Request](https://github.com/ExpTechTW/TREM/pulls) +- 回報問題或提出新功能建議:請在 [Issues](https://github.com/ExpTechTW/DPIP/issues) 中提出 +- 提交程式碼:請 [Fork](https://github.com/ExpTechTW/DPIP/fork) 此倉庫,建立新分支進行修改,然後提交 [Pull Request](https://github.com/ExpTechTW/DPIP/pulls) - 改進文件:協助我們改進現有文件或撰寫新文件 衷心感謝所有讓 DPIP 成為可能的貢獻者: - + ## 開放原始碼授權 -詳細的授權資訊請參閱 [LICENSE](LICENSE) 檔案 +本專案以開放原始碼授權釋出,詳細授權資訊請見 GitHub 儲存庫。 ## Star History - + - - - Star History Chart + + + Star History Chart diff --git a/analysis_options.yaml b/analysis_options.yaml index b9a6fec0c..345c5b0ea 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,12 @@ analyzer: - "**/*.freezed.dart" - "**/*.g.dart" - lib/l10n/gen/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** errors: # Logging must go through core/logging Log — never print/debugPrint. avoid_print: error diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 028a85ff3..87f3bdf3c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -35,6 +35,17 @@ android { targetSdk = flutter.targetSdkVersion versionCode = 300909009 versionName = flutter.versionName + + // Flutter ≥3.35 auto-sets release abiFilters to its 3 supported + // architectures (union with whatever defaultConfig declares), so the + // map SDK's libmaplibre.so gets copied for ABI-less engines too. Clear + // and pin to arm64-v8a: minSdk 26 means no armv7-era devices, and + // x86_64 is emulator-only (debug builds, which keep all ABIs). This + // drops the dead libmaplibre.so copies (~18MB uncompressed). + ndk { + abiFilters.clear() + abiFilters.addAll(listOf("arm64-v8a")) + } } signingConfigs { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 98cf8741c..1fc218097 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,17 @@ keeps running on aggressive battery managers (safety-app use). --> + + + + + + result.notImplemented() } } + + /** Total physical RAM in MiB — the cheap proxy for the low-end tier. */ + private fun totalMemoryMb(): Long { + val mem = ActivityManager.MemoryInfo() + (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) + .getMemoryInfo(mem) + return mem.totalMem / 1024 / 1024 + } } diff --git a/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt b/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt index a18d3f729..d0228d2ab 100644 --- a/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt +++ b/android/app/src/main/kotlin/com/exptech/dpip/MainActivity.kt @@ -35,12 +35,19 @@ class MainActivity : FlutterActivity() { MethodChannel(messenger, MapCacheChannel.NAME) .setMethodCallHandler(MapCacheChannel(applicationContext)) + MethodChannel(messenger, StorageScanChannel.NAME) + .setMethodCallHandler(StorageScanChannel(applicationContext)) + MethodChannel(messenger, BackgroundLocationChannel.NAME) .setMethodCallHandler(BackgroundLocationChannel(applicationContext)) MethodChannel(messenger, BatteryOptimizationChannel.NAME) .setMethodCallHandler(BatteryOptimizationChannel(applicationContext)) + // Activity-scoped: the keep-awake window flag belongs to this window. + MethodChannel(messenger, ScreenWakeChannel.NAME) + .setMethodCallHandler(ScreenWakeChannel(this)) + EventChannel(messenger, CompassChannel.NAME) .setStreamHandler(CompassChannel(applicationContext)) } diff --git a/android/app/src/main/kotlin/com/exptech/dpip/ScreenWakeChannel.kt b/android/app/src/main/kotlin/com/exptech/dpip/ScreenWakeChannel.kt new file mode 100644 index 000000000..e56ad4fdc --- /dev/null +++ b/android/app/src/main/kotlin/com/exptech/dpip/ScreenWakeChannel.kt @@ -0,0 +1,50 @@ +package com.exptech.dpip + +import android.app.Activity +import android.view.WindowManager +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +/** + * Keeps the display on while a screen asks for it — the mesh conversation, so + * a radio being watched doesn't go dark mid-exchange. + * + * Uses the window flag rather than a `PowerManager` wake lock on purpose: the + * flag is scoped to this activity's visibility, so the screen stops being held + * the moment the app is backgrounded or the activity is destroyed. A wake lock + * would outlive both and could be leaked into the background. + * + * Takes the Activity, not the application context: window flags belong to a + * window. + */ +class ScreenWakeChannel(private val activity: Activity) : + MethodChannel.MethodCallHandler { + + companion object { + const val NAME = "com.exptech.dpip/screen_wake" + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "enable" -> { + activity.runOnUiThread { + activity.window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + ) + } + result.success(null) + } + + "disable" -> { + activity.runOnUiThread { + activity.window.clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + ) + } + result.success(null) + } + + else -> result.notImplemented() + } + } +} diff --git a/android/app/src/main/kotlin/com/exptech/dpip/StorageScanChannel.kt b/android/app/src/main/kotlin/com/exptech/dpip/StorageScanChannel.kt new file mode 100644 index 000000000..9595d9080 --- /dev/null +++ b/android/app/src/main/kotlin/com/exptech/dpip/StorageScanChannel.kt @@ -0,0 +1,86 @@ +package com.exptech.dpip + +import android.content.Context +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.io.File + +/** + * App-owned channel that scans the app's on-disk usage (Android Settings + * reports the same numbers) and clears the system HTTP cache. The big + * consumers on Android are the SQLite ETag cache in [Context.cacheDir] and + * MapLibre's own database under [Context.filesDir]. + */ +class StorageScanChannel(private val context: Context) : MethodChannel.MethodCallHandler { + companion object { + const val NAME = "com.exptech.dpip/storage_scan" + + /** Files above this size are reported individually by `scan`. */ + private const val TOP_FILE_FLOOR = 512 * 1024L + + /** Hard cap on visited files so a pathological tree can't hang the channel. */ + private const val VISIT_CAP = 100_000 + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "scan" -> result.success(scan()) + "configure" -> result.success(null) + "clearSystemHttpCache" -> result.success(null) + // No separate tmp tree on Android — cacheDir is the whole story + // and is covered by the app-side cache clears. + "clearTmp" -> result.success(null) + else -> result.notImplemented() + } + } + + private fun scan(): Map { + val dirs = listOf(context.cacheDir, context.filesDir).filterNotNull() + val dirEntries = mutableListOf>() + val topFiles = mutableListOf>() + var total = 0L + for (dir in dirs) { + val walked = walk(dir) + dirEntries.add(mapOf("path" to dir.absolutePath, "bytes" to walked.first)) + total += walked.first + topFiles.addAll(walked.second) + } + topFiles.sortByDescending { it.second } + return mapOf( + "totalBytes" to total, + "dirs" to dirEntries, + "files" to topFiles.take(30).map { + mapOf("path" to it.first, "bytes" to it.second) + }, + ) + } + + /** One pass over [root]: total file bytes plus every file above the floor. */ + private fun walk(root: File): Pair>> { + var bytes = 0L + val top = mutableListOf>() + val stack = ArrayDeque() + stack.add(root) + var visited = 0 + while (stack.isNotEmpty()) { + val dir = stack.removeLast() + val children = dir.listFiles() ?: continue + for (child in children) { + visited++ + if (visited > VISIT_CAP) return bytes to top + if (child.isDirectory) { + stack.add(child) + } else { + val size = child.length() + if (size > 0) { + bytes += size + if (size >= TOP_FILE_FLOOR) { + top.add(child.absolutePath to size) + } + } + } + } + } + return bytes to top + } +} diff --git a/android/app/src/main/res/raw/eew.mp3 b/android/app/src/main/res/raw/eew.mp3 new file mode 100644 index 000000000..9f1570a1d Binary files /dev/null and b/android/app/src/main/res/raw/eew.mp3 differ diff --git a/android/app/src/main/res/raw/eew.ogg b/android/app/src/main/res/raw/eew.ogg deleted file mode 100644 index 3263a5415..000000000 Binary files a/android/app/src/main/res/raw/eew.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/eew_alert.mp3 b/android/app/src/main/res/raw/eew_alert.mp3 new file mode 100644 index 000000000..d2a6434c5 Binary files /dev/null and b/android/app/src/main/res/raw/eew_alert.mp3 differ diff --git a/android/app/src/main/res/raw/eew_alert.ogg b/android/app/src/main/res/raw/eew_alert.ogg deleted file mode 100644 index 9f37891e8..000000000 Binary files a/android/app/src/main/res/raw/eew_alert.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/eq.mp3 b/android/app/src/main/res/raw/eq.mp3 new file mode 100644 index 000000000..c2eb8602f Binary files /dev/null and b/android/app/src/main/res/raw/eq.mp3 differ diff --git a/android/app/src/main/res/raw/eq.ogg b/android/app/src/main/res/raw/eq.ogg deleted file mode 100644 index 65718efa7..000000000 Binary files a/android/app/src/main/res/raw/eq.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/info.mp3 b/android/app/src/main/res/raw/info.mp3 new file mode 100644 index 000000000..ef4a6eec3 Binary files /dev/null and b/android/app/src/main/res/raw/info.mp3 differ diff --git a/android/app/src/main/res/raw/info.ogg b/android/app/src/main/res/raw/info.ogg deleted file mode 100644 index cd181e564..000000000 Binary files a/android/app/src/main/res/raw/info.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/normal.mp3 b/android/app/src/main/res/raw/normal.mp3 new file mode 100644 index 000000000..fa8dea87e Binary files /dev/null and b/android/app/src/main/res/raw/normal.mp3 differ diff --git a/android/app/src/main/res/raw/normal.ogg b/android/app/src/main/res/raw/normal.ogg deleted file mode 100644 index ee78c196c..000000000 Binary files a/android/app/src/main/res/raw/normal.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/rain.mp3 b/android/app/src/main/res/raw/rain.mp3 new file mode 100644 index 000000000..13bc48bef Binary files /dev/null and b/android/app/src/main/res/raw/rain.mp3 differ diff --git a/android/app/src/main/res/raw/rain.ogg b/android/app/src/main/res/raw/rain.ogg deleted file mode 100644 index f8271183a..000000000 Binary files a/android/app/src/main/res/raw/rain.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/report.mp3 b/android/app/src/main/res/raw/report.mp3 new file mode 100644 index 000000000..374c69123 Binary files /dev/null and b/android/app/src/main/res/raw/report.mp3 differ diff --git a/android/app/src/main/res/raw/report.ogg b/android/app/src/main/res/raw/report.ogg deleted file mode 100644 index b8cb90698..000000000 Binary files a/android/app/src/main/res/raw/report.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/tsunami.mp3 b/android/app/src/main/res/raw/tsunami.mp3 new file mode 100644 index 000000000..0a0b3dafc Binary files /dev/null and b/android/app/src/main/res/raw/tsunami.mp3 differ diff --git a/android/app/src/main/res/raw/tsunami.ogg b/android/app/src/main/res/raw/tsunami.ogg deleted file mode 100644 index 8a2d6217f..000000000 Binary files a/android/app/src/main/res/raw/tsunami.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/warn.mp3 b/android/app/src/main/res/raw/warn.mp3 new file mode 100644 index 000000000..5e9df9c82 Binary files /dev/null and b/android/app/src/main/res/raw/warn.mp3 differ diff --git a/android/app/src/main/res/raw/warn.ogg b/android/app/src/main/res/raw/warn.ogg deleted file mode 100644 index a00609bc9..000000000 Binary files a/android/app/src/main/res/raw/warn.ogg and /dev/null differ diff --git a/android/app/src/main/res/raw/weather.mp3 b/android/app/src/main/res/raw/weather.mp3 new file mode 100644 index 000000000..635639f6d Binary files /dev/null and b/android/app/src/main/res/raw/weather.mp3 differ diff --git a/android/app/src/main/res/raw/weather.ogg b/android/app/src/main/res/raw/weather.ogg deleted file mode 100644 index 6a2736320..000000000 Binary files a/android/app/src/main/res/raw/weather.ogg and /dev/null differ diff --git a/api.md b/api.md index 63e2cb1cc..a537c61af 100644 --- a/api.md +++ b/api.md @@ -12,7 +12,8 @@ > 這是**端點目錄**,不是程式碼對照表。沒有 `lib/api/` 巨石檔:每個端點在其所屬 > feature 的 `data/`(基礎設施則在 `core/`)裡,各自建成一個輕薄的 datasource, -> 並帶著自己的 `ApiTier`。 +> 並帶著自己的 `ApiTier`(`core/network/api_region.dart`);路徑字串集中於 +> `core/network/api_paths.dart`(與 `EtagInterceptor` 共用,不會漂移)。 > > **對時不是 HTTP 端點。** App 的時鐘使用真正的 **SNTP** > (`flutter_ntp`,UDP/123),對 `time.exptech.com.tw`(主)/ @@ -28,6 +29,7 @@ | `openRtsSse` | `/api/v2/trem/rts?sse=1&compress=1` | `lbApi` | `api.lb-{tpe1,khh1}.exptech.dev` | | `getRtsRealtime` | `/api/v2/trem/rts` | `lbApi` | `api.lb-{tpe1,khh1}.exptech.dev` | | `getEewRealtime` | `/api/v2/eq/eew` | `lbApi` | `api.lb-{tpe1,khh1}.exptech.dev` | +| `getEewAt` | `/api/v2/eq/eew/{sec}` | `coreApi` | `api.core-{tyo1,tnn1}.exptech.dev` | | `getReportList` | `/api/v2/eq/report` | `coreApi` | `api.core-{tyo1,tnn1}.exptech.dev` | | `getReport` | `/api/v2/eq/report/{id}` | `coreApi` | `api.core-{tyo1,tnn1}.exptech.dev` | @@ -36,6 +38,7 @@ > 區間、`startTime`/`endTime` 為 **`YYYY-MM-DD`(Asia/Taipei 當日)**、可選 > `city`/`cityMinInt`/`cityMaxInt`。`loc` 與經緯度篩選已移除。伺服器會把非正規 > query **302** 到 canonical(參數字母序、去掉預設值)以利 ETag/快取。 +> `getEewAt` 是**歷史回放**(時間軸),tier 為 `coreApi`。 > **即時串流走 SSE(gzip 壓縮),不是輪詢。** `?sse=1` 把端點切換成 > `text/event-stream`;再加 `&compress=1`,payload 會以 `event: g` 事件送出,其 @@ -108,16 +111,31 @@ QPESUMS 定量降水預報 XYZ WebP。時間清單是差量編碼的 Unix **毫 ### 防災地圖 DPM(v2)—— `core-tnn1` -MapLibre **vector tiles**(gzip MVT)+ 點位詳情 JSON。目前僅 **AED**;未來其他 -類型走同一路徑形狀 `/api/v2/tiles/dpm/{layer}/…`。Tile 與詳情都在 **static** -主機(`Cache-Control: max-age=60, must-revalidate` + ETag);tile 由 MapLibre -直接抓,詳情經 `ApiClient`。Source-layer 名 = `{layer}`(AED 為 `aed`)。單點有 -`id`(內部 PK,打詳情用,非 `aed_id`);低 zoom 的 cluster 帶 `point_count`。 +MapLibre **vector tiles**(gzip MVT)+ 點位詳情 JSON。目前有 **AED / 無障礙廁所 / +避難所**三層;其他類型走同一路徑形狀 `/api/v2/tiles/dpm/{layer}/…`。Tile 與詳情都 +在 **static** 主機(`Cache-Control: max-age=60, must-revalidate` + ETag); +tile 由 MapLibre 直接抓,詳情經 `ApiClient`。Source-layer 名 = `{layer}` +(AED 為 `aed`)。單點有 `id`(內部 PK,打詳情用,非 `aed_id`);低 zoom 的 +cluster 帶 `point_count`。 | 方法 | 路徑 | 層級 | 主機 | |---|---|---|---| | `tileUrl` | `/api/v2/tiles/dpm/{layer}/{z}/{x}/{y}.mvt` | `coreStaticExclusive` | `static.core-tnn1.exptech.dev` | | `getAedDetail` | `/api/v2/tiles/dpm/aed/{id}` | `coreStaticExclusive` | `static.core-tnn1.exptech.dev` | +| `getRestroomDetail` | `/api/v2/tiles/dpm/restroom/{id}` | `coreStaticExclusive` | `static.core-tnn1.exptech.dev` | +| `getShelterDetail` | `/api/v2/tiles/dpm/shelter/{id}` | `coreStaticExclusive` | `static.core-tnn1.exptech.dev` | + +### 風場 Wind(v2 / v1)—— `core-tnn1` + +風場 overlay:XYZ WebP 圖層 + 低 zoom 的 **`.bin` 向量風場**(`WND1` 格式, +`fetchWindBin`)。時間清單/圖層與其他 tiles 家族同形狀;`.bin` 用 `{model}` +(`gfs` / `ecmwf`)與 `{frame}` 定址。圖層選擇器把 wind 註冊為獨立圖層。 + +| 方法 | 路徑 | 層級 | 主機 | +|---|---|---|---| +| `getFrames` | `/api/v2/tiles/wind/list[?model=…]` | `coreExclusiveApi` | `api.core-tnn1.exptech.dev` | +| `tileUrl` | `/api/v2/tiles/wind/{ts}/{z}/{x}/{y}.webp[?model=…]` | `coreStaticExclusive` | `static.core-tnn1.exptech.dev` | +| `fetchWindBin` | `/api/v1/wind/{model}/{frame}.bin` | `coreStaticExclusive` | `static.core-tnn1.exptech.dev` | ### 氣象家族(**v5**)—— `core-tnn1` @@ -184,31 +202,22 @@ typhoon)共用同一組形狀:`/api/v5/meteor/{family}` 是最新快照、`/ | `getHistoryRegion` | `/api/v1/dpip/history/{region}` | `legacyApi` | 事件頁(鄉鎮) | | `getRealtimeList` | `/api/v1/dpip/realtime/list` | `legacyApi` | 首頁拖盤收起(全國生效中) | | `getRealtimeRegion` | `/api/v1/dpip/realtime/{region}` | `legacyApi` | 首頁拖盤收起(鄉鎮生效中) | +| `getRtsAt` | `/api/v2/trem/rts/{sec}` | `legacyApi` | 強震波形回放(時間軸) | 尚未接上、但端點存在於 `api-1`: | 方法 | 路徑 | 層級 | |---|---|---| | `getEvent` | `/api/v1/dpip/event/{id}` | `legacyApi` | -| `getRtsAt` | `/api/v2/trem/rts/{sec}` | `legacyApi` | -| `getEewAt` | `/api/v2/eq/eew/{sec}` | `legacyApi` | - -## 暫時無 (unavailable) - -| 方法 | 說明 | -|---|---| -| `getTsunamiList` | 暫時無法使用 —— 會拋出 `UnsupportedError`。 | -| `getTsunami` | `/api/v1/tsunami/{id}` 於 `api-1` 回 404(2026-08-02 實測)。 | ## 外部(第三方,無區域) | 方法 | URL | |---|---| -| `getLocalizationProgress` | `https://exptech.dev/api/v1/dpip/locale` | | `getReleases` | `https://api.github.com/repos/ExpTechTW/DPIP/releases`(ETag;`per_page=30`) | | `getRainHourForecast` | `https://exptech.dingbot.tw/api/weather/rainforecast/{code}`(`{code}` = 鄉鎮 3 碼;回應為單 series 信封 `{"<系列名>": [{"start": 秒, "rain": [60 × mm]}]}`;空 series `[]` = 該小時無雨,卡片隱藏) | -## curl 可用性(2026-08-02,HTTP 狀態碼) +## curl 可用性(2026-08-02 實測,HTTP 狀態碼) | 端點 | lb-tpe1 | lb-khh1 | core-tyo1 | core-tnn1 | api-1 | |---|:--:|:--:|:--:|:--:|:--:| diff --git a/assets/astro/README.md b/assets/astro/README.md new file mode 100644 index 000000000..b4bcd886d --- /dev/null +++ b/assets/astro/README.md @@ -0,0 +1,15 @@ +# Lunar surface maps + +Source: **NASA/Goddard Scientific Visualization Studio — CGI Moon Kit** + + +| File | What | Origin | +|---|---|---| +| `moon_color_2k.jpg` | Colour / albedo, equirectangular 2048×1024, centred on 0° longitude | `lroc_color_2k.jpg` — Hapke-normalised mosaic of >100,000 LRO Wide Angle Camera images, poles filled from the LOLA albedo map | +| `moon_height_1k.png` | Elevation, equirectangular 1024×512, 8-bit greyscale | `ldem_4_uint.tif` (LOLA laser altimeter, 4 px/°) downsampled and converted | + +Both are derived from Lunar Reconnaissance Orbiter data. NASA imagery is +generally **not copyrighted** and may be used for any purpose; NASA requests +attribution and does not endorse any product. Kept as PNG for the elevation +map on purpose: the shader takes finite differences of it to build surface +normals, and JPEG block artefacts would show up as banding in the relief. diff --git a/assets/astro/constellations.bin.gz b/assets/astro/constellations.bin.gz new file mode 100644 index 000000000..3ea4a1a31 Binary files /dev/null and b/assets/astro/constellations.bin.gz differ diff --git a/assets/astro/moon_color_2k.jpg b/assets/astro/moon_color_2k.jpg new file mode 100644 index 000000000..ed05d2cbc Binary files /dev/null and b/assets/astro/moon_color_2k.jpg differ diff --git a/assets/astro/moon_height_1k.png b/assets/astro/moon_height_1k.png new file mode 100644 index 000000000..7967875e3 Binary files /dev/null and b/assets/astro/moon_height_1k.png differ diff --git a/assets/astro/satellites.tle b/assets/astro/satellites.tle new file mode 100644 index 000000000..e66174880 --- /dev/null +++ b/assets/astro/satellites.tle @@ -0,0 +1,6 @@ +ISS (ZARYA) +1 25544U 98067A 26226.43871707 .00004555 00000+0 89427-4 0 9997 +2 25544 51.6329 11.7957 0007493 45.9133 314.2471 15.49439755580788 +CSS (TIANHE) +1 48274U 21035A 26224.98627525 .00000101 00000+0 54127-5 0 9991 +2 48274 41.4709 337.2096 0001079 250.4973 109.5748 15.58975796302033 diff --git a/assets/astro/stars.bin.gz b/assets/astro/stars.bin.gz new file mode 100644 index 000000000..2568c42a8 Binary files /dev/null and b/assets/astro/stars.bin.gz differ diff --git a/assets/box.json b/assets/box.json deleted file mode 100644 index 90e6f3b39..000000000 --- a/assets/box.json +++ /dev/null @@ -1 +0,0 @@ -{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,25.1],[121.38,25.46],[120.89,25.46],[120.89,25.1],[121.38,25.1]]]},"properties":{"ID":0}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,25.1],[121.86,25.46],[121.38,25.46],[121.38,25.1],[121.86,25.1]]]},"properties":{"ID":1}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[122.35,25.1],[122.35,25.46],[121.86,25.46],[121.86,25.1],[122.35,25.1]]]},"properties":{"ID":2}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,24.74],[121.38,25.1],[120.89,25.1],[120.89,24.74],[121.38,24.74]]]},"properties":{"ID":3}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,24.74],[121.86,25.1],[121.38,25.1],[121.38,24.74],[121.86,24.74]]]},"properties":{"ID":4}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[122.35,24.74],[122.35,25.1],[121.86,25.1],[121.86,24.74],[122.35,24.74]]]},"properties":{"ID":5}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,24.38],[120.89,24.74],[120.4,24.74],[120.4,24.38],[120.89,24.38]]]},"properties":{"ID":6}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,24.38],[121.38,24.74],[120.89,24.74],[120.89,24.38],[121.38,24.38]]]},"properties":{"ID":7}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,24.38],[121.86,24.74],[121.38,24.74],[121.38,24.38],[121.86,24.38]]]},"properties":{"ID":8}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[122.35,24.38],[122.35,24.74],[121.86,24.74],[121.86,24.38],[122.35,24.38]]]},"properties":{"ID":9}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.4,24.02],[120.4,24.38],[119.92,24.38],[119.92,24.02],[120.4,24.02]]]},"properties":{"ID":10}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,24.02],[120.89,24.38],[120.4,24.38],[120.4,24.02],[120.89,24.02]]]},"properties":{"ID":11}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,24.02],[121.38,24.38],[120.89,24.38],[120.89,24.02],[121.38,24.02]]]},"properties":{"ID":12}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,24.02],[121.86,24.38],[121.38,24.38],[121.38,24.02],[121.86,24.02]]]},"properties":{"ID":13}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[119.92,23.66],[119.92,24.02],[119.43,24.02],[119.43,23.66],[119.92,23.66]]]},"properties":{"ID":14}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.4,23.66],[120.4,24.02],[119.92,24.02],[119.92,23.66],[120.4,23.66]]]},"properties":{"ID":15}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,23.66],[120.89,24.02],[120.4,24.02],[120.4,23.66],[120.89,23.66]]]},"properties":{"ID":16}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,23.66],[121.38,24.02],[120.89,24.02],[120.89,23.66],[121.38,23.66]]]},"properties":{"ID":17}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,23.66],[121.86,24.02],[121.38,24.02],[121.38,23.66],[121.86,23.66]]]},"properties":{"ID":18}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[119.43,23.3],[119.43,23.66],[118.95,23.66],[118.95,23.3],[119.43,23.3]]]},"properties":{"ID":19}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[119.92,23.3],[119.92,23.66],[119.43,23.66],[119.43,23.3],[119.92,23.3]]]},"properties":{"ID":20}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.4,23.3],[120.4,23.66],[119.92,23.66],[119.92,23.3],[120.4,23.3]]]},"properties":{"ID":21}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,23.3],[120.89,23.66],[120.4,23.66],[120.4,23.3],[120.89,23.3]]]},"properties":{"ID":22}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,23.3],[121.38,23.66],[120.89,23.66],[120.89,23.3],[121.38,23.3]]]},"properties":{"ID":23}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,23.3],[121.86,23.66],[121.38,23.66],[121.38,23.3],[121.86,23.3]]]},"properties":{"ID":24}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[119.43,22.94],[119.43,23.3],[118.95,23.3],[118.95,22.94],[119.43,22.94]]]},"properties":{"ID":25}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[119.92,22.94],[119.92,23.3],[119.43,23.3],[119.43,22.94],[119.92,22.94]]]},"properties":{"ID":26}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.4,22.94],[120.4,23.3],[119.92,23.3],[119.92,22.94],[120.4,22.94]]]},"properties":{"ID":27}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,22.94],[120.89,23.3],[120.4,23.3],[120.4,22.94],[120.89,22.94]]]},"properties":{"ID":28}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,22.94],[121.38,23.3],[120.89,23.3],[120.89,22.94],[121.38,22.94]]]},"properties":{"ID":29}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,22.94],[121.86,23.3],[121.38,23.3],[121.38,22.94],[121.86,22.94]]]},"properties":{"ID":30}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.4,22.58],[120.4,22.94],[119.92,22.94],[119.92,22.58],[120.4,22.58]]]},"properties":{"ID":31}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,22.58],[120.89,22.94],[120.4,22.94],[120.4,22.58],[120.89,22.58]]]},"properties":{"ID":32}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,22.58],[121.38,22.94],[120.89,22.94],[120.89,22.58],[121.38,22.58]]]},"properties":{"ID":33}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,22.58],[121.86,22.94],[121.38,22.94],[121.38,22.58],[121.86,22.58]]]},"properties":{"ID":34}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.4,22.22],[120.4,22.58],[119.92,22.58],[119.92,22.22],[120.4,22.22]]]},"properties":{"ID":35}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,22.22],[120.89,22.58],[120.4,22.58],[120.4,22.22],[120.89,22.22]]]},"properties":{"ID":36}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,22.22],[121.38,22.58],[120.89,22.58],[120.89,22.22],[121.38,22.22]]]},"properties":{"ID":37}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[120.89,21.86],[120.89,22.22],[120.4,22.22],[120.4,21.86],[120.89,21.86]]]},"properties":{"ID":38}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.38,21.86],[121.38,22.22],[120.89,22.22],[120.89,21.86],[121.38,21.86]]]},"properties":{"ID":39}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[121.86,21.86],[121.86,22.22],[121.38,22.22],[121.38,21.86],[121.86,21.86]]]},"properties":{"ID":40}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[118.46,24.38],[118.46,24.74],[117.97,24.74],[117.97,24.38],[118.46,24.38]]]},"properties":{"ID":41}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[118.95,24.38],[118.95,24.74],[118.46,24.74],[118.46,24.38],[118.95,24.38]]]},"properties":{"ID":42}}]} \ No newline at end of file diff --git a/assets/box.json.gz b/assets/box.json.gz new file mode 100644 index 000000000..5000af7d5 Binary files /dev/null and b/assets/box.json.gz differ diff --git a/assets/location.json.gz b/assets/location.json.gz index c624b839f..029a4935f 100644 Binary files a/assets/location.json.gz and b/assets/location.json.gz differ diff --git a/assets/map/icons/cross.png b/assets/map/icons/cross.png deleted file mode 100644 index 4e177ddbb..000000000 Binary files a/assets/map/icons/cross.png and /dev/null differ diff --git a/assets/map/icons/intensity-1-dark.png b/assets/map/icons/intensity-1-dark.png deleted file mode 100644 index b28a3d6a1..000000000 Binary files a/assets/map/icons/intensity-1-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-1.png b/assets/map/icons/intensity-1.png deleted file mode 100644 index 0b6649fa1..000000000 Binary files a/assets/map/icons/intensity-1.png and /dev/null differ diff --git a/assets/map/icons/intensity-2-dark.png b/assets/map/icons/intensity-2-dark.png deleted file mode 100644 index 8caa74b21..000000000 Binary files a/assets/map/icons/intensity-2-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-2.png b/assets/map/icons/intensity-2.png deleted file mode 100644 index e9004fe7a..000000000 Binary files a/assets/map/icons/intensity-2.png and /dev/null differ diff --git a/assets/map/icons/intensity-3-dark.png b/assets/map/icons/intensity-3-dark.png deleted file mode 100644 index a449b02ba..000000000 Binary files a/assets/map/icons/intensity-3-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-3.png b/assets/map/icons/intensity-3.png deleted file mode 100644 index 26c4a2ecd..000000000 Binary files a/assets/map/icons/intensity-3.png and /dev/null differ diff --git a/assets/map/icons/intensity-4-dark.png b/assets/map/icons/intensity-4-dark.png deleted file mode 100644 index f991a0a1d..000000000 Binary files a/assets/map/icons/intensity-4-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-4.png b/assets/map/icons/intensity-4.png deleted file mode 100644 index 7c79b08d2..000000000 Binary files a/assets/map/icons/intensity-4.png and /dev/null differ diff --git a/assets/map/icons/intensity-5-dark.png b/assets/map/icons/intensity-5-dark.png deleted file mode 100644 index 75b7730c6..000000000 Binary files a/assets/map/icons/intensity-5-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-5.png b/assets/map/icons/intensity-5.png deleted file mode 100644 index 1cd02c24c..000000000 Binary files a/assets/map/icons/intensity-5.png and /dev/null differ diff --git a/assets/map/icons/intensity-6-dark.png b/assets/map/icons/intensity-6-dark.png deleted file mode 100644 index f87d569de..000000000 Binary files a/assets/map/icons/intensity-6-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-6.png b/assets/map/icons/intensity-6.png deleted file mode 100644 index f7afc68ab..000000000 Binary files a/assets/map/icons/intensity-6.png and /dev/null differ diff --git a/assets/map/icons/intensity-7-dark.png b/assets/map/icons/intensity-7-dark.png deleted file mode 100644 index 867a20de4..000000000 Binary files a/assets/map/icons/intensity-7-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-7.png b/assets/map/icons/intensity-7.png deleted file mode 100644 index eca3d1e99..000000000 Binary files a/assets/map/icons/intensity-7.png and /dev/null differ diff --git a/assets/map/icons/intensity-8-dark.png b/assets/map/icons/intensity-8-dark.png deleted file mode 100644 index 02f2e07b6..000000000 Binary files a/assets/map/icons/intensity-8-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-8.png b/assets/map/icons/intensity-8.png deleted file mode 100644 index 4480547ca..000000000 Binary files a/assets/map/icons/intensity-8.png and /dev/null differ diff --git a/assets/map/icons/intensity-9-dark.png b/assets/map/icons/intensity-9-dark.png deleted file mode 100644 index 525654176..000000000 Binary files a/assets/map/icons/intensity-9-dark.png and /dev/null differ diff --git a/assets/map/icons/intensity-9.png b/assets/map/icons/intensity-9.png deleted file mode 100644 index 3f1fd0d64..000000000 Binary files a/assets/map/icons/intensity-9.png and /dev/null differ diff --git a/assets/weather/sky/starmap.webp b/assets/weather/sky/starmap.webp index 267f96095..177dee88f 100644 Binary files a/assets/weather/sky/starmap.webp and b/assets/weather/sky/starmap.webp differ diff --git a/assets/weather/sky/sun_rays.webp b/assets/weather/sky/sun_rays.webp index 8257379bf..9a7b34007 100644 Binary files a/assets/weather/sky/sun_rays.webp and b/assets/weather/sky/sun_rays.webp differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 87515d42d..a6c6afbd2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -25,10 +25,12 @@ 9F74F772B6E8760DD47AB2FE /* normal.aiff in Resources */ = {isa = PBXBuildFile; fileRef = B916667D1B2356583B174E80 /* normal.aiff */; }; A8D382D04B4ACD327E29F46B /* eq.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 682D0165E2FF3895C5B252C5 /* eq.aiff */; }; AA0000000000000000000C02 /* CompassPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000C01 /* CompassPlugin.swift */; }; + AA0000000000000000000E02 /* ScreenWakePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000E01 /* ScreenWakePlugin.swift */; }; AA0000000000000000000D02 /* DeviceInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000D01 /* DeviceInfoPlugin.swift */; }; AE92AD9862B7A721B0924557 /* eew.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7A6E88CB92902C0CACB07792 /* eew.aiff */; }; CAC4EF00000000000000B001 /* MapCachePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC4EF00000000000000B002 /* MapCachePlugin.swift */; }; CAC4EF00000000000000C001 /* BackgroundLocationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC4EF00000000000000C002 /* BackgroundLocationPlugin.swift */; }; + CAC4EF00000000000000D001 /* StorageScanPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC4EF00000000000000D002 /* StorageScanPlugin.swift */; }; DB8E84D957A6C2B12EEB0AB7 /* report.aiff in Resources */ = {isa = PBXBuildFile; fileRef = FD769D73A7C4BE3619C1F9FB /* report.aiff */; }; E1EBD3F0B2991F7D60DF56C5 /* weather.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 762B2690D1D999F97D84162C /* weather.aiff */; }; F56D5186166227E46D96919D /* info.aiff in Resources */ = {isa = PBXBuildFile; fileRef = F9BCED8E5498E9FD7A454E8F /* info.aiff */; }; @@ -62,21 +64,21 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3AE87ED82FDB896B2B5C5F1B /* warn.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = warn.aiff; sourceTree = ""; }; + 3AE87ED82FDB896B2B5C5F1B /* warn.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/warn.aiff; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4D00D2962CF4598B61D0C722 /* eew_alert.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = eew_alert.aiff; sourceTree = ""; }; + 4D00D2962CF4598B61D0C722 /* eew_alert.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/eew_alert.aiff; sourceTree = ""; }; 522508B8301F863A006148C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; 522508BA301F8687006148C2 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; 522508BB301F868B006148C2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; 522508BC301F868E006148C2 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; - 682D0165E2FF3895C5B252C5 /* eq.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = eq.aiff; sourceTree = ""; }; + 682D0165E2FF3895C5B252C5 /* eq.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/eq.aiff; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 762B2690D1D999F97D84162C /* weather.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = weather.aiff; sourceTree = ""; }; + 762B2690D1D999F97D84162C /* weather.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/weather.aiff; sourceTree = ""; }; 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 797A71348A49484748BB3224 /* MapSnapshotPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapSnapshotPlugin.swift; sourceTree = ""; }; - 7A6E88CB92902C0CACB07792 /* eew.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = eew.aiff; sourceTree = ""; }; + 7A6E88CB92902C0CACB07792 /* eew.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/eew.aiff; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -85,15 +87,17 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A382CD9DEA741E45DBF741D7 /* rain.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = rain.aiff; sourceTree = ""; }; + A382CD9DEA741E45DBF741D7 /* rain.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/rain.aiff; sourceTree = ""; }; AA0000000000000000000C01 /* CompassPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompassPlugin.swift; sourceTree = ""; }; + AA0000000000000000000E01 /* ScreenWakePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenWakePlugin.swift; sourceTree = ""; }; AA0000000000000000000D01 /* DeviceInfoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoPlugin.swift; sourceTree = ""; }; - B916667D1B2356583B174E80 /* normal.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = normal.aiff; sourceTree = ""; }; + B916667D1B2356583B174E80 /* normal.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/normal.aiff; sourceTree = ""; }; CAC4EF00000000000000B002 /* MapCachePlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapCachePlugin.swift; sourceTree = ""; }; CAC4EF00000000000000C002 /* BackgroundLocationPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BackgroundLocationPlugin.swift; sourceTree = ""; }; - CCF0E2286072A9283A916C69 /* tsunami.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = tsunami.aiff; sourceTree = ""; }; - F9BCED8E5498E9FD7A454E8F /* info.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = info.aiff; sourceTree = ""; }; - FD769D73A7C4BE3619C1F9FB /* report.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = report.aiff; sourceTree = ""; }; + CAC4EF00000000000000D002 /* StorageScanPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StorageScanPlugin.swift; sourceTree = ""; }; + CCF0E2286072A9283A916C69 /* tsunami.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/tsunami.aiff; sourceTree = ""; }; + F9BCED8E5498E9FD7A454E8F /* info.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/info.aiff; sourceTree = ""; }; + FD769D73A7C4BE3619C1F9FB /* report.aiff */ = {isa = PBXFileReference; includeInIndex = 1; path = Sounds/report.aiff; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -166,12 +170,14 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, AA0000000000000000000C01 /* CompassPlugin.swift */, + AA0000000000000000000E01 /* ScreenWakePlugin.swift */, AA0000000000000000000D01 /* DeviceInfoPlugin.swift */, 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 797A71348A49484748BB3224 /* MapSnapshotPlugin.swift */, CAC4EF00000000000000B002 /* MapCachePlugin.swift */, CAC4EF00000000000000C002 /* BackgroundLocationPlugin.swift */, + CAC4EF00000000000000D002 /* StorageScanPlugin.swift */, 7A6E88CB92902C0CACB07792 /* eew.aiff */, 4D00D2962CF4598B61D0C722 /* eew_alert.aiff */, 682D0165E2FF3895C5B252C5 /* eq.aiff */, @@ -356,12 +362,14 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, AA0000000000000000000C02 /* CompassPlugin.swift in Sources */, + AA0000000000000000000E02 /* ScreenWakePlugin.swift in Sources */, AA0000000000000000000D02 /* DeviceInfoPlugin.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, 071504DB0F1524DF85241961 /* MapSnapshotPlugin.swift in Sources */, CAC4EF00000000000000B001 /* MapCachePlugin.swift in Sources */, CAC4EF00000000000000C001 /* BackgroundLocationPlugin.swift in Sources */, + CAC4EF00000000000000D001 /* StorageScanPlugin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 11c5949ca..e8d7affd2 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", - "version" : "5.3.0" + "revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc", + "version" : "5.3.1" } }, { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0bc0f97e9..369008fb8 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -22,6 +22,8 @@ import UIKit CompassPlugin.register(with: registry.registrar(forPlugin: "CompassPlugin")!) MapSnapshotPlugin.register(with: registry.registrar(forPlugin: "MapSnapshotPlugin")!) MapCachePlugin.register(with: registry.registrar(forPlugin: "MapCachePlugin")!) + StorageScanPlugin.register(with: registry.registrar(forPlugin: "StorageScanPlugin")!) + ScreenWakePlugin.register(with: registry.registrar(forPlugin: "ScreenWakePlugin")!) BackgroundLocationPlugin.register( with: registry.registrar(forPlugin: "BackgroundLocationPlugin")!) } diff --git a/ios/Runner/DeviceInfoPlugin.swift b/ios/Runner/DeviceInfoPlugin.swift index 1df15ed8a..e6b1cbcfc 100644 --- a/ios/Runner/DeviceInfoPlugin.swift +++ b/ios/Runner/DeviceInfoPlugin.swift @@ -25,6 +25,7 @@ public class DeviceInfoPlugin: NSObject, FlutterPlugin { "osVersion": UIDevice.current.systemVersion, "sdkInt": NSNull(), "identifier": UIDevice.current.identifierForVendor?.uuidString as Any, + "totalMemoryMb": ProcessInfo.processInfo.physicalMemory / 1024 / 1024, ]) default: result(FlutterMethodNotImplemented) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index cc034f652..055e3eefe 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -12,6 +12,12 @@ DPIP 需要你的位置,以提供你所在地的地震、天氣等災害警報。 NSLocationAlwaysAndWhenInUseUsageDescription DPIP 需要在背景持續存取你的位置,即使關閉 App 也能為你所在地推送地震、天氣等災害警報。 + + NSBluetoothAlwaysUsageDescription + DPIP 需要藍牙以連線 Meshtastic LoRa 無線電,在通訊中斷時收發緊急訊息。 + NSBluetoothPeripheralUsageDescription + DPIP 需要藍牙以連線 Meshtastic LoRa 無線電,在通訊中斷時收發緊急訊息。 CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion diff --git a/ios/Runner/ScreenWakePlugin.swift b/ios/Runner/ScreenWakePlugin.swift new file mode 100644 index 000000000..3b25d4825 --- /dev/null +++ b/ios/Runner/ScreenWakePlugin.swift @@ -0,0 +1,33 @@ +import Flutter +import UIKit + +/// Keeps the display awake while a screen asks for it — the mesh conversation, +/// so a radio being watched doesn't go dark mid-exchange. +/// +/// `isIdleTimerDisabled` only applies to the foreground app, so backgrounding +/// already neutralises it; the flag is still cleared explicitly when the screen +/// that asked for it goes away, so it can never outlive its reason. +public class ScreenWakePlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "com.exptech.dpip/screen_wake", + binaryMessenger: registrar.messenger()) + registrar.addMethodCallDelegate(ScreenWakePlugin(), channel: channel) + } + + public func handle( + _ call: FlutterMethodCall, result: @escaping FlutterResult + ) { + switch call.method { + case "enable", "disable": + let keepAwake = call.method == "enable" + // UIApplication is main-thread only. + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = keepAwake + result(nil) + } + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/ios/Runner/Sounds/eew.aiff b/ios/Runner/Sounds/eew.aiff new file mode 100644 index 000000000..3738d70a6 Binary files /dev/null and b/ios/Runner/Sounds/eew.aiff differ diff --git a/ios/Runner/Sounds/eew_alert.aiff b/ios/Runner/Sounds/eew_alert.aiff new file mode 100644 index 000000000..fac4c2298 Binary files /dev/null and b/ios/Runner/Sounds/eew_alert.aiff differ diff --git a/ios/Runner/Sounds/eq.aiff b/ios/Runner/Sounds/eq.aiff new file mode 100644 index 000000000..1de45f98e Binary files /dev/null and b/ios/Runner/Sounds/eq.aiff differ diff --git a/ios/Runner/Sounds/info.aiff b/ios/Runner/Sounds/info.aiff new file mode 100644 index 000000000..10128c020 Binary files /dev/null and b/ios/Runner/Sounds/info.aiff differ diff --git a/ios/Runner/Sounds/normal.aiff b/ios/Runner/Sounds/normal.aiff new file mode 100644 index 000000000..b0160abe3 Binary files /dev/null and b/ios/Runner/Sounds/normal.aiff differ diff --git a/ios/Runner/Sounds/rain.aiff b/ios/Runner/Sounds/rain.aiff new file mode 100644 index 000000000..a7633adbe Binary files /dev/null and b/ios/Runner/Sounds/rain.aiff differ diff --git a/ios/Runner/Sounds/report.aiff b/ios/Runner/Sounds/report.aiff new file mode 100644 index 000000000..549bbd37f Binary files /dev/null and b/ios/Runner/Sounds/report.aiff differ diff --git a/ios/Runner/Sounds/tsunami.aiff b/ios/Runner/Sounds/tsunami.aiff new file mode 100644 index 000000000..24c8257d1 Binary files /dev/null and b/ios/Runner/Sounds/tsunami.aiff differ diff --git a/ios/Runner/Sounds/warn.aiff b/ios/Runner/Sounds/warn.aiff new file mode 100644 index 000000000..32ac13aa3 Binary files /dev/null and b/ios/Runner/Sounds/warn.aiff differ diff --git a/ios/Runner/Sounds/weather.aiff b/ios/Runner/Sounds/weather.aiff new file mode 100644 index 000000000..6b75898e2 Binary files /dev/null and b/ios/Runner/Sounds/weather.aiff differ diff --git a/ios/Runner/StorageScanPlugin.swift b/ios/Runner/StorageScanPlugin.swift new file mode 100644 index 000000000..65dd27436 --- /dev/null +++ b/ios/Runner/StorageScanPlugin.swift @@ -0,0 +1,135 @@ +import Flutter +import Foundation +import UIKit + +/// App-owned channel that scans the sandbox for disk usage, bounds the system +/// HTTP cache, and clears it. +/// +/// iOS Settings reports the whole sandbox ("文件與資料"), which is far larger +/// than the ETag cache budget (150 MB of body blobs): the SQLite file carries +/// page/free-space overhead on top of the bodies, and the system URL cache +/// (NSURLCache, shared by every NSURLSession) can quietly hold hundreds of MB +/// of disk. The Debug page needs real numbers, so `scan` measures every +/// top-level sandbox directory and the biggest files. +public class StorageScanPlugin: NSObject, FlutterPlugin { + /// Files above this size are reported individually by `scan`. + static let topFileFloor: Int64 = 512 * 1024 + + /// Hard cap on visited files per directory so a pathological tree can't hang + /// the channel. + static let visitCap = 100_000 + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "com.exptech.dpip/storage_scan", + binaryMessenger: registrar.messenger() + ) + registrar.addMethodCallDelegate(StorageScanPlugin(), channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "scan": + result(scan()) + case "configure": + // The disk URL cache is turned off outright: every byte MapLibre + // downloads is persisted in the app's own SQLite store through the Dart + // tile bridge, so a second disk copy is pure overhead. The remaining + // memory-only cache still lets a SQLite miss short-circuit the network + // without costing disk. Any residue from before this ran is dropped so + // the change actually takes effect. + URLCache.shared.memoryCapacity = 16 * 1024 * 1024 + URLCache.shared.diskCapacity = 0 + URLCache.shared.removeAllCachedResponses() + result(nil) + case "clearSystemHttpCache": + URLCache.shared.removeAllCachedResponses() + result(nil) + case "clearTmp": + // The app's own caches never touch tmp — anything in there is either a + // leftover from an aborted native operation or MapLibre's transient + // tile work. Both are safe to drop at any time; iOS purges tmp on its + // own when space runs low anyway, so this is just reclaiming early. + let tmp = URL(fileURLWithPath: NSTemporaryDirectory()) + if let contents = try? FileManager.default.contentsOfDirectory( + at: tmp, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) { + for url in contents { + try? FileManager.default.removeItem(at: url) + } + } + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + + /// One top-level sandbox directory: its path and total on-disk size. + private func dirEntry(_ url: URL, bytes: Int64) -> [String: Any] { + ["path": url.path, "bytes": bytes] + } + + private func scan() -> [String: Any] { + let fileManager = FileManager.default + // standardize every root so the walk below reports paths in the same + // style (symlinks resolved, e.g. /var vs /private/var) — otherwise the + // Dart side can't match a file back to the directory that contains it + // and the breakdown double-counts. + let dirs = [ + fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first, + fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first, + fileManager.urls(for: .documentDirectory, in: .userDomainMask).first, + URL(fileURLWithPath: NSTemporaryDirectory()), + ].compactMap { $0?.standardizedFileURL } + + var topFiles = [(path: String, bytes: Int64)]() + var total: Int64 = 0 + var dirEntries = [[String: Any]]() + for dir in dirs { + let result = walk(dir) + dirEntries.append(dirEntry(dir, bytes: result.bytes)) + total += result.bytes + topFiles.append(contentsOf: result.top) + } + topFiles.sort { $0.bytes > $1.bytes } + let files = topFiles.prefix(30).map { + ["path": $0.path, "bytes": $0.bytes] + } + return [ + "totalBytes": total, + "dirs": dirEntries, + "files": files, + ] + } + + /// One pass over [root]: total file bytes plus every file above + /// [StorageScanPlugin.topFileFloor]. + private func walk(_ root: URL) -> (bytes: Int64, top: [(path: String, bytes: Int64)]) { + let fileManager = FileManager.default + guard let enumerator = fileManager.enumerator( + at: root, + includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey], + options: [.skipsHiddenFiles] + ) else { return (0, []) } + var bytes: Int64 = 0 + var top = [(path: String, bytes: Int64)]() + var visited = 0 + for case let url as URL in enumerator { + visited += 1 + if visited > StorageScanPlugin.visitCap { break } + guard let values = try? url.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { + continue + } + if values.isDirectory == true { continue } + let fileBytes = Int64(values.fileSize ?? 0) + guard fileBytes > 0 else { continue } + bytes += fileBytes + if fileBytes >= StorageScanPlugin.topFileFloor { + top.append((url.standardizedFileURL.path, fileBytes)) + } + } + return (bytes, top) + } +} diff --git a/ios/Runner/eew.aiff b/ios/Runner/eew.aiff deleted file mode 100644 index 7c15daf46..000000000 Binary files a/ios/Runner/eew.aiff and /dev/null differ diff --git a/ios/Runner/eew_alert.aiff b/ios/Runner/eew_alert.aiff deleted file mode 100644 index 9346db8fc..000000000 Binary files a/ios/Runner/eew_alert.aiff and /dev/null differ diff --git a/ios/Runner/eq.aiff b/ios/Runner/eq.aiff deleted file mode 100644 index a1d634fbc..000000000 Binary files a/ios/Runner/eq.aiff and /dev/null differ diff --git a/ios/Runner/info.aiff b/ios/Runner/info.aiff deleted file mode 100644 index 94480f79b..000000000 Binary files a/ios/Runner/info.aiff and /dev/null differ diff --git a/ios/Runner/normal.aiff b/ios/Runner/normal.aiff deleted file mode 100644 index 4a9302583..000000000 Binary files a/ios/Runner/normal.aiff and /dev/null differ diff --git a/ios/Runner/rain.aiff b/ios/Runner/rain.aiff deleted file mode 100644 index 240c642b3..000000000 Binary files a/ios/Runner/rain.aiff and /dev/null differ diff --git a/ios/Runner/report.aiff b/ios/Runner/report.aiff deleted file mode 100644 index 5af228e09..000000000 Binary files a/ios/Runner/report.aiff and /dev/null differ diff --git a/ios/Runner/tsunami.aiff b/ios/Runner/tsunami.aiff deleted file mode 100644 index 04d3ecf30..000000000 Binary files a/ios/Runner/tsunami.aiff and /dev/null differ diff --git a/ios/Runner/warn.aiff b/ios/Runner/warn.aiff deleted file mode 100644 index 5c9e5477e..000000000 Binary files a/ios/Runner/warn.aiff and /dev/null differ diff --git a/ios/Runner/weather.aiff b/ios/Runner/weather.aiff deleted file mode 100644 index 8791e06dc..000000000 Binary files a/ios/Runner/weather.aiff and /dev/null differ diff --git a/lib/app/app.dart b/lib/app/app.dart index 86bc8a6bd..5c9a462e3 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -5,6 +5,7 @@ import 'package:dpip/core/di/shared_deps.dart'; import 'package:dpip/core/geo/device_location_reporter.dart'; import 'package:dpip/core/geo/location_monitor.dart'; import 'package:dpip/core/geo/location_service.dart'; +import 'package:dpip/core/logging/log.dart'; import 'package:dpip/core/notifications/notification_service.dart'; import 'package:dpip/core/notifications/notification_taps.dart'; import 'package:dpip/core/platform/background_location.dart'; @@ -119,7 +120,15 @@ class _AppServicesHostState extends State<_AppServicesHost> NotificationTaps.onTap = routeNotificationTap; widget.onboarding.addListener(_onOnboardingChanged); WidgetsBinding.instance.addPostFrameCallback((_) { - widget.realtimeService.startAll(); + Log.debug( + 'first frame rendered ${Log.sinceStart.elapsedMilliseconds} ms ' + 'after start', + ); + // Spread the first polls so the post-first-frame burst doesn't hammer + // the network and UI isolate at once (EEW leads; the rest follow). + widget.realtimeService.startAll( + stagger: const Duration(milliseconds: 250), + ); NotificationTaps.drainPending(); // Permissions belong to onboarding — only run the permission-dependent // setup once it's complete (immediately for returning users). diff --git a/lib/app/router/app_router.dart b/lib/app/router/app_router.dart index 70d77097b..a2de77a0a 100644 --- a/lib/app/router/app_router.dart +++ b/lib/app/router/app_router.dart @@ -13,6 +13,14 @@ import 'package:dpip/features/location/presentation/pages/region_select_page.dar import 'package:dpip/features/changelog/presentation/pages/changelog_page.dart'; import 'package:dpip/features/log/presentation/pages/log_page.dart'; import 'package:dpip/features/map/presentation/pages/map_page.dart'; +import 'package:dpip/features/data/presentation/pages/moon_page.dart'; +import 'package:dpip/features/data/presentation/pages/almanac_page.dart'; +import 'package:dpip/features/data/presentation/pages/planets_page.dart'; +import 'package:dpip/features/data/presentation/pages/sky_chart_page.dart'; +import 'package:dpip/features/data/presentation/pages/tide_page.dart'; +import 'package:dpip/features/data/presentation/pages/tonight_page.dart'; +import 'package:dpip/features/data/presentation/pages/sun_page.dart'; +import 'package:dpip/features/meshtastic/presentation/pages/meshtastic_page.dart'; import 'package:dpip/features/more/presentation/pages/more_page.dart'; import 'package:dpip/features/notification/presentation/pages/notify_page.dart'; import 'package:dpip/features/onboarding/presentation/pages/onboarding_page.dart'; @@ -108,6 +116,41 @@ final GoRouter appRouter = GoRouter( ), ), ), + GoRoute( + path: AppRoutes.moonPath, + name: AppRoutes.moon, + builder: (_, _) => const MoonPage(), + ), + GoRoute( + path: AppRoutes.sunPath, + name: AppRoutes.sun, + builder: (_, _) => const SunPage(), + ), + GoRoute( + path: AppRoutes.planetsPath, + name: AppRoutes.planets, + builder: (_, _) => const PlanetsPage(), + ), + GoRoute( + path: AppRoutes.tonightPath, + name: AppRoutes.tonight, + builder: (_, _) => const TonightPage(), + ), + GoRoute( + path: AppRoutes.almanacPath, + name: AppRoutes.almanac, + builder: (_, _) => const AlmanacPage(), + ), + GoRoute( + path: AppRoutes.skyChartPath, + name: AppRoutes.skyChart, + builder: (_, _) => const SkyChartPage(), + ), + GoRoute( + path: AppRoutes.tidePath, + name: AppRoutes.tide, + builder: (_, _) => const TidePage(), + ), ], ), ], @@ -126,6 +169,11 @@ final GoRouter appRouter = GoRouter( name: AppRoutes.log, builder: (_, _) => const LogPage(), ), + GoRoute( + path: AppRoutes.meshtasticPath, + name: AppRoutes.meshtastic, + builder: (_, _) => const MeshtasticPage(), + ), GoRoute( path: AppRoutes.changelogPath, name: AppRoutes.changelog, diff --git a/lib/app/router/notification_routes.dart b/lib/app/router/notification_routes.dart index dfa92db68..e41b723a4 100644 --- a/lib/app/router/notification_routes.dart +++ b/lib/app/router/notification_routes.dart @@ -5,14 +5,13 @@ import 'package:dpip/core/notifications/notification_tap.dart'; import 'package:dpip/shared/navigation/app_routes.dart'; /// The slice of the router a notification tap needs — [GoRouter.goNamed]. -typedef NotificationRouteNavigator = - void Function( - String name, { - Map pathParameters, - Map queryParameters, - String? fragment, - Object? extra, - }); +typedef NotificationRouteNavigator = void Function( + String name, { + Map pathParameters, + Map queryParameters, + String? fragment, + Object? extra, +}); /// Single owner of notification → destination, mirroring the legacy /// `notify.dart` tap table in one file: [NotificationTaps] carries the tap @@ -48,6 +47,7 @@ String routeForNotificationChannel(String? channelKey) { return switch (NotificationChannels.groupOf(channelKey)) { 'group_eew' => AppRoutes.eew, 'group_eq' => AppRoutes.earthquake, + 'group_mesh' => AppRoutes.meshtastic, 'group_info' || 'group_tsunami' || 'group_other' => AppRoutes.home, _ => _unmapped(channelKey), }; diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index e3b9415b7..5e353bf47 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -1,5 +1,8 @@ +import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart' show kReleaseMode; + import 'package:dpip/app/app.dart'; import 'package:dpip/core/di/core_providers.dart'; import 'package:dpip/core/di/shared_deps.dart'; @@ -11,8 +14,17 @@ import 'package:dpip/core/platform/background_location.dart'; import 'package:dpip/core/network/dio_client.dart'; import 'package:dpip/core/network/etag_cache_store.dart'; import 'package:dpip/core/network/network_usage_store.dart'; +import 'package:dpip/core/storage/app_storage_scan.dart'; import 'package:dpip/shared/map/map_tile_cache.dart'; import 'package:dpip/core/network/region_selection.dart'; +import 'package:dpip/core/meshtastic/data/dpip_mesh_gateway_impl.dart'; +import 'package:dpip/core/meshtastic/data/mesh_log_migration.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/meshtastic/data/meshtastic_client_impl.dart'; +import 'package:dpip/core/meshtastic/mesh_metrics_recorder.dart'; +import 'package:dpip/core/meshtastic/mesh_alerts.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; import 'package:dpip/core/notifications/notification_service.dart'; import 'package:dpip/core/realtime/app_time.dart'; import 'package:dpip/core/realtime/clock.dart'; @@ -37,6 +49,7 @@ import 'package:dpip/features/disaster_map/disaster_map_providers.dart'; import 'package:dpip/features/earthquake/earthquake_providers.dart'; import 'package:dpip/features/events/events_providers.dart'; import 'package:dpip/features/home/home_providers.dart'; +import 'package:dpip/features/meshtastic/meshtastic_providers.dart'; import 'package:dpip/features/notification/notification_providers.dart'; import 'package:dpip/features/sponsor/sponsor_providers.dart'; import 'package:dpip/features/typhoon/typhoon_providers.dart'; @@ -66,20 +79,17 @@ Future bootstrap() async { Log.installErrorHandlers(); Log.info('DPIP starting up'); - try { - // Hot-restart safe: the native Firebase app survives a Dart hot restart, so - // re-initializing then throws — only initialize when no default app exists. - if (Firebase.apps.isEmpty) { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); - } - Log.info('Firebase initialized'); - } catch (error, stackTrace) { - Log.handle(error, stackTrace, 'Firebase init failed (push unavailable)'); - } + // Kick off every independent resource load in parallel — Firebase, prefs, + // the SQLite cache, the town directory and package info never touch each + // other, so the serial chain would simply add their latencies. Each is + // awaited individually below so failures keep their per-resource handling. + unawaited(_initFirebase()); + final prefsFuture = SharedPreferences.getInstance(); + final cacheFuture = _openCache(); + final townDirectoryFuture = TownDirectory.load(); + final appVersionFuture = PackageInfo.fromPlatform().then((p) => p.version); - final prefs = Prefs(await SharedPreferences.getInstance()); + final prefs = Prefs(await prefsFuture); final regions = RegionSelection(prefs); final experimental = ExperimentalSettings(prefs); final onboarding = OnboardingStore(prefs); @@ -87,7 +97,7 @@ Future bootstrap() async { final theme = ThemeController(prefs); final defaultMapLayer = DefaultMapLayerController(prefs); final mapLayerOrder = MapLayerOrderController(prefs); - final cache = await _openCache(); + final cache = await cacheFuture; final dio = createDio(etagCache: cache?.etag, usage: cache?.usage); final apiClient = ApiClient(dio, regions); // MapLibre asks Dart for every ExpTech tile before it asks the network, so @@ -96,6 +106,18 @@ Future bootstrap() async { ? null : MapTileCache(cache.etag, usage: cache.usage); await mapTileCache?.install(); + // Turn off the OS-level disk HTTP cache (iOS NSURLCache) and drop its + // residue: every cached byte now lives in the app's own SQLite, so a second + // disk copy is pure overhead. Fire-and-forget: it never delays launch. + unawaited(const StorageScanner().configure()); + // Debug runs leave JIT kernel snapshots (main.dart.dill / .swap.dill) in + // tmp — a debug → release switch on a dev device would otherwise carry + // hundreds of MB of them around. tmp is scratch space, so wiping it on a + // release launch is always safe (release never has anything there of its + // own); the debug → release direction is the only one that matters. + if (kReleaseMode) { + unawaited(const StorageScanner().clearTmp()); + } // Calibrated clock: real SNTP (flutter_ntp, ExpTech primary / Apple backup) // anchored to a monotonic clock, exposed globally via `AppTime` and resynced @@ -110,19 +132,18 @@ Future bootstrap() async { serverClock.sync().ignore(); final realtimeService = RealtimeService(serverClock); - // Push: best-effort so a missing push environment never blocks launch. + // Push: best-effort and off the first frame — a missing push environment or + // slow FCM registration must never gate launch. The token is only consumed + // by device-location reports, which fire after GPS permission, so it lands + // long before it is needed. final notificationService = NotificationService(prefs); - try { - await notificationService.init(); - } catch (error, stackTrace) { - Log.handle(error, stackTrace, 'Notification init skipped'); - } + unawaited(_initNotifications(notificationService)); // Location: the township directory (centroids) backs Home region labels and // the nearest-centroid fallback; the boundary polygons back exact - // point-in-polygon GPS resolution and load in the background so they never - // delay launch (a fix before they land falls back to nearest-centroid). - final townDirectory = await TownDirectory.load(); + // point-in-polygon GPS resolution and decode in a background isolate (see + // `TownBoundaries.load`) so they never delay launch or the first frames. + final townDirectory = await townDirectoryFuture; final townBoundaries = TownBoundaries.load(); final regionStore = RegionStore(prefs); final locationService = LocationService( @@ -135,7 +156,7 @@ Future bootstrap() async { // after the first frame once GPS permission is granted; a null token (not yet // registered) simply skips — it self-heals on the next move. final locationApi = LocationApi(apiClient); - final appVersion = (await PackageInfo.fromPlatform()).version; + final appVersion = await appVersionFuture; final reportPlatform = Platform.isIOS ? 1 : 0; final deviceLocationReporter = DeviceLocationReporter( positions: () => locationService.positionStream(), @@ -167,6 +188,22 @@ Future bootstrap() async { regions: regionStore, ); + // The mesh link is app-wide, not page-owned: a radio stays attached (and + // keeps reconnecting) for the whole app session, because it is a reception + // path for disaster information. `start()` picks a saved radio back up. + final meshtastic = MeshtasticClientImpl(); + final meshLink = MeshLink(meshtastic, prefs); + final meshAlerts = MeshAlerts(meshtastic, prefs); + final meshNodes = MeshNodeStore(meshtastic, prefs); + // Mesh data gets its own database, deliberately **not** the HTTP cache one: + // that lives in the platform cache directory, which the OS may purge, and a + // conversation is the one thing here that cannot be fetched again. + final meshStore = await _openMeshStore(); + if (meshStore != null) { + await migrateLegacyMeshLog(prefs, meshStore); + unawaited(meshStore.prune()); + } + final deps = SharedDeps( prefs: prefs, apiClient: apiClient, @@ -187,13 +224,31 @@ Future bootstrap() async { theme: theme, defaultMapLayer: defaultMapLayer, mapLayerOrder: mapLayerOrder, + meshtastic: meshtastic, + meshLink: meshLink, + meshAlerts: meshAlerts, + meshNodes: meshNodes, + meshStore: meshStore, + meshGateway: DpipMeshGatewayImpl(meshtastic, () => meshLink.dpipChannel), etagCache: cache?.etag, networkUsage: cache?.usage, mapTileCache: mapTileCache, ); + // Reconnects to the saved radio, if there is one. Deliberately not awaited: + // BLE takes seconds and the first frame must not wait for it. + meshLink.start(); + // Local notifications for anything the mesh delivers while the user is + // elsewhere — raised by the app itself, so they work with no internet. + meshAlerts.start(); + meshNodes.start(); + if (meshStore != null) { + MeshMetricsRecorder(meshtastic, meshStore).start(); + } + // Each feature turns [deps] into its providers (and registers its realtime // channels). Adding a feature = one line here + its `*Providers` function. + Log.info('bootstrap ready in ${Log.sinceStart.elapsedMilliseconds} ms'); runApp( DpipApp( deps: deps, @@ -206,6 +261,7 @@ Future bootstrap() async { ...eventsProviders(deps), ...changelogProviders(deps), ...notificationProviders(deps), + ...meshtasticProviders(deps), ...sponsorProviders(), ...homeProviders(), ], @@ -240,3 +296,53 @@ Future<({EtagCacheStore etag, NetworkUsageStore usage})?> _openCache() async { return null; } } + +/// Opens the mesh database under the application-support directory. +/// +/// Support, not cache: the mesh log and its utilization history are user data +/// that no server can re-supply, and the cache directory is purgeable by the +/// OS. Best-effort like the cache — a failure means the conversation lives +/// only for this session rather than the app failing to launch. +Future _openMeshStore() async { + try { + final base = await getApplicationSupportDirectory(); + final db = await openDatabase( + '${base.path}/meshtastic.db', + version: 1, + onCreate: (db, _) => MeshStore.createSchema(db), + ); + // Also on open, so a database created by an older build picks up tables + // added later (every statement is `IF NOT EXISTS`). + await MeshStore.createSchema(db); + return MeshStore(db); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh database unavailable'); + return null; + } +} + +/// Initializes Firebase in parallel with the rest of bootstrap. Hot-restart +/// safe: the native Firebase app survives a Dart hot restart, so +/// re-initializing then throws — only initialize when no default app exists. +/// Best-effort: a failure is logged and the app still launches. +Future _initFirebase() async { + try { + if (Firebase.apps.isEmpty) { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } + Log.info('Firebase initialized'); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'Firebase init failed (push unavailable)'); + } +} + +/// Initializes push after the first frame — never gate launch on FCM. +Future _initNotifications(NotificationService service) async { + try { + await service.init(); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'Notification init skipped'); + } +} diff --git a/lib/core/astro/astro_time.dart b/lib/core/astro/astro_time.dart new file mode 100644 index 000000000..9894fdd63 --- /dev/null +++ b/lib/core/astro/astro_time.dart @@ -0,0 +1,96 @@ +/// The two clocks every ephemeris needs, and the angles derived from them. +/// +/// Astronomy runs on two time scales and mixing them is the classic way to be +/// quietly wrong all year: +/// +/// * **Terrestrial Time** is uniform, and is what the orbital series are +/// defined on. It runs about 75 s ahead of UTC this decade. +/// * **Universal Time** is the Earth's rotation angle, and is what sidereal +/// time — and therefore every hour angle, altitude and rise time — must be +/// built from. +/// +/// So a rise time takes the body's position from TT and the observer's +/// orientation from UT. Both live here, next to each other, so neither can be +/// reached for by accident. +library; + +import 'dart:math' as math; + +/// Radians per degree — the conversion every file in `astro/` needs. +const double degrees = math.pi / 180; + +/// Radians per arcminute. +const double arcminutes = degrees / 60; + +/// Julian Day of [utc] on the **UT** scale. +double julianDay(DateTime utc) => + utc.millisecondsSinceEpoch / Duration.millisecondsPerDay + 2440587.5; + +/// TT − UT in seconds: Espenak & Meeus's fit for 2005–2050, which is the span +/// this app plausibly runs over. +/// +/// It matters more than its size suggests. Left out entirely, the Moon's +/// longitude is 60″ wrong — four times the truncation error of the series it +/// feeds. The Earth's rotation is not predictable, so no model is exact; being +/// a few seconds off is irrelevant, being 75 s off is not. +double deltaTSeconds(DateTime utc) { + final years = utc.year + (utc.month - 0.5) / 12 - 2000; + return 62.92 + 0.32217 * years + 0.005589 * years * years; +} + +/// Julian centuries of **Terrestrial Time** from J2000.0 — the unit the +/// orbital series are written in. +double julianCenturies(DateTime utc) => + (julianDay(utc) + deltaTSeconds(utc) / Duration.secondsPerDay - 2451545.0) / + 36525.0; + +/// Mean obliquity of the ecliptic in radians (Meeus 21.2, linear term). +/// +/// Nutation adds under 0.003°, which is inside the truncation error of every +/// series in this package — carrying it would be precision theatre. +double meanObliquity(double centuries) => + (23.439291 - 0.0130042 * centuries) * degrees; + +/// Greenwich mean sidereal time at [utc], radians (Meeus 11.4). +/// +/// Built from UT on purpose — see the library doc. +double greenwichSiderealTime(DateTime utc) { + final sinceEpoch = julianDay(utc) - 2451545.0; + final centuries = sinceEpoch / 36525.0; + return turn( + (280.46061837 + + 360.98564736629 * sinceEpoch + + 0.000387933 * centuries * centuries) * + degrees, + ); +} + +/// Wraps an angle into `[0, 2π)`. +double turn(double radians) { + const full = 2 * math.pi; + return ((radians % full) + full) % full; +} + +/// Wraps an angle into `(-π, π]` — the signed form, so a correction can go +/// backwards instead of the long way round. +double signedTurn(double radians) { + final wrapped = turn(radians); + return wrapped > math.pi ? wrapped - 2 * math.pi : wrapped; +} + +/// Solves Kepler's equation `M = E − e sin E` for the eccentric anomaly. +/// +/// Newton–Raphson from `E₀ = M + e sin M`. For every orbit in the solar +/// system (e < 0.25 here) that converges to machine precision in a handful of +/// passes; the iteration cap is a backstop, not the expected exit. +double eccentricAnomaly(double meanAnomaly, double eccentricity) { + var e = meanAnomaly + eccentricity * math.sin(meanAnomaly); + for (var pass = 0; pass < 12; pass++) { + final delta = + (e - eccentricity * math.sin(e) - meanAnomaly) / + (1 - eccentricity * math.cos(e)); + e -= delta; + if (delta.abs() < 1e-12) break; + } + return e; +} diff --git a/lib/core/astro/deep_sky.dart b/lib/core/astro/deep_sky.dart new file mode 100644 index 000000000..ad9271669 --- /dev/null +++ b/lib/core/astro/deep_sky.dart @@ -0,0 +1,263 @@ +/// The Messier catalogue — the 110 objects a small telescope can actually +/// reach, and the standard target list for a night out. +/// +/// Positions are J2000, precessed to the equinox of date on use. That matters +/// less here than for the planets (a quarter of a degree over 25 years, on +/// objects tens of arcminutes across), but it is free, and it keeps every +/// coordinate in this package in one frame. +/// +/// Common names are kept in English. Translating 110 proper nouns into eleven +/// languages would be eleven sets of guesses that nobody could check, and the +/// catalogue designation — M31, M42 — is what observers actually use and is +/// language-neutral. The *type* is localised, because that is a description +/// rather than a name. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; + +/// What kind of object — the part of a catalogue entry worth translating. +enum DeepSkyType { + /// Open cluster. + oc, + + /// Globular cluster. + gc, + + /// Spiral galaxy. + s, + + /// Elliptical galaxy. + e, + + /// Irregular galaxy. + i, + + /// Planetary nebula. + pn, + + /// Supernova remnant. + snr, + + /// Star-forming region — a diffuse emission nebula. + sfr, + + /// Reflection nebula. + rn, + + /// A position only: an asterism or a patch of Milky Way, catalogued by + /// Messier but not a single object. + pos, +} + +/// One catalogue entry. +class DeepSkyObject { + const DeepSkyObject({ + required this.messier, + required this.designation, + required this.commonName, + required this.type, + required this.magnitude, + required this.rightAscensionJ2000, + required this.declinationJ2000, + }); + + /// The Messier number, 1-110. + final int messier; + + /// The NGC/IC designation. + final String designation; + + /// The English common name, or empty where there is none. + final String commonName; + + final DeepSkyType type; + + /// Integrated visual magnitude. Deceptive for extended objects — M31 is + /// magnitude 3.4 and still invisible from a city — so it ranks targets + /// rather than promising them. + final double magnitude; + + /// J2000 position, degrees. + final double rightAscensionJ2000; + final double declinationJ2000; + + /// `M31`. + String get label => 'M$messier'; + + /// The position at [utc], precessed from J2000 to the equinox of date. + Equatorial positionAt(DateTime utc) { + final t = julianCenturies(utc); + // Rigorous precession would rotate through three angles; over the decades + // this app spans, the first-order form is within an arcsecond and is what + // keeps this a table rather than a matrix library. + final ra = rightAscensionJ2000 * degrees; + final dec = declinationJ2000 * degrees; + const mSeconds = 3.07496; + const nSeconds = 1.33621; + const nArcsec = 20.0431; + final years = t * 100; + return Equatorial( + rightAscension: turn( + ra + + (mSeconds + nSeconds * math.sin(ra) * math.tan(dec)) * + years * + 15 / + 3600 * + degrees, + ), + declination: dec + nArcsec * math.cos(ra) * years / 3600 * degrees, + ); + } +} + +/// The catalogue, indexed by Messier number. +/// +/// Packed as `(number, designation, common name, type, magnitude, RA, Dec)`. +/// Source: the Messier list distributed with `d3-celestial`, itself derived +/// from the NGC/IC catalogues. +const List<(int, String, String, DeepSkyType, double, double, double)> +_messier = [ + (1, 'NGC 1952', 'Crab Nebula', DeepSkyType.snr, 8.4, 83.625, 22.0167), + (2, 'NGC 7089', '', DeepSkyType.gc, 6.5, 323.375, -0.8167), + (3, 'NGC 5272', '', DeepSkyType.gc, 6.4, 205.55, 28.3833), + (4, 'NGC 6121', '', DeepSkyType.gc, 5.9, 245.9, -26.5333), + (5, 'NGC 5904', '', DeepSkyType.gc, 5.8, 229.65, 2.0833), + (6, 'NGC 6405', 'Butterfly Cluster', DeepSkyType.oc, 4.2, 265.0249, -32.2167), + (7, 'NGC 6475', 'Ptolemy´s Cluster', DeepSkyType.oc, 3.3, 268.475, -34.8167), + (8, 'NGC 6523', 'Lagoon Nebula', DeepSkyType.sfr, 5.8, 270.95, -24.3833), + (9, 'NGC 6333', '', DeepSkyType.gc, 7.9, 259.8, -18.5167), + (10, 'NGC 6254', '', DeepSkyType.gc, 6.6, 254.2751, -4.1), + (11, 'NGC 6705', 'Wild Duck Cluster', DeepSkyType.oc, 5.8, 282.775, -6.2667), + (12, 'NGC 6218', '', DeepSkyType.gc, 6.6, 251.8001, -1.95), + ( + 13, + 'NGC 6205', + 'Great Hercules Cluster', + DeepSkyType.gc, + 5.9, + 250.425, + 36.4667, + ), + (14, 'NGC 6402', '', DeepSkyType.gc, 7.6, 264.4001, -3.25), + (15, 'NGC 7078', '', DeepSkyType.gc, 6.4, 322.5, 12.1667), + (16, 'NGC 6611', 'Eagle Nebula', DeepSkyType.sfr, 6, 274.7, -13.7833), + (17, 'NGC 6618', 'Omega Nebula', DeepSkyType.sfr, 7, 275.2, -16.1833), + (18, 'NGC 6613', '', DeepSkyType.oc, 6.9, 274.9751, -17.1333), + (19, 'NGC 6273', '', DeepSkyType.gc, 7.2, 255.65, -26.2667), + (20, 'NGC 6514', 'Trifid Nebula', DeepSkyType.sfr, 8.5, 270.6499, -23.0333), + (21, 'NGC 6531', '', DeepSkyType.oc, 5.9, 271.1501, -22.5), + (22, 'NGC 6656', '', DeepSkyType.gc, 5.1, 279.1001, -23.9), + (23, 'NGC 6494', '', DeepSkyType.oc, 5.5, 269.2001, -19.0167), + (24, '', 'Milky Way patch', DeepSkyType.pos, 4.5, 274.225, -18.4833), + (25, 'IC 4725', '', DeepSkyType.oc, 4.6, 277.9, -19.25), + (26, 'NGC 6694', '', DeepSkyType.oc, 8, 281.2999, -9.4), + (27, 'NGC 6853', 'Dumbbell Nebula', DeepSkyType.pn, 8.1, 299.8999, 22.7167), + (28, 'NGC 6626', '', DeepSkyType.gc, 6.9, 276.125, -24.8667), + (29, 'NGC 6913', '', DeepSkyType.oc, 6.6, 305.975, 38.5333), + (30, 'NGC 7099', '', DeepSkyType.gc, 7.5, 325.0999, -23.1833), + (31, 'NGC 224', 'Andromeda', DeepSkyType.s, 3.4, 10.6751, 41.2667), + (32, 'NGC 221', '', DeepSkyType.e, 8.2, 10.6751, 40.8667), + (33, 'NGC 598', 'Triangulum', DeepSkyType.s, 5.7, 23.475, 30.65), + (34, 'NGC 1039', '', DeepSkyType.oc, 5.2, 40.5, 42.7833), + (35, 'NGC 2168', '', DeepSkyType.oc, 5.1, 92.2249, 24.3333), + (36, 'NGC 1960', '', DeepSkyType.oc, 6, 84.0251, 34.1333), + (37, 'NGC 2099', '', DeepSkyType.oc, 5.6, 88.1, 32.55), + (38, 'NGC 1912', '', DeepSkyType.oc, 6.4, 82.175, 35.8333), + (39, 'NGC 7092', '', DeepSkyType.oc, 4.6, 323.0501, 48.4333), + (40, 'WN 4', '', DeepSkyType.pos, 8, 185.5999, 58.0833), + (41, 'NGC 2287', '', DeepSkyType.oc, 4.5, 101.75, -20.7333), + (42, 'NGC 1976', 'Orion Nebula', DeepSkyType.sfr, 4, 83.85, -5.45), + (43, 'NGC 1982', '', DeepSkyType.sfr, 9, 83.9, -5.2667), + (44, 'NGC 2632', 'Praesepe', DeepSkyType.oc, 3.1, 130.025, 19.9833), + (45, '', 'Pleiades', DeepSkyType.oc, 1.2, 56.75, 24.1167), + (46, 'NGC 2437', '', DeepSkyType.oc, 6.1, 115.4501, -14.8167), + (47, 'NGC 2422', '', DeepSkyType.oc, 4.4, 114.15, -14.5), + (48, 'NGC 2548', '', DeepSkyType.oc, 5.8, 123.45, -5.8), + (49, 'NGC 4472', '', DeepSkyType.e, 8.4, 187.4501, 8), + (50, 'NGC 2323', '', DeepSkyType.oc, 5.9, 105.8, -8.3333), + (51, 'NGC 5194/5', 'Whirlpool', DeepSkyType.s, 8.1, 202.4749, 47.2), + (52, 'NGC 7654', '', DeepSkyType.oc, 6.9, 351.05, 61.5833), + (53, 'NGC 5024', '', DeepSkyType.gc, 7.7, 198.225, 18.1667), + (54, 'NGC 6715', '', DeepSkyType.gc, 7.7, 283.7749, -30.4833), + (55, 'NGC 6809', '', DeepSkyType.gc, 7, 295, -30.9667), + (56, 'NGC 6779', '', DeepSkyType.gc, 8.2, 289.15, 30.1833), + (57, 'NGC 6720', 'Ring Nebula', DeepSkyType.pn, 9, 283.3999, 33.0333), + (58, 'NGC 4579', '', DeepSkyType.s, 9.8, 189.425, 11.8167), + (59, 'NGC 4621', '', DeepSkyType.e, 9.8, 190.5, 11.65), + (60, 'NGC 4649', '', DeepSkyType.e, 8.8, 190.925, 11.55), + (61, 'NGC 4303', '', DeepSkyType.s, 9.7, 185.475, 4.4667), + (62, 'NGC 6266', '', DeepSkyType.gc, 6.6, 255.3, -30.1167), + (63, 'NGC 5055', 'Sunflower Galaxy', DeepSkyType.s, 8.6, 198.95, 42.0333), + (64, 'NGC 4826', 'Blackeye Galaxy', DeepSkyType.s, 8.5, 194.175, 21.6833), + (65, 'NGC 3623', '', DeepSkyType.s, 9.3, 169.725, 13.0833), + (66, 'NGC 3627', '', DeepSkyType.s, 9, 170.0501, 12.9833), + (67, 'NGC 2682', '', DeepSkyType.oc, 6.9, 132.6, 11.8167), + (68, 'NGC 4590', '', DeepSkyType.gc, 8.2, 189.8749, -26.75), + (69, 'NGC 6637', '', DeepSkyType.gc, 7.7, 277.85, -32.35), + (70, 'NGC 6681', '', DeepSkyType.gc, 8.1, 280.8, -32.3), + (71, 'NGC 6838', '', DeepSkyType.gc, 8.3, 298.4501, 18.7833), + (72, 'NGC 6981', '', DeepSkyType.gc, 9.4, 313.3751, -12.5333), + (73, 'NGC 6994', '4 Star asterism', DeepSkyType.pos, 10, 314.7251, -12.6333), + (74, 'NGC 628', '', DeepSkyType.s, 9.2, 24.1751, 15.7833), + (75, 'NGC 6864', '', DeepSkyType.gc, 8.6, 301.525, -21.9167), + ( + 76, + 'NGC 650/1', + 'Little Dumbbell Nebula', + DeepSkyType.pn, + 11.5, + 25.6001, + 51.5667, + ), + (77, 'NGC 1068', 'Cetus A', DeepSkyType.s, 8.8, 40.6751, -0.0167), + (78, 'NGC 2068', '', DeepSkyType.rn, 8, 86.675, 0.05), + (79, 'NGC 1904', '', DeepSkyType.gc, 8, 81.125, -24.55), + (80, 'NGC 6093', '', DeepSkyType.gc, 7.2, 244.2499, -22.9833), + (81, 'NGC 3031', 'Bode´s Galaxy', DeepSkyType.s, 6.8, 148.8996, 69.0667), + (82, 'NGC 3034', 'Cigar Galaxy', DeepSkyType.i, 8.4, 148.9496, 69.6833), + (83, 'NGC 5236', 'Southern Pinwheel', DeepSkyType.s, 7.6, 204.25, -29.8667), + (84, 'NGC 4374', '', DeepSkyType.e, 9.3, 186.275, 12.8833), + (85, 'NGC 4382', '', DeepSkyType.e, 9.2, 186.35, 18.1833), + (86, 'NGC 4406', '', DeepSkyType.e, 9.2, 186.5501, 12.95), + (87, 'NGC 4486', 'Virgo A', DeepSkyType.e, 8.6, 187.7, 12.4), + (88, 'NGC 4501', '', DeepSkyType.s, 9.5, 187.9999, 14.4167), + (89, 'NGC 4552', '', DeepSkyType.e, 9.8, 188.925, 12.55), + (90, 'NGC 4569', '', DeepSkyType.s, 9.5, 189.2, 13.1667), + (91, 'NGC 4548', '', DeepSkyType.s, 10.2, 188.85, 14.5), + (92, 'NGC 6341', '', DeepSkyType.gc, 6.5, 259.275, 43.1333), + (93, 'NGC 2447', '', DeepSkyType.oc, 6.2, 116.15, -23.8667), + (94, 'NGC 4736', 'Cat’s Eye Galaxy', DeepSkyType.s, 8.1, 192.725, 41.1167), + (95, 'NGC 3351', '', DeepSkyType.s, 9.7, 161, 11.7), + (96, 'NGC 3368', '', DeepSkyType.s, 9.2, 161.7, 11.8167), + (97, 'NGC 3587', 'Owl Nebula', DeepSkyType.pn, 11.2, 168.7001, 55.0167), + (98, 'NGC 4192', '', DeepSkyType.s, 10.1, 183.45, 14.9), + (99, 'NGC 4254', '', DeepSkyType.s, 9.8, 184.7, 14.4167), + (100, 'NGC 4321', '', DeepSkyType.s, 9.4, 185.7251, 15.8167), + (101, 'NGC 5457', 'Pinwheel', DeepSkyType.s, 7.7, 210.8, 54.35), + (102, 'NGC 5866', 'Spindle', DeepSkyType.s, 9.9, 226.6226, 55.76), + (103, 'NGC 581', '', DeepSkyType.oc, 7.4, 23.3, 60.7), + (104, 'NGC 4594', 'Sombrero', DeepSkyType.s, 8.3, 190, -11.6167), + (105, 'NGC 3379', '', DeepSkyType.e, 9.3, 161.9501, 12.5833), + (106, 'NGC 4258', '', DeepSkyType.s, 8.3, 184.7501, 47.3), + (107, 'NGC 6171', '', DeepSkyType.gc, 8.1, 248.125, -13.05), + (108, 'NGC 3556', '', DeepSkyType.s, 10, 167.8751, 55.6667), + (109, 'NGC 3992', '', DeepSkyType.s, 9.8, 179.4, 53.3833), + (110, 'NGC 205', '', DeepSkyType.e, 8, 10.1, 41.6833), +]; + +/// The catalogue as objects. +final List messierCatalogue = [ + for (final (number, designation, name, type, magnitude, ra, dec) in _messier) + DeepSkyObject( + messier: number, + designation: designation, + commonName: name, + type: type, + magnitude: magnitude, + rightAscensionJ2000: ra, + declinationJ2000: dec, + ), +]; diff --git a/lib/core/astro/eclipse.dart b/lib/core/astro/eclipse.dart new file mode 100644 index 000000000..fb405708c --- /dev/null +++ b/lib/core/astro/eclipse.dart @@ -0,0 +1,359 @@ +/// Eclipses, found rather than tabulated. +/// +/// An eclipse is not a separate calculation. It is what the positions this +/// package already computes happen to do when the Sun, Earth and Moon line up, +/// so both kinds are found by walking the syzygies and measuring: +/// +/// * **Lunar** — at each full moon, how far the Moon's centre is from the +/// axis of the Earth's shadow. The shadow's umbra and penumbra have known +/// angular radii at the Moon's distance, so comparing that separation with +/// them gives the type and the magnitude. A lunar eclipse is the same +/// everywhere on the night side, so there are no local circumstances +/// beyond "is the Moon up". +/// * **Solar** — at each new moon, how far the Moon's centre is from the +/// Sun's *as seen from where you are standing*. This is why +/// `Observer.topocentric` exists: the Moon's parallax is nearly a degree, +/// twice its own diameter, so a solar eclipse is a local event and a +/// geocentric calculation would describe one nobody can see. Working +/// topocentrically gives real local magnitude and contact times without +/// Besselian elements. +/// +/// The shadow radii carry the standard 1.02 enlargement for the Earth's +/// atmosphere, which is what makes a predicted umbral magnitude agree with an +/// observed one. +/// +/// **Measured** against NASA's eclipse catalogue in `test/core/astro/`. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/moon_phase.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// The Sun's equatorial horizontal parallax at 1 au — 8.794″. +const double _solarParallax = 8.794 / 3600 * degrees; + +/// Enlargement of the Earth's shadow by its atmosphere (Danjon's 1/50 rule). +const double _atmosphere = 1.02; + +/// What kind of eclipse, if any. +enum EclipseKind { + none, + + /// The Moon in the Earth's outer shadow only — a subtle shading. + penumbral, + + /// Part of the Moon in the umbra, or part of the Sun covered. + partial, + + /// The Moon entirely in the umbra. + total, + + /// The Moon too far away to cover the Sun: a ring is left. + annular, +} + +/// One eclipse. +class Eclipse { + const Eclipse({ + required this.kind, + required this.peak, + required this.magnitude, + required this.isSolar, + this.penumbralMagnitude, + this.begins, + this.ends, + }); + + final EclipseKind kind; + + /// Greatest eclipse — least separation. + final DateTime peak; + + /// Fraction of the eclipsed body's diameter covered at [peak]. Over 1 means + /// total; for a solar eclipse it is the fraction of the Sun's diameter. + final double magnitude; + + /// For a lunar eclipse, how deep into the outer shadow it goes. A penumbral + /// eclipse has this without an umbral magnitude. + final double? penumbralMagnitude; + + final bool isSolar; + + /// First and last contact, where the eclipse is visible at all. + final DateTime? begins; + final DateTime? ends; + + bool get isVisible => kind != EclipseKind.none; +} + +/// Finding eclipses. +abstract final class Eclipses { + /// The next lunar eclipse after [utc], searching at most [withinDays]. + /// + /// Lunar eclipses are global: whoever has the Moon above the horizon sees + /// the same event at the same instant, so nothing here depends on a place. + static Eclipse? nextLunar(DateTime utc, {int withinDays = 400}) { + var full = MoonPhase.nextFullMoon(utc); + final limit = utc.add(Duration(days: withinDays)); + while (full.isBefore(limit)) { + final eclipse = lunarAt(full); + if (eclipse.isVisible) return eclipse; + full = MoonPhase.nextFullMoon(full.add(const Duration(days: 1))); + } + return null; + } + + /// The lunar eclipse, if any, at the full moon near [near]. + static Eclipse lunarAt(DateTime near) { + final peak = _minimise(near, _shadowSeparation); + final separation = _shadowSeparation(peak); + final moon = MoonEphemeris.at(peak); + final sun = SunEphemeris.at(peak); + + final moonSemidiameter = moon.angularDiameter / 2; + final sunSemidiameter = sun.angularDiameter / 2; + final solarParallax = + _solarParallax / (sun.distanceKm / astronomicalUnitKm); + // Meeus ch. 54: the shadow's angular radii at the Moon's distance. + final penumbra = + _atmosphere * (moon.parallax + solarParallax + sunSemidiameter); + final umbra = + _atmosphere * (moon.parallax + solarParallax - sunSemidiameter); + + final umbralMagnitude = + (umbra + moonSemidiameter - separation) / (2 * moonSemidiameter); + final penumbralMagnitude = + (penumbra + moonSemidiameter - separation) / (2 * moonSemidiameter); + + final kind = umbralMagnitude >= 1 + ? EclipseKind.total + : umbralMagnitude > 0 + ? EclipseKind.partial + : penumbralMagnitude > 0 + ? EclipseKind.penumbral + : EclipseKind.none; + + if (kind == EclipseKind.none) { + return Eclipse(kind: kind, peak: peak, magnitude: 0, isSolar: false); + } + + // Contacts: where the relevant magnitude passes through zero. + final threshold = kind == EclipseKind.penumbral ? penumbra : umbra; + double clearance(DateTime at) { + final m = MoonEphemeris.at(at); + return _shadowSeparation(at) - (threshold + m.angularDiameter / 2); + } + + return Eclipse( + kind: kind, + peak: peak, + magnitude: kind == EclipseKind.penumbral + ? penumbralMagnitude + : umbralMagnitude, + penumbralMagnitude: penumbralMagnitude, + isSolar: false, + begins: _crossBefore(peak, clearance), + ends: _crossAfter(peak, clearance), + ); + } + + /// The next solar eclipse after [utc] **visible from this place**. + /// + /// A solar eclipse elsewhere on Earth is not an event here, so this asks the + /// only question that matters to a reader: does the Moon cover any of the + /// Sun from where I am standing, and is the Sun up at the time? + static Eclipse? nextSolar( + DateTime utc, { + required double latitude, + required double longitude, + int withinDays = 1200, + }) { + var newMoon = MoonPhase.nextNewMoon(utc); + final limit = utc.add(Duration(days: withinDays)); + while (newMoon.isBefore(limit)) { + final eclipse = solarAt( + newMoon, + latitude: latitude, + longitude: longitude, + ); + if (eclipse.isVisible) return eclipse; + newMoon = MoonPhase.nextNewMoon(newMoon.add(const Duration(days: 1))); + } + return null; + } + + /// The solar eclipse, if any, seen from this place at the new moon near + /// [near]. + static Eclipse solarAt( + DateTime near, { + required double latitude, + required double longitude, + }) { + final observer = Observer(latitude: latitude, longitude: longitude); + + double separation(DateTime at) { + final moon = observer.topocentric(MoonEphemeris.at(at).equatorial, at); + final sun = SunEphemeris.at(at).equatorial; + return moon.separationFrom(sun); + } + + final peak = _minimise(near, separation); + final gap = separation(peak); + final moon = MoonEphemeris.at(peak); + final sun = SunEphemeris.at(peak); + // The Moon's apparent size grows with the parallax correction, since the + // observer is up to an Earth radius nearer to it than the centre is. + final moonRadius = + moon.angularDiameter / + 2 * + (moon.distanceKm / (moon.distanceKm - _observerShift(observer, peak))); + final sunRadius = sun.angularDiameter / 2; + + final magnitude = (sunRadius + moonRadius - gap) / (2 * sunRadius); + final kind = magnitude <= 0 + ? EclipseKind.none + : gap < (moonRadius - sunRadius).abs() + ? (moonRadius >= sunRadius ? EclipseKind.total : EclipseKind.annular) + : EclipseKind.partial; + + if (kind == EclipseKind.none) { + return Eclipse(kind: kind, peak: peak, magnitude: 0, isSolar: true); + } + + double clearance(DateTime at) { + final m = MoonEphemeris.at(at); + final s = SunEphemeris.at(at); + return separation(at) - (m.angularDiameter + s.angularDiameter) / 2; + } + + final begins = _crossBefore(peak, clearance); + final ends = _crossAfter(peak, clearance); + + // The geometry alone is not visibility. Standing on the night side of the + // Earth, the parallax correction still produces a small formal separation + // — an eclipse that is happening, just not to you. Requiring the Sun to be + // above the horizon at some point between the contacts is what turns the + // geometry into an answer, and it is the difference between listing the + // 2026 Iceland eclipse for Taiwan and correctly not listing it. + var sunIsUp = false; + final from = begins ?? peak; + final to = ends ?? peak; + final span = to.difference(from).inMinutes; + for (var minute = 0; minute <= span; minute += 5) { + final at = from.add(Duration(minutes: minute)); + if (observer.lookAt(SunEphemeris.at(at).equatorial, at).altitude > + sunHorizon) { + sunIsUp = true; + break; + } + } + if (!sunIsUp) { + return Eclipse( + kind: EclipseKind.none, + peak: peak, + magnitude: 0, + isSolar: true, + ); + } + + return Eclipse( + kind: kind, + peak: peak, + magnitude: magnitude, + isSolar: true, + begins: begins, + ends: ends, + ); + } + + /// Whether the Moon is above the horizon at [at] — the only local question a + /// lunar eclipse raises, since the event itself is the same everywhere. + static bool moonIsUp( + DateTime at, { + required double latitude, + required double longitude, + }) { + final moon = MoonEphemeris.at(at); + return Observer( + latitude: latitude, + longitude: longitude, + ).lookAt(moon.equatorial, at).altitude > + moon.horizonAltitude; + } + + /// How much nearer the observer is to the Moon than the Earth's centre is, + /// in kilometres — the reason the Moon looks fractionally larger overhead. + static double _observerShift(Observer observer, DateTime at) { + final moon = MoonEphemeris.at(at); + final horizontal = observer.lookAt(moon.equatorial, at); + return 6378.14 * math.sin(horizontal.altitude); + } + + /// Angular distance from the Moon's centre to the axis of the Earth's + /// shadow — the antisolar point. + static double _shadowSeparation(DateTime at) { + final moon = MoonEphemeris.at(at); + final sun = SunEphemeris.at(at); + final shadow = Equatorial.fromEcliptic( + longitude: sun.longitude + math.pi, + latitude: 0, + obliquity: meanObliquity(sun.centuries), + ); + return moon.equatorial.separationFrom(shadow); + } + + /// The instant near [seed] at which [of] is least — ternary search, because + /// the separation is smooth and single-minimum across a syzygy. + static DateTime _minimise(DateTime seed, double Function(DateTime) of) { + var low = seed.subtract(const Duration(hours: 12)); + var high = seed.add(const Duration(hours: 12)); + for (var i = 0; i < 40; i++) { + final third = high.difference(low).inMilliseconds ~/ 3; + final a = low.add(Duration(milliseconds: third)); + final b = high.subtract(Duration(milliseconds: third)); + if (of(a) < of(b)) { + high = b; + } else { + low = a; + } + } + return low.add( + Duration(milliseconds: high.difference(low).inMilliseconds ~/ 2), + ); + } + + /// Where [of] last crossed zero before [peak]. + static DateTime? _crossBefore(DateTime peak, double Function(DateTime) of) => + _bisect(peak.subtract(const Duration(hours: 6)), peak, of); + + /// Where [of] next crosses zero after [peak]. + static DateTime? _crossAfter(DateTime peak, double Function(DateTime) of) => + _bisect(peak.add(const Duration(hours: 6)), peak, of); + + /// Bisects between an instant where [of] is positive ([outside]) and one + /// where it is negative ([inside]). + static DateTime? _bisect( + DateTime outside, + DateTime inside, + double Function(DateTime) of, + ) { + if (of(outside) < 0 || of(inside) > 0) return null; + var lo = outside; + var hi = inside; + for (var i = 0; i < 30; i++) { + final mid = lo.add( + Duration(microseconds: hi.difference(lo).inMicroseconds ~/ 2), + ); + if (of(mid) > 0) { + lo = mid; + } else { + hi = mid; + } + } + return hi; + } +} diff --git a/lib/core/astro/lunisolar_calendar.dart b/lib/core/astro/lunisolar_calendar.dart new file mode 100644 index 000000000..3a221b42b --- /dev/null +++ b/lib/core/astro/lunisolar_calendar.dart @@ -0,0 +1,268 @@ +/// 農曆 — the Chinese lunisolar calendar, computed rather than tabulated. +/// +/// Most implementations ship a table of packed month lengths per year, copied +/// from somewhere, valid for a fixed span and unfalsifiable. This derives the +/// calendar from its actual rules, which are astronomical and which this +/// package already computes: +/// +/// 1. A lunar month begins on the **civil day containing the new moon**. +/// 2. **冬至 always falls in month 11.** That fixes the numbering. +/// 3. A 歲 runs from one month 11 to the next. If it holds 13 months, one is +/// a leap month: the **first month containing no 中氣** (a major solar +/// term, at a multiple of 30° of solar longitude). It repeats the number +/// of the month before it. +/// +/// Everything is evaluated in a fixed civil zone, because "the day containing +/// the new moon" depends on where the day boundary is. Taiwan and China both +/// use UTC+8, which is why the same rules give the same calendar in both. +/// +/// **Measured** against the CWA's 中華民國115年日曆資料表 in +/// `test/core/astro/` — every day of 2026, not a sample. +library; + +import 'package:dpip/core/astro/moon_phase.dart'; +import 'package:dpip/core/astro/solar_terms.dart'; + +/// The civil zone the calendar is defined in. +const Duration lunisolarZone = Duration(hours: 8); + +/// The ten heavenly stems, 天干. +const List heavenlyStems = [ + '甲', + '乙', + '丙', + '丁', + '戊', + '己', + '庚', + '辛', + '壬', + '癸', +]; + +/// The twelve earthly branches, 地支. +const List earthlyBranches = [ + '子', + '丑', + '寅', + '卯', + '辰', + '巳', + '午', + '未', + '申', + '酉', + '戌', + '亥', +]; + +/// One date in the lunisolar calendar. +class LunisolarDate { + const LunisolarDate({ + required this.year, + required this.month, + required this.day, + required this.isLeapMonth, + required this.monthLength, + }); + + /// The lunar year — the Gregorian year the lunar new year fell in, which is + /// what 歲次 names. + final int year; + + /// 1…12. A leap month repeats the previous month's number. + final int month; + + /// 1…30. + final int day; + + /// Whether this is the 閏 (intercalary) month. + final bool isLeapMonth; + + /// 29 (小月) or 30 (大月) days. + final int monthLength; + + /// Sexagenary index of the year, 0…59. + int get sexagenaryIndex => ((year - 4) % 60 + 60) % 60; + + /// 歲次, e.g. `丙午`. + String get sexagenaryYear => + heavenlyStems[sexagenaryIndex % 10] + + earthlyBranches[sexagenaryIndex % 12]; + + /// The zodiac animal's branch index, 0 = 鼠 … 11 = 豬. + int get zodiacIndex => sexagenaryIndex % 12; + + /// Whether this is 正月初一 — lunar new year. + bool get isNewYearDay => month == 1 && day == 1 && !isLeapMonth; +} + +/// Deriving the lunisolar calendar. +abstract final class LunisolarCalendar { + /// The lunisolar date of the civil day containing [utc]. + static LunisolarDate of(DateTime utc) { + final day = _civilDay(utc); + final suiMonths = _suiContaining(day); + for (var i = 0; i < suiMonths.length; i++) { + final month = suiMonths[i]; + final next = i + 1 < suiMonths.length + ? suiMonths[i + 1].start + : month.start.add(const Duration(days: 31)); + if (!day.isBefore(month.start) && day.isBefore(next)) { + return LunisolarDate( + year: _lunarYearOf(month, suiMonths), + month: month.number, + day: day.difference(month.start).inDays + 1, + isLeapMonth: month.isLeap, + monthLength: next.difference(month.start).inDays, + ); + } + } + // Unreachable: the 歲 by construction spans the day it was built around. + throw StateError('no lunar month contains $day'); + } + + /// The civil (UTC+8) midnight beginning the day that contains [utc], as a + /// UTC instant. + static DateTime _civilDay(DateTime utc) { + final local = utc.add(lunisolarZone); + return DateTime.utc( + local.year, + local.month, + local.day, + ).subtract(lunisolarZone); + } + + /// The new moon at or before [utc], as the civil day it falls on. + static DateTime _newMoonDayAtOrBefore(DateTime utc) { + // Step back far enough to be certain of landing before it, then walk + // forward one lunation at a time. + var candidate = MoonPhase.nextNewMoon( + utc.subtract(const Duration(days: 40)), + ); + var day = _civilDay(candidate); + while (true) { + final nextMoon = MoonPhase.nextNewMoon(candidate); + final nextDay = _civilDay(nextMoon); + if (nextDay.isAfter(utc)) return day; + candidate = nextMoon; + day = nextDay; + } + } + + /// The winter solstice at or before [utc]. + static DateTime _solsticeAtOrBefore(DateTime utc) { + var candidate = SolarTerms.next( + utc.subtract(const Duration(days: 400)), + SolarTerm.winterSolstice, + ); + while (true) { + final next = SolarTerms.next(candidate, SolarTerm.winterSolstice); + if (next.isAfter(utc)) return candidate; + candidate = next; + } + } + + /// The months of the 歲 containing [day], numbered and leap-marked. + static List<_Month> _suiContaining(DateTime day) { + // A 歲 opens with month 11, which begins on the new-moon day at or before + // its winter solstice — and that is up to a month *before* the solstice + // itself. So the solstice at or before the day does not always identify + // the right 歲: in the weeks between a month 11 starting and its solstice + // arriving, the day already belongs to the next one. Both directions are + // resolved here rather than left to the caller to notice. + var solstice = _solsticeAtOrBefore(day.add(const Duration(days: 1))); + var start = _newMoonDayAtOrBefore(solstice); + while (day.isBefore(start)) { + solstice = _solsticeAtOrBefore( + solstice.subtract(const Duration(days: 1)), + ); + start = _newMoonDayAtOrBefore(solstice); + } + while (true) { + final following = SolarTerms.next(solstice, SolarTerm.winterSolstice); + final followingStart = _newMoonDayAtOrBefore(following); + if (day.isBefore(followingStart)) break; + solstice = following; + start = followingStart; + } + final nextSolstice = SolarTerms.next(solstice, SolarTerm.winterSolstice); + final end = _newMoonDayAtOrBefore(nextSolstice); + + // Every new-moon day from this month 11 up to (and including) the next. + final starts = [start]; + var moon = MoonPhase.nextNewMoon(start.add(const Duration(days: 1))); + while (true) { + final moonDay = _civilDay(moon); + starts.add(moonDay); + if (!moonDay.isBefore(end)) break; + moon = MoonPhase.nextNewMoon(moon.add(const Duration(days: 1))); + } + + // The 歲 is the months from the first up to but not including the last — + // the last is month 11 of the following 歲. + final span = starts.length - 1; + final leapIndex = span == 13 ? _leapIndex(starts) : -1; + + final months = <_Month>[]; + var number = 11; + for (var i = 0; i < span; i++) { + final isLeap = i == leapIndex; + if (!isLeap && i > 0) number = number % 12 + 1; + months.add(_Month(start: starts[i], number: number, isLeap: isLeap)); + } + // Keep the following month 11 so the caller can measure the last month's + // length without recomputing the next 歲. + months.add(_Month(start: starts[span], number: 11, isLeap: false)); + return months; + } + + /// The first month of a thirteen-month 歲 that contains no 中氣. + /// + /// Month 11 itself always contains 冬至, so the search starts after it. If + /// no month qualifies — which the rules make impossible but floating point + /// does not — the last candidate is taken, so the calendar stays consistent + /// rather than throwing. + static int _leapIndex(List starts) { + for (var i = 1; i < starts.length - 1; i++) { + if (!_containsMajorTerm(starts[i], starts[i + 1])) return i; + } + return starts.length - 2; + } + + /// Whether a 中氣 falls in `[from, to)`. + static bool _containsMajorTerm(DateTime from, DateTime to) { + for (final term in SolarTerm.values) { + if (!term.isMajor) continue; + final at = SolarTerms.next(from.subtract(const Duration(days: 1)), term); + if (!at.isBefore(from) && at.isBefore(to)) return true; + } + return false; + } + + /// The lunar year a month belongs to: the Gregorian year in which that + /// year's 正月初一 fell. + static int _lunarYearOf(_Month month, List<_Month> sui) { + // Months 11 and 12 of a 歲 belong to the year that began earlier; months + // 1 onward belong to the year beginning with this 歲's month 1. + final firstMonth = sui.firstWhere( + (m) => m.number == 1 && !m.isLeap, + orElse: () => sui.first, + ); + final newYear = firstMonth.start.add(lunisolarZone).year; + return month.start.isBefore(firstMonth.start) ? newYear - 1 : newYear; + } +} + +class _Month { + const _Month({ + required this.start, + required this.number, + required this.isLeap, + }); + + /// UTC instant of the civil midnight the month begins on. + final DateTime start; + final int number; + final bool isLeap; +} diff --git a/lib/core/astro/meteor_showers.dart b/lib/core/astro/meteor_showers.dart new file mode 100644 index 000000000..aad442f9e --- /dev/null +++ b/lib/core/astro/meteor_showers.dart @@ -0,0 +1,327 @@ +/// The major meteor showers, and whether tonight is worth staying up for. +/// +/// The table is tiny — a dozen radiants and peak dates — but the useful part +/// is not the table. It is the two things that decide whether a shower is +/// worth watching from *here*, both of which this package already computes: +/// +/// * **Radiant altitude.** Rates fall roughly as the sine of the radiant's +/// height. A shower whose radiant never rises above the horizon at your +/// latitude produces nothing, however famous it is. +/// * **Moonlight.** A full moon at the peak wipes out all but the brightest +/// meteors. This is the single most common reason a "great shower" turns +/// out to be a disappointment, and it is knowable months ahead. +/// +/// So a shower gets a computed condition rather than a ZHR number that implies +/// a promise. ZHR is a *zenithal* rate under a dark sky — it is the ceiling, +/// almost never the observation. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/moon_rise_set.dart'; +import 'package:dpip/core/astro/sky_position.dart'; + +/// One shower's fixed properties. +class MeteorShower { + const MeteorShower({ + required this.id, + required this.peakMonth, + required this.peakDay, + required this.startMonth, + required this.startDay, + required this.endMonth, + required this.endDay, + required this.rightAscensionJ2000, + required this.declinationJ2000, + required this.zenithalRate, + required this.velocityKmS, + }); + + /// A stable key for localisation — never shown raw. + final String id; + + /// Peak date. Showers drift by a day or so between years; the radiant and + /// the date are conventional, not computed. + final int peakMonth; + final int peakDay; + + /// The span over which the shower is active at all. + final int startMonth; + final int startDay; + final int endMonth; + final int endDay; + + /// Radiant, J2000 degrees. + final double rightAscensionJ2000; + final double declinationJ2000; + + /// Zenithal hourly rate at maximum, under a dark sky with the radiant + /// overhead — a ceiling, not a forecast. + final int zenithalRate; + + /// Entry speed, km/s. Fast showers give brighter, shorter trails. + final double velocityKmS; + + /// The radiant's position, precessed to the equinox of date. + Equatorial radiantAt(DateTime utc) { + final t = julianCenturies(utc) * 100; + final ra = rightAscensionJ2000 * degrees; + final dec = declinationJ2000 * degrees; + return Equatorial( + rightAscension: turn( + ra + + (3.07496 + 1.33621 * math.sin(ra) * math.tan(dec)) * + t * + 15 / + 3600 * + degrees, + ), + declination: dec + 20.0431 * math.cos(ra) * t / 3600 * degrees, + ); + } + + /// The peak instant in [year], as a UTC time. Conventional to the day; the + /// hour is taken as local midnight, when radiants are typically highest. + DateTime peakOf(int year, {Duration zone = const Duration(hours: 8)}) => + DateTime.utc(year, peakMonth, peakDay).subtract(zone); +} + +/// How good a shower will actually be from one place. +class ShowerConditions { + const ShowerConditions({ + required this.shower, + required this.peak, + required this.bestTime, + required this.bestAltitude, + required this.moonIllumination, + required this.moonIsUp, + }); + + final MeteorShower shower; + final DateTime peak; + + /// When the radiant is highest during the dark hours around the peak. + final DateTime bestTime; + + /// The radiant's altitude then, radians. Negative means it never rises. + final double bestAltitude; + + /// The Moon's illuminated fraction at the peak. + final double moonIllumination; + + /// Whether the Moon is above the horizon at [bestTime]. + final bool moonIsUp; + + /// An estimate of the rate actually visible, meteors per hour. + /// + /// The ZHR scaled by the sine of the radiant's altitude, then cut by + /// moonlight. Both factors are approximations of a messy reality — this is + /// an expectation, not a measurement, and it is deliberately pessimistic + /// about the Moon because that is the way disappointment runs. + double get visibleRate { + if (bestAltitude <= 0) return 0; + final geometry = math.sin(bestAltitude); + final moon = moonIsUp ? 1 - 0.8 * moonIllumination : 1.0; + return shower.zenithalRate * geometry * moon; + } + + /// Better than a third of the theoretical rate, and the radiant well up. + bool get isFavourable => visibleRate >= shower.zenithalRate / 3; +} + +/// The showers worth listing, by peak date through the year. +/// +/// Rates and radiants follow the IMO's working list of visual showers. +const List meteorShowers = [ + MeteorShower( + id: 'quadrantids', + peakMonth: 1, + peakDay: 3, + startMonth: 12, + startDay: 28, + endMonth: 1, + endDay: 12, + rightAscensionJ2000: 230.0, + declinationJ2000: 49.0, + zenithalRate: 110, + velocityKmS: 41, + ), + MeteorShower( + id: 'lyrids', + peakMonth: 4, + peakDay: 22, + startMonth: 4, + startDay: 14, + endMonth: 4, + endDay: 30, + rightAscensionJ2000: 271.0, + declinationJ2000: 34.0, + zenithalRate: 18, + velocityKmS: 49, + ), + MeteorShower( + id: 'etaAquariids', + peakMonth: 5, + peakDay: 6, + startMonth: 4, + startDay: 19, + endMonth: 5, + endDay: 28, + rightAscensionJ2000: 338.0, + declinationJ2000: -1.0, + zenithalRate: 50, + velocityKmS: 66, + ), + MeteorShower( + id: 'deltaAquariids', + peakMonth: 7, + peakDay: 30, + startMonth: 7, + startDay: 12, + endMonth: 8, + endDay: 23, + rightAscensionJ2000: 340.0, + declinationJ2000: -16.0, + zenithalRate: 25, + velocityKmS: 41, + ), + MeteorShower( + id: 'perseids', + peakMonth: 8, + peakDay: 12, + startMonth: 7, + startDay: 17, + endMonth: 8, + endDay: 24, + rightAscensionJ2000: 48.0, + declinationJ2000: 58.0, + zenithalRate: 100, + velocityKmS: 59, + ), + MeteorShower( + id: 'orionids', + peakMonth: 10, + peakDay: 21, + startMonth: 10, + startDay: 2, + endMonth: 11, + endDay: 7, + rightAscensionJ2000: 95.0, + declinationJ2000: 16.0, + zenithalRate: 20, + velocityKmS: 66, + ), + MeteorShower( + id: 'southernTaurids', + peakMonth: 11, + peakDay: 5, + startMonth: 9, + startDay: 10, + endMonth: 11, + endDay: 20, + rightAscensionJ2000: 52.0, + declinationJ2000: 15.0, + zenithalRate: 5, + velocityKmS: 27, + ), + MeteorShower( + id: 'leonids', + peakMonth: 11, + peakDay: 17, + startMonth: 11, + startDay: 6, + endMonth: 11, + endDay: 30, + rightAscensionJ2000: 152.0, + declinationJ2000: 22.0, + zenithalRate: 15, + velocityKmS: 71, + ), + MeteorShower( + id: 'geminids', + peakMonth: 12, + peakDay: 14, + startMonth: 12, + startDay: 4, + endMonth: 12, + endDay: 20, + rightAscensionJ2000: 112.0, + declinationJ2000: 33.0, + zenithalRate: 150, + velocityKmS: 35, + ), + MeteorShower( + id: 'ursids', + peakMonth: 12, + peakDay: 22, + startMonth: 12, + startDay: 17, + endMonth: 12, + endDay: 26, + rightAscensionJ2000: 217.0, + declinationJ2000: 76.0, + zenithalRate: 10, + velocityKmS: 33, + ), +]; + +/// Working out how a shower will go. +abstract final class MeteorShowerConditions { + /// Conditions for [shower] at its peak in [year], from this place. + /// + /// The best moment is found by scanning the night around the peak rather + /// than assumed to be midnight: a radiant that rises at 02:00 is best just + /// before dawn, and saying "midnight" would be wrong by hours. + static ShowerConditions of( + MeteorShower shower, + int year, { + required double latitude, + required double longitude, + Duration zone = const Duration(hours: 8), + }) { + final peak = shower.peakOf(year, zone: zone); + final observer = Observer(latitude: latitude, longitude: longitude); + + var bestTime = peak; + var bestAltitude = -math.pi; + // The night either side of local midnight at the peak. + for (var minutes = -8 * 60; minutes <= 8 * 60; minutes += 15) { + final at = peak.add(Duration(minutes: minutes)); + final altitude = observer.lookAt(shower.radiantAt(at), at).altitude; + if (altitude > bestAltitude) { + bestAltitude = altitude; + bestTime = at; + } + } + + return ShowerConditions( + shower: shower, + peak: peak, + bestTime: bestTime, + bestAltitude: bestAltitude, + moonIllumination: MoonEphemeris.at(peak).illuminated, + moonIsUp: MoonRiseSet.aboveHorizon( + bestTime, + latitude: latitude, + longitude: longitude, + ), + ); + } + + /// Showers active on [utc], soonest peak first. + static List activeOn(DateTime utc) { + final month = utc.month; + final day = utc.day; + bool covers(MeteorShower s) { + final from = s.startMonth * 100 + s.startDay; + final to = s.endMonth * 100 + s.endDay; + final now = month * 100 + day; + // A shower can straddle the new year, so the window may wrap. + return from <= to ? now >= from && now <= to : now >= from || now <= to; + } + + return meteorShowers.where(covers).toList(); + } +} diff --git a/lib/core/astro/moon_ephemeris.dart b/lib/core/astro/moon_ephemeris.dart new file mode 100644 index 000000000..a1bb7f006 --- /dev/null +++ b/lib/core/astro/moon_ephemeris.dart @@ -0,0 +1,261 @@ +/// Where the Moon is — one series, evaluated once, that everything else reads. +/// +/// This is the astronomy layer's foundation. The phase, the distance readout, +/// the libration of the rendered globe and the rise/set times are not four +/// calculations: they are four *views* of one position. Deriving them from one +/// evaluation here, instead of from a hand-truncated series each, is what keeps +/// them consistent — a page that says "full moon" while the rise time disagrees +/// is the failure mode this removes. +/// +/// The series is Meeus, *Astronomical Algorithms*, table 45.A/45.B (47.A/47.B +/// in the 2nd ed.) — the ELP-2000/82 truncation, kept to the terms that matter +/// at display resolution. Everything is closed form: no ephemeris file, no +/// network. That is deliberate for a disaster-preparedness app, where "works +/// with no service" outranks the last arcsecond. +/// +/// **Measured**, not asserted. Against JPL Horizons over 2024–2027 (1385 +/// samples, `tool/` harness): longitude within 37″, latitude within 32″, +/// distance within 59 km (0.015%). Meeus's own worked example 45.a is pinned +/// as a test. Distance is at the floor of this truncation — the remaining +/// error is the mean arguments' dropped quadratic terms, not the table. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// Earth's equatorial radius, km — turns distance into horizontal parallax. +const double _earthRadiusKm = 6378.14; + +/// The Moon's mean radius, km — turns distance into apparent size. +const double _moonRadiusKm = 1737.4; + +/// The Moon's geocentric position at one instant. +class MoonEphemeris { + const MoonEphemeris({ + required this.longitude, + required this.latitude, + required this.distanceKm, + required this.sunLongitude, + required this.argumentOfLatitude, + required this.centuries, + }); + + /// Geocentric ecliptic longitude λ of date, radians. + final double longitude; + + /// Ecliptic latitude β, radians — ±5.3°, the tilt of the lunar orbit. + final double latitude; + + /// Centre-to-centre Earth–Moon distance Δ, kilometres. + final double distanceKm; + + /// The Sun's apparent ecliptic longitude, radians. Carried here because + /// every phase quantity is a difference against it, and recomputing it + /// elsewhere is exactly how two readouts start to disagree. + final double sunLongitude; + + /// Mean argument of latitude F, radians — the libration needs it, and it is + /// free here because the series already computed it. + final double argumentOfLatitude; + + /// Julian centuries of Terrestrial Time from J2000.0 — the series' own unit. + final double centuries; + + /// The Moon's position at [utc]. + factory MoonEphemeris.at(DateTime utc) { + final t = julianCenturies(utc); + + // Mean arguments (Meeus 45.1–45.5), degrees. Their quadratic and higher + // terms are dropped: over 1900–2100 they move λ by well under the + // truncation error of the table below. + final lp = 218.3164477 + 481267.88123421 * t; // mean longitude L' + final d = 297.8501921 + 445267.1114034 * t; // elongation D + final m = 357.5291092 + 35999.0502909 * t; // sun's anomaly M + final mp = 134.9633964 + 477198.8675055 * t; // moon's anomaly M' + final f = 93.2720950 + 483202.0175233 * t; // argument of latitude F + + // The Earth's orbit is slowly circularising, which detunes every term that + // depends on the Sun's anomaly (Meeus 45.6). + final e = 1 - 0.002516 * t - 0.0000074 * t * t; + + var sumL = 0.0; // 1e-6 degrees + var sumR = 0.0; // 1e-3 km + for (var i = 0; i < _longitudeAndRadius.length; i += 6) { + final row = _longitudeAndRadius; + final arg = + (row[i] * d + row[i + 1] * m + row[i + 2] * mp + row[i + 3] * f) * + degrees; + final scale = _eccentricityPower(e, row[i + 1]); + sumL += row[i + 4] * scale * math.sin(arg); + sumR += row[i + 5] * scale * math.cos(arg); + } + + var sumB = 0.0; // 1e-6 degrees + for (var i = 0; i < _latitude.length; i += 5) { + final row = _latitude; + final arg = + (row[i] * d + row[i + 1] * m + row[i + 2] * mp + row[i + 3] * f) * + degrees; + sumB += row[i + 4] * _eccentricityPower(e, row[i + 1]) * math.sin(arg); + } + + // Venus, Jupiter and the Earth's flattening, folded into three fictitious + // arguments (Meeus, p. 308). Small, but A1 alone is 0.004° — bigger than + // everything the table truncation drops. + final a1 = (119.75 + 131.849 * t) * degrees; + final a2 = (53.09 + 479264.290 * t) * degrees; + final a3 = (313.45 + 481266.484 * t) * degrees; + sumL += + 3958 * math.sin(a1) + + 1962 * math.sin((lp - f) * degrees) + + 318 * math.sin(a2); + sumB += + -2235 * math.sin(lp * degrees) + + 382 * math.sin(a3) + + 175 * math.sin(a1 - f * degrees) + + 175 * math.sin(a1 + f * degrees) + + 127 * math.sin((lp - mp) * degrees) - + 115 * math.sin((lp + mp) * degrees); + + return MoonEphemeris( + longitude: turn((lp + sumL / 1e6) * degrees), + latitude: sumB / 1e6 * degrees, + distanceKm: 385000.56 + sumR / 1000, + sunLongitude: SunEphemeris.atCenturies(t).longitude, + argumentOfLatitude: turn(f * degrees), + centuries: t, + ); + } + + /// Sun–Moon elongation, radians: 0 = new, π/2 = first quarter, π = full, + /// 3π/2 = last quarter. The phase *is* this difference of two longitudes — + /// not a series of its own. + double get phaseAngle => turn(longitude - sunLongitude); + + /// Illuminated fraction of the disc, 0 (new) … 1 (full). + /// + /// Strictly the elongation, not the Sun–Moon–Earth phase angle; they differ + /// by at most Δ/R ≈ 0.15°, which moves the fraction by under 0.2% — a fifth + /// of the last digit the page shows. + double get illuminated => (1 - math.cos(phaseAngle)) / 2; + + /// Equatorial coordinates of date — the frame rise, set and pointing need. + Equatorial get equatorial => Equatorial.fromEcliptic( + longitude: longitude, + latitude: latitude, + obliquity: meanObliquity(centuries), + distanceKm: distanceKm, + ); + + /// The altitude at which this Moon counts as rising or setting, radians. + /// + /// Not zero, and not a constant. Refraction lifts the limb 34′, while the + /// parallax — nearly a degree, and varying 14% with distance — converts the + /// geocentric position this series gives into the one an observer on the + /// surface actually sees (Meeus ch. 14). + double get horizonAltitude => horizonAltitudeFor(distanceKm); + + /// The same, from a distance alone — the form the rise/set solver uses, so + /// it need not re-evaluate the whole series to ask about the horizon. + static double horizonAltitudeFor(double distanceKm) => + 0.7275 * math.asin(_earthRadiusKm / distanceKm) - horizonRefraction; + + /// Equatorial horizontal parallax, radians — how far the Moon's apparent + /// place shifts between the Earth's centre and a point on its surface. + /// Nearly a degree, which is why a rise time computed without it is minutes + /// wrong. + double get parallax => math.asin(_earthRadiusKm / distanceKm); + + /// Apparent angular diameter, radians — 29.4′ at apogee, 33.5′ at perigee. + double get angularDiameter => 2 * math.asin(_moonRadiusKm / distanceKm); + + /// `e` raised to |[solarAnomaly]| — the correction applies once per power of + /// M in the argument. + static double _eccentricityPower(double e, double solarAnomaly) => + switch (solarAnomaly.abs()) { + 0 => 1.0, + 1 => e, + _ => e * e, + }; +} + +/// Periodic terms for longitude and distance — Meeus table 45.A, first 35 rows. +/// +/// Packed six to a row as `D, M, M', F, Σl (1e-6°), Σr (1e-3 km)`: a flat list +/// of numbers, because that is what a table is. Written out as 35 lines of +/// `+ 6288.774 * sin(mp)` it would be the same data with 35 more chances to +/// mistype it — which is how the three separate hand-written series this +/// replaces came to disagree with each other. +/// +/// The cut is where accuracy stops improving: rows 36–60 buy 25″ of longitude +/// and 12 km of distance, both far below what the page renders. +const List _longitudeAndRadius = [ + 0, 0, 1, 0, 6288774, -20905355, // + 2, 0, -1, 0, 1274027, -3699111, // + 2, 0, 0, 0, 658314, -2955968, // + 0, 0, 2, 0, 213618, -569925, // + 0, 1, 0, 0, -185116, 48888, // + 0, 0, 0, 2, -114332, -3149, // + 2, 0, -2, 0, 58793, 246158, // + 2, -1, -1, 0, 57066, -152138, // + 2, 0, 1, 0, 53322, -170733, // + 2, -1, 0, 0, 45758, -204586, // + 0, 1, -1, 0, -40923, -129620, // + 1, 0, 0, 0, -34720, 108743, // + 0, 1, 1, 0, -30383, 104755, // + 2, 0, 0, -2, 15327, 10321, // + 0, 0, 1, 2, -12528, 0, // + 0, 0, 1, -2, 10980, 79661, // + 4, 0, -1, 0, 10675, -34782, // + 0, 0, 3, 0, 10034, -23210, // + 4, 0, -2, 0, 8548, -21636, // + 2, 1, -1, 0, -7888, 24208, // + 2, 1, 0, 0, -6766, 30824, // + 1, 0, -1, 0, -5163, -8379, // + 1, 1, 0, 0, 4987, -16675, // + 2, -1, 1, 0, 4036, -12831, // + 2, 0, 2, 0, 3994, -10445, // + 4, 0, 0, 0, 3861, -11650, // + 2, 0, -3, 0, 3665, 14403, // + 0, 1, -2, 0, -2689, -7003, // + 2, 0, -1, 2, -2602, 0, // + 2, -1, -2, 0, 2390, 10056, // + 1, 0, 1, 0, -2348, 6322, // + 2, -2, 0, 0, 2236, -9884, // + 0, 1, 2, 0, -2120, 5751, // + 0, 2, 0, 0, -2069, 0, // + 2, -2, -1, 0, 2048, -4950, // +]; + +/// Periodic terms for ecliptic latitude — Meeus table 45.B, first 25 rows. +/// Packed five to a row as `D, M, M', F, Σb (1e-6°)`. +const List _latitude = [ + 0, 0, 0, 1, 5128122, // + 0, 0, 1, 1, 280602, // + 0, 0, 1, -1, 277693, // + 2, 0, 0, -1, 173237, // + 2, 0, -1, 1, 55413, // + 2, 0, -1, -1, 46271, // + 2, 0, 0, 1, 32573, // + 0, 0, 2, 1, 17198, // + 2, 0, 1, -1, 9266, // + 0, 0, 2, -1, 8822, // + 2, -1, 0, -1, 8216, // + 2, 0, -2, -1, 4324, // + 2, 0, 1, 1, 4200, // + 2, 1, 0, -1, -3359, // + 2, -1, -1, 1, 2463, // + 2, -1, 0, 1, 2211, // + 2, -1, -1, -1, 2065, // + 0, 1, -1, -1, -1870, // + 4, 0, -1, -1, 1828, // + 0, 1, 0, 1, -1794, // + 0, 0, 0, 3, -1749, // + 0, 1, -1, 1, -1565, // + 1, 0, 0, 1, -1491, // + 0, 1, 1, 1, -1475, // + 0, 1, 1, -1, -1410, // +]; diff --git a/lib/core/astro/moon_orientation.dart b/lib/core/astro/moon_orientation.dart new file mode 100644 index 000000000..b30da7aaa --- /dev/null +++ b/lib/core/astro/moon_orientation.dart @@ -0,0 +1,86 @@ +/// How the Moon is *tilted* when you actually look at it. +/// +/// A rendered Moon that ignores this is the commonest mistake in the genre: it +/// draws the terminator vertical and the north pole up, which is what you would +/// see from the north celestial pole and nowhere else. At Taiwan's latitude the +/// real crescent is visibly rolled, and low in the sky it lies almost on its +/// back — the "smiling moon" everybody has photographed. +/// +/// Two independent angles are needed, and one will not do: +/// +/// * [northBearing] — which way the Moon's own north pole points on the sky, +/// so the maria land where an observer sees them. +/// * [brightLimbBearing] — which way the lit side faces. This is *not* 90° +/// from the pole: the Sun is on the ecliptic and the Moon is near it, so +/// the terminator tilts against the polar axis by up to about 30° over a +/// month. +/// +/// Both are derived as great-circle bearings between two apparent places +/// rather than from a position-angle convention. That is deliberate: the +/// statement "the crescent's bright side faces the Sun" is a physical fact +/// that cannot come out backwards, whereas a sign convention transcribed from +/// a book can, and would look plausible either way. +library; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// The Moon's orientation for one observer at one instant. +class MoonOrientation { + const MoonOrientation({ + required this.northBearing, + required this.brightLimbBearing, + required this.horizontal, + }); + + /// Screen bearing of the Moon's north pole: measured from straight up, + /// increasing clockwise. Zero means the pole points at the zenith. + final double northBearing; + + /// Screen bearing of the middle of the lit limb, same convention. This is + /// the direction of the Sun as seen from the Moon. + final double brightLimbBearing; + + /// Where the Moon is — how high, and which way to face. + final Horizontal horizontal; + + /// The tilt of the lit limb relative to the Moon's own axis, radians. Purely + /// derived, but it is the number that says how much a "north-up" rendering + /// would be wrong by. + double get limbTiltFromPole => signedTurn(brightLimbBearing - northBearing); + + /// The Moon's orientation at [utc] for the observer at [latitude] / + /// [longitude] in degrees, east positive. + factory MoonOrientation.at( + DateTime utc, { + required double latitude, + required double longitude, + }) { + final observer = Observer(latitude: latitude, longitude: longitude); + final moon = MoonEphemeris.at(utc); + final moonAt = observer.lookAt(moon.equatorial, utc); + + // A point a little way toward the celestial pole from the Moon. The Moon's + // own axis leans under 1.5° from that, which is inside the libration + // already being applied to the globe. + const nudge = 0.001; + final polewards = observer.lookAt( + Equatorial( + rightAscension: moon.equatorial.rightAscension, + declination: moon.equatorial.declination + nudge, + distanceKm: moon.distanceKm, + ), + utc, + ); + + final sunAt = observer.lookAt(SunEphemeris.at(utc).equatorial, utc); + + return MoonOrientation( + northBearing: moonAt.bearingTo(polewards), + brightLimbBearing: moonAt.bearingTo(sunAt), + horizontal: moonAt, + ); + } +} diff --git a/lib/core/astro/moon_phase.dart b/lib/core/astro/moon_phase.dart new file mode 100644 index 000000000..c54b780dd --- /dev/null +++ b/lib/core/astro/moon_phase.dart @@ -0,0 +1,150 @@ +/// The Moon as the page shows it — phase, illumination, distance, libration. +/// +/// A thin reading of [MoonEphemeris]: every value here is the same position +/// looked at differently, so nothing in this file evaluates a series of its +/// own. Pass any UTC instant, past or future; nothing needs a server or a +/// downloaded star table. +/// +/// Convention: `angle` 0 = new moon, π = full moon, 2π = the next new moon. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/moon_ephemeris.dart'; + +/// The synodic month — mean Earth days between identical phases. +const double synodicMonthDays = 29.530588853; + +/// Inclination of the lunar equator to the ecliptic (Meeus ch. 51). +const double _lunarEquatorInclination = 1.54242 * math.pi / 180; + +/// How the Moon looks from Earth at one instant. +class MoonPhase { + const MoonPhase._(this._ephemeris); + + final MoonEphemeris _ephemeris; + + /// The Moon at [utc]. + factory MoonPhase.at(DateTime utc) => MoonPhase._(MoonEphemeris.at(utc)); + + /// Phase angle in radians: 0 = new, π/2 = first quarter (waxing), + /// π = full, 3π/2 = last quarter (waning), 2π = next new. + double get angle => _ephemeris.phaseAngle; + + /// Illuminated fraction, 0 (new) … 1 (full). + double get brightness => _ephemeris.illuminated; + + /// Centre-to-centre distance in kilometres — 356,500 at perigee to 406,700 + /// at apogee, a 14% swing that is most of why some full moons look larger. + double get distanceKm => _ephemeris.distanceKm; + + /// Apparent angular diameter in degrees — 0.49° to 0.56°. + double get apparentDiameterDegrees => + _ephemeris.angularDiameter * 180 / math.pi; + + /// Age in days since the last new moon (0…[synodicMonthDays]). + double get ageInDays => angle / (2 * math.pi) * synodicMonthDays; + + /// Whether the disc is gaining light (new → full). + bool get waxing => angle < math.pi; + + /// The eight classical phases, resolved by the 45° sector the angle sits in. + MoonPhaseName get name => + MoonPhaseName.values[((angle / (2 * math.pi)) * 8).round() % 8]; + + /// Just the angle — the scalar form the shader wants. + static double angleAt(DateTime utc) => MoonEphemeris.at(utc).phaseAngle; + + /// The next instant at which the phase angle reaches [target] (default: full + /// moon, π), strictly after [utc]. + /// + /// The elongation gains 2π every synodic month and never reverses, so there + /// is nothing to search for: coast to the target at the mean rate, then + /// re-measure and correct. The true rate strays at most a fifth from the + /// mean, so each pass cuts the error fivefold and five passes land within + /// five seconds — no scan, no bracket, no horizon to fall off. + static DateTime nextAngle(DateTime utc, {double target = math.pi}) { + final at = _settle(utc.add(_coast(turn(target - angleAt(utc)))), target); + // Asked from the target instant itself, the coast is zero and the search + // lands right back on it. "Next" then means a month later. + return at.isAfter(utc) ? at : _settle(at.add(_coast(2 * math.pi)), target); + } + + /// Next full moon strictly after [utc]. + static DateTime nextFullMoon(DateTime utc) => nextAngle(utc); + + /// Next new moon strictly after [utc]. New is the wrap point, so the target + /// is the wrap itself — the coast to it is a whole month from just after one + /// and almost nothing from just before the next, which is the right answer + /// in both directions. + static DateTime nextNewMoon(DateTime utc) => nextAngle(utc, target: 0); + + /// Optical libration at [utc] as `(longitude, latitude)` in **radians** — + /// the selenographic point facing Earth. + /// + /// The Moon keeps one face toward us, but not a still one: its orbit is + /// elliptical and its equator tilted, so over a month it rocks about ±7° in + /// longitude and ±6.7° in latitude, showing a little around each limb in + /// turn. Leaving it out is what makes a rendered moon look like a decal — + /// every night identical. + /// + /// Optical libration only (Meeus ch. 51). The physical libration is under a + /// tenth of a degree, invisible at any size this is drawn. + static ({double longitude, double latitude}) librationAt(DateTime utc) { + final moon = MoonEphemeris.at(utc); + // Mean longitude of the ascending node of the lunar orbit. + final node = (125.0445479 - 1934.1362891 * moon.centuries) * math.pi / 180; + final w = moon.longitude - node; + final sinB = math.sin(moon.latitude); + final cosB = math.cos(moon.latitude); + final sinI = math.sin(_lunarEquatorInclination); + final cosI = math.cos(_lunarEquatorInclination); + final a = math.atan2( + math.sin(w) * cosB * cosI - sinB * sinI, + math.cos(w) * cosB, + ); + return ( + longitude: _shortestWay(a - moon.argumentOfLatitude), + latitude: math.asin(-math.sin(w) * cosB * sinI - sinB * cosI), + ); + } + + /// Refines an estimate onto [target] by re-measuring and correcting. + static DateTime _settle(DateTime estimate, double target) { + var at = estimate; + for (var pass = 0; pass < 5; pass++) { + at = at.add(_coast(_shortestWay(target - angleAt(at)))); + } + return at; + } + + /// How long the mean phase takes to advance by [radians]. + static Duration _coast(double radians) => Duration( + milliseconds: + (radians / + (2 * math.pi) * + synodicMonthDays * + Duration.millisecondsPerDay) + .round(), + ); + + /// Wraps an angle into `(-π, π]` — the signed error, so a correction can go + /// backwards instead of round the whole month. + static double _shortestWay(double radians) { + final wrapped = turn(radians); + return wrapped > math.pi ? wrapped - 2 * math.pi : wrapped; + } +} + +/// The eight classical lunar phases. +enum MoonPhaseName { + newMoon, + waxingCrescent, + firstQuarter, + waxingGibbous, + fullMoon, + waningGibbous, + lastQuarter, + waningCrescent, +} diff --git a/lib/core/astro/moon_rise_set.dart b/lib/core/astro/moon_rise_set.dart new file mode 100644 index 000000000..59f56dea8 --- /dev/null +++ b/lib/core/astro/moon_rise_set.dart @@ -0,0 +1,107 @@ +/// When the Moon rises, transits and sets at one place. +/// +/// A thin naming of the shared solver in `sky_position.dart` — the Moon's own +/// contribution is only its track and its horizon. The horizon is the +/// interesting part: refraction lifts the limb 34′, while the parallax (nearly +/// a degree, and varying 14% with distance) converts the geocentric series +/// into what an observer on the surface sees. Because the parallax comes from +/// the distance already computed, it is exact for the night rather than a mean +/// value. +/// +/// Some days have no moonrise at all: the Moon comes up ~50 minutes later each +/// day, so roughly once a month a calendar day gets skipped. `null` is a real +/// answer here, not a failure. +/// +/// **Measured**, not asserted. Against the US Naval Observatory over 2026 +/// (`test/core/astro/`) for Taipei, Sydney and Reykjavík — the last because at +/// 64°N the Moon skims the horizon and any weakness in the search shows there +/// first — and against the CWA's published Keelung timetable. +library; + +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/sky_position.dart'; + +/// Moonrise, transit and moonset within one window at one place. +class MoonRiseSet { + const MoonRiseSet({ + required this.rise, + required this.set, + this.transit, + this.startsAbove = false, + }); + + /// When the Moon's upper limb clears the horizon, or `null` if it does not + /// rise in the window. + final DateTime? rise; + + /// When it goes back down, on the same terms. + final DateTime? set; + + /// Upper transit — the Moon at its highest, and the best moment to look. + final DateTime? transit; + + /// Whether it was already up when the window opened. + final bool startsAbove; + + /// Neither a rise nor a set fell in the window. [aboveHorizon] — or + /// [startsAbove] — says which side of the horizon that was. + bool get isCircumpolar => rise == null && set == null; + + /// Rise, transit and set within [window] (24 h by default) starting at + /// [from], for the observer at [latitude] / [longitude] in degrees, east + /// positive. + /// + /// Pass the UTC instant the local day begins — the caller owns the timezone, + /// this owns the astronomy. + factory MoonRiseSet.of( + DateTime from, { + required double latitude, + required double longitude, + Duration window = const Duration(hours: 24), + }) { + final solved = RiseSet.solve( + from: from, + observer: Observer(latitude: latitude, longitude: longitude), + track: moonTrack, + horizon: moonHorizon, + window: window, + ); + return MoonRiseSet( + rise: solved.rise, + set: solved.set, + transit: solved.transit, + startsAbove: solved.startsAbove, + ); + } + + /// Whether the Moon is above the horizon at [utc] — the other half of the + /// answer when neither a rise nor a set falls inside the window. + static bool aboveHorizon( + DateTime utc, { + required double latitude, + required double longitude, + }) { + final moon = MoonEphemeris.at(utc); + final observer = Observer(latitude: latitude, longitude: longitude); + return observer.lookAt(moon.equatorial, utc).altitude > + moon.horizonAltitude; + } + + /// Where the Moon appears at [utc] — how high, and which way to look. + static Horizontal lookFrom( + DateTime utc, { + required double latitude, + required double longitude, + }) => Observer( + latitude: latitude, + longitude: longitude, + ).lookAt(MoonEphemeris.at(utc).equatorial, utc); +} + +/// The Moon's track, for the shared solver. +Equatorial moonTrack(DateTime utc) => MoonEphemeris.at(utc).equatorial; + +/// The Moon's rise/set horizon — distance-dependent, so it is recomputed at +/// every sample rather than fixed at a mean parallax. +double moonHorizon(Equatorial position) => + MoonEphemeris.horizonAltitudeFor(position.distanceKm!); diff --git a/lib/core/astro/night_window.dart b/lib/core/astro/night_window.dart new file mode 100644 index 000000000..09e461e0e --- /dev/null +++ b/lib/core/astro/night_window.dart @@ -0,0 +1,148 @@ +/// When it is actually dark enough to observe. +/// +/// The question every observer asks first, and the one no single reading +/// answers. Sunset is not darkness — the sky takes another hour and a half to +/// finish. And a sky with no Sun in it can still be useless, because a gibbous +/// Moon washes out everything faint. +/// +/// So the observing window is the intersection of two conditions this package +/// computes separately: the Sun below −18° (astronomical night) **and** the +/// Moon below the horizon. Either one alone is a half-answer that reads as a +/// whole one. +/// +/// The result can be empty, and often is: for a week around full moon there is +/// no dark window at all at some latitudes, and above about 49° there is none +/// in midsummer whatever the Moon does. Both are real answers. +library; + +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/moon_rise_set.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; +import 'package:dpip/core/astro/sun_events.dart'; + +/// One stretch of usable dark. +class DarkWindow { + const DarkWindow({required this.from, required this.to}); + + final DateTime from; + final DateTime to; + + Duration get length => to.difference(from); +} + +/// Tonight's observing conditions at one place. +class NightConditions { + const NightConditions({ + required this.astronomicalNight, + required this.darkWindows, + required this.moonIllumination, + required this.moonRise, + required this.moonSet, + }); + + /// Dusk to dawn with the Sun 18° down, or null if the Sun never gets there. + final (DateTime, DateTime)? astronomicalNight; + + /// The parts of that night with the Moon also down. Empty when the Moon is + /// up throughout — which is not an error, it is a bad night. + final List darkWindows; + + /// The Moon's illuminated fraction in the middle of the night — the number + /// that decides how bad "moon up" actually is. + final double moonIllumination; + + final DateTime? moonRise; + final DateTime? moonSet; + + /// The longest usable stretch, if any. + DarkWindow? get best => darkWindows.isEmpty + ? null + : darkWindows.reduce((a, b) => a.length >= b.length ? a : b); + + /// Total dark time. + Duration get totalDark => + darkWindows.fold(Duration.zero, (sum, window) => sum + window.length); + + /// Conditions for the night of [from] — pass the UTC instant the local day + /// begins, as the other astronomy pages do. + /// + /// The Sun is solved from local **noon**, not from midnight. Solved from + /// midnight, the first dusk found is this evening's and the first dawn is + /// this *morning's* — a pair in the wrong order, describing a night that + /// already ended. Noon-to-noon is the window a night actually sits in. + factory NightConditions.of( + DateTime from, { + required double latitude, + required double longitude, + Duration window = const Duration(hours: 24), + }) { + final sun = SunEvents.of( + from.add(const Duration(hours: 12)), + latitude: latitude, + longitude: longitude, + window: window, + ); + final night = sun.astronomicalNight; + final observer = Observer(latitude: latitude, longitude: longitude); + + final moonEvents = MoonRiseSet.of( + from, + latitude: latitude, + longitude: longitude, + window: window, + ); + + final windows = []; + if (night != null) { + // Walk the astronomical night and keep the stretches with no Moon. A + // scan rather than interval arithmetic, because the Moon can rise and + // set inside one night and the cases multiply quickly. + const step = Duration(minutes: 5); + DateTime? openedAt; + var at = night.$1; + while (!at.isAfter(night.$2)) { + final moon = MoonEphemeris.at(at); + final moonUp = + observer.lookAt(moon.equatorial, at).altitude > + moon.horizonAltitude; + if (!moonUp && openedAt == null) { + openedAt = at; + } else if (moonUp && openedAt != null) { + windows.add(DarkWindow(from: openedAt, to: at)); + openedAt = null; + } + at = at.add(step); + } + if (openedAt != null) { + windows.add(DarkWindow(from: openedAt, to: night.$2)); + } + } + + final middle = night == null + ? from.add(const Duration(hours: 12)) + : night.$1.add(night.$2.difference(night.$1) ~/ 2); + + return NightConditions( + astronomicalNight: night, + // Sub-ten-minute slivers are an artefact of the scan step, not an + // observing opportunity. + darkWindows: windows + .where((w) => w.length > const Duration(minutes: 10)) + .toList(), + moonIllumination: MoonEphemeris.at(middle).illuminated, + moonRise: moonEvents.rise, + moonSet: moonEvents.set, + ); + } + + /// The Sun's position right now, so a caller can say "still daylight". + static Horizontal sunNow( + DateTime utc, { + required double latitude, + required double longitude, + }) => Observer( + latitude: latitude, + longitude: longitude, + ).lookAt(SunEphemeris.at(utc).equatorial, utc); +} diff --git a/lib/core/astro/planet_ephemeris.dart b/lib/core/astro/planet_ephemeris.dart new file mode 100644 index 000000000..bcd7a0e40 --- /dev/null +++ b/lib/core/astro/planet_ephemeris.dart @@ -0,0 +1,428 @@ +/// Where the planets are, and how bright. +/// +/// Elements, not series. JPL's *Keplerian Elements for Approximate Positions +/// of the Major Planets* gives six orbital elements and six per-century rates +/// per planet, valid 1800–2050 — one table of ninety-six numbers against the +/// thousands a VSOP87 truncation would need, with published error bounds +/// (15″ for Mercury, 40″ for Mars, 600″ for Saturn in heliocentric longitude). +/// For "is Jupiter up tonight, and where do I point", that is far finer than +/// the question. +/// +/// Three things have to be right or the answer is quietly wrong by a +/// noticeable amount, and each is easy to skip: +/// +/// 1. **Geocentric, not heliocentric.** The Earth's own orbital position is +/// subtracted, so this solves for both bodies and takes the difference. +/// 2. **Light time.** Saturn is over an hour away; using its position *now* +/// rather than where it was when the light left puts it up to an +/// arcminute off. One iteration closes it. +/// 3. **Precession to the equinox of date.** The elements are referred to +/// J2000, but sidereal time is of date. Mixing them is a 0.36° error +/// today — larger than every other error here combined. +/// +/// **Measured** against JPL Horizons over 2024–2027 in `test/core/astro/`. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// Speed of light in astronomical units per day — the light-time correction. +const double _auPerDay = 173.144632674; + +/// The naked-eye and small-telescope planets, in orbital order. The Earth is +/// not a target but is solved alongside every one of them. +enum Planet { mercury, venus, mars, jupiter, saturn, uranus, neptune } + +/// A planet's geocentric place and appearance at one instant. +class PlanetEphemeris { + const PlanetEphemeris({ + required this.planet, + required this.longitude, + required this.latitude, + required this.distanceAu, + required this.heliocentricDistanceAu, + required this.elongation, + required this.signedElongation, + required this.phaseAngle, + required this.magnitude, + required this.centuries, + }); + + final Planet planet; + + /// Geocentric ecliptic longitude and latitude **of date**, radians. + final double longitude; + final double latitude; + + /// Distance from the Earth, astronomical units. + final double distanceAu; + + /// Distance from the Sun, astronomical units. + final double heliocentricDistanceAu; + + /// Angular distance from the Sun as seen from the Earth, radians. Under + /// about 15° the planet is lost in twilight whatever its magnitude; the + /// greatest elongations of Mercury and Venus are the maxima of this. + final double elongation; + + /// Sun–planet–Earth angle, radians. Zero is fully lit; it only gets large + /// for the inner planets, which is why only they show phases. + final double phaseAngle; + + /// Apparent visual magnitude (Astronomical Almanac / Meeus ch. 41). + final double magnitude; + + final double centuries; + + /// Illuminated fraction of the disc, 0…1. + double get illuminated => (1 + math.cos(phaseAngle)) / 2; + + /// Elongation with a sign: positive east of the Sun (sets after it, so an + /// evening object), negative west (rises before it — a morning object). + /// The unsigned [elongation] cannot tell those apart, and which one it is + /// decides whether you look after dusk or before dawn. + final double signedElongation; + + /// Sets after the Sun, so it is visible in the evening sky. + bool get isEvening => signedElongation > 0; + + /// Equatorial coordinates of date. + Equatorial get equatorial => Equatorial.fromEcliptic( + longitude: longitude, + latitude: latitude, + obliquity: meanObliquity(centuries), + distanceKm: distanceAu * astronomicalUnitKm, + ); + + /// The planet's position at [utc]. + factory PlanetEphemeris.at(Planet planet, DateTime utc) { + final t = julianCenturies(utc); + final earth = _heliocentric(_earth, t); + + // Light time: solve where the planet was when the light now arriving left + // it. One pass is enough — the correction to the correction is under a + // milliarcsecond even for Neptune. + var body = _heliocentric(_elements[planet]!, t); + var offset = _difference(body, earth); + final firstDistance = _length(offset); + body = _heliocentric( + _elements[planet]!, + t - firstDistance / _auPerDay / 36525.0, + ); + offset = _difference(body, earth); + + final distance = _length(offset); + final heliocentric = _length(body); + final sunDistance = _length(earth); + + // Precession of the ecliptic frame from J2000 to the equinox of date. + final precession = (1.396971 * t + 0.0003086 * t * t) * degrees; + final longitude = turn(math.atan2(offset.y, offset.x) + precession); + final latitude = math.asin((offset.z / distance).clamp(-1.0, 1.0)); + + // The Sun–planet–Earth triangle gives both the elongation seen from here + // and the phase angle seen from there. + final elongation = math.acos( + ((distance * distance + + sunDistance * sunDistance - + heliocentric * heliocentric) / + (2 * distance * sunDistance)) + .clamp(-1.0, 1.0), + ); + final phaseAngle = math.acos( + ((heliocentric * heliocentric + + distance * distance - + sunDistance * sunDistance) / + (2 * heliocentric * distance)) + .clamp(-1.0, 1.0), + ); + + return PlanetEphemeris( + planet: planet, + longitude: longitude, + latitude: latitude, + distanceAu: distance, + heliocentricDistanceAu: heliocentric, + elongation: elongation, + signedElongation: signedTurn( + longitude - SunEphemeris.atCenturies(t).longitude, + ), + phaseAngle: phaseAngle, + magnitude: _magnitude( + planet, + heliocentric, + distance, + phaseAngle / degrees, + longitude, + latitude, + t, + ), + centuries: t, + ); + } + + /// The planet's track, for the shared rise/set solver. + static SkyTrack trackOf(Planet planet) => + (utc) => PlanetEphemeris.at(planet, utc).equatorial; + + /// Heliocentric rectangular ecliptic coordinates (J2000), astronomical + /// units, from the Keplerian elements at [t] Julian centuries. + static _Vector _heliocentric(_Elements e, double t) { + final a = e.semiMajorAxis + e.semiMajorAxisRate * t; + final eccentricity = e.eccentricity + e.eccentricityRate * t; + final inclination = (e.inclination + e.inclinationRate * t) * degrees; + final meanLongitude = e.meanLongitude + e.meanLongitudeRate * t; + final perihelion = e.perihelion + e.perihelionRate * t; + final node = (e.node + e.nodeRate * t) * degrees; + + final meanAnomaly = signedTurn((meanLongitude - perihelion) * degrees); + final anomaly = eccentricAnomaly(meanAnomaly, eccentricity); + final argument = (perihelion) * degrees - node; + + // In the orbital plane, then rotated by the argument of perihelion, the + // inclination and the node — the standard three-rotation chain. + final x = a * (math.cos(anomaly) - eccentricity); + final y = + a * math.sqrt(1 - eccentricity * eccentricity) * math.sin(anomaly); + + final cosArgument = math.cos(argument); + final sinArgument = math.sin(argument); + final cosNode = math.cos(node); + final sinNode = math.sin(node); + final cosInclination = math.cos(inclination); + final sinInclination = math.sin(inclination); + + final xOrbit = cosArgument * x - sinArgument * y; + final yOrbit = sinArgument * x + cosArgument * y; + + return _Vector( + cosNode * xOrbit - sinNode * yOrbit * cosInclination, + sinNode * xOrbit + cosNode * yOrbit * cosInclination, + yOrbit * sinInclination, + ); + } + + static _Vector _difference(_Vector a, _Vector b) => + _Vector(a.x - b.x, a.y - b.y, a.z - b.z); + + static double _length(_Vector v) => + math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + + /// Apparent magnitude (Meeus ch. 41, the Astronomical Almanac forms). + /// + /// Saturn carries a ring term because its rings swing its brightness by well + /// over a magnitude between edge-on and wide open — leaving it out would + /// make the single most conspicuous planet the least accurate entry. + static double _magnitude( + Planet planet, + double r, + double delta, + double phaseDegrees, + double longitude, + double latitude, + double t, + ) { + final base = 5 * (math.log(r * delta) / math.ln10); + final i = phaseDegrees; + return switch (planet) { + Planet.mercury => + -0.42 + base + 0.0380 * i - 0.000273 * i * i + 0.000002 * i * i * i, + Planet.venus => + -4.40 + base + 0.0009 * i + 0.000239 * i * i - 0.00000065 * i * i * i, + Planet.mars => -1.52 + base + 0.016 * i, + Planet.jupiter => -9.40 + base + 0.005 * i, + Planet.saturn => _saturn(base, longitude, latitude, t), + Planet.uranus => -7.19 + base, + Planet.neptune => -6.87 + base, + }; + } + + /// Saturn, with the ring tilt. `B` is the Saturnicentric latitude of the + /// Earth — how far the rings are opened toward us (Meeus 45.1). At B = 0 the + /// rings are edge-on and vanish; near ±27° they are at their brightest. + static double _saturn( + double base, + double longitude, + double latitude, + double t, + ) { + const ringInclination = 28.075 * degrees; + final ringNode = (169.508 + 1.394 * t) * degrees; + final sinB = + math.sin(ringInclination) * + math.cos(latitude) * + math.sin(longitude - ringNode) - + math.cos(ringInclination) * math.sin(latitude); + return -8.88 + base + 0.044 * 0 - 2.60 * sinB.abs() + 1.25 * sinB * sinB; + } +} + +class _Vector { + const _Vector(this.x, this.y, this.z); + final double x; + final double y; + final double z; +} + +/// Keplerian elements and their per-century rates. +class _Elements { + const _Elements({ + required this.semiMajorAxis, + required this.semiMajorAxisRate, + required this.eccentricity, + required this.eccentricityRate, + required this.inclination, + required this.inclinationRate, + required this.meanLongitude, + required this.meanLongitudeRate, + required this.perihelion, + required this.perihelionRate, + required this.node, + required this.nodeRate, + }); + + /// Astronomical units, and au per century. + final double semiMajorAxis; + final double semiMajorAxisRate; + + /// Dimensionless, and per century. + final double eccentricity; + final double eccentricityRate; + + /// Degrees, and degrees per century, for the remaining four. + final double inclination; + final double inclinationRate; + final double meanLongitude; + final double meanLongitudeRate; + + /// Longitude of perihelion ϖ, not the argument ω. + final double perihelion; + final double perihelionRate; + final double node; + final double nodeRate; +} + +/// The Earth–Moon barycentre. Its offset from the Earth's centre is at most +/// 4700 km, which at Mars' distance is under two arcseconds — a twentieth of +/// this table's own error for Mars. +const _earth = _Elements( + semiMajorAxis: 1.00000261, + semiMajorAxisRate: 0.00000562, + eccentricity: 0.01671123, + eccentricityRate: -0.00004392, + inclination: -0.00001531, + inclinationRate: -0.01294668, + meanLongitude: 100.46457166, + meanLongitudeRate: 35999.37244981, + perihelion: 102.93768193, + perihelionRate: 0.32327364, + node: 0.0, + nodeRate: 0.0, +); + +/// JPL's table 1, valid 1800–2050. Transcribed from +/// `ssd.jpl.nasa.gov/planets/approx_pos.html`; the tests pin it against +/// Horizons, which is what catches a mistyped digit. +const Map _elements = { + Planet.mercury: _Elements( + semiMajorAxis: 0.38709927, + semiMajorAxisRate: 0.00000037, + eccentricity: 0.20563593, + eccentricityRate: 0.00001906, + inclination: 7.00497902, + inclinationRate: -0.00594749, + meanLongitude: 252.25032350, + meanLongitudeRate: 149472.67411175, + perihelion: 77.45779628, + perihelionRate: 0.16047689, + node: 48.33076593, + nodeRate: -0.12534081, + ), + Planet.venus: _Elements( + semiMajorAxis: 0.72333566, + semiMajorAxisRate: 0.00000390, + eccentricity: 0.00677672, + eccentricityRate: -0.00004107, + inclination: 3.39467605, + inclinationRate: -0.00078890, + meanLongitude: 181.97909950, + meanLongitudeRate: 58517.81538729, + perihelion: 131.60246718, + perihelionRate: 0.00268329, + node: 76.67984255, + nodeRate: -0.27769418, + ), + Planet.mars: _Elements( + semiMajorAxis: 1.52371034, + semiMajorAxisRate: 0.00001847, + eccentricity: 0.09339410, + eccentricityRate: 0.00007882, + inclination: 1.84969142, + inclinationRate: -0.00813131, + meanLongitude: -4.55343205, + meanLongitudeRate: 19140.30268499, + perihelion: -23.94362959, + perihelionRate: 0.44441088, + node: 49.55953891, + nodeRate: -0.29257343, + ), + Planet.jupiter: _Elements( + semiMajorAxis: 5.20288700, + semiMajorAxisRate: -0.00011607, + eccentricity: 0.04838624, + eccentricityRate: -0.00013253, + inclination: 1.30439695, + inclinationRate: -0.00183714, + meanLongitude: 34.39644051, + meanLongitudeRate: 3034.74612775, + perihelion: 14.72847983, + perihelionRate: 0.21252668, + node: 100.47390909, + nodeRate: 0.20469106, + ), + Planet.saturn: _Elements( + semiMajorAxis: 9.53667594, + semiMajorAxisRate: -0.00125060, + eccentricity: 0.05386179, + eccentricityRate: -0.00050991, + inclination: 2.48599187, + inclinationRate: 0.00193609, + meanLongitude: 49.95424423, + meanLongitudeRate: 1222.49362201, + perihelion: 92.59887831, + perihelionRate: -0.41897216, + node: 113.66242448, + nodeRate: -0.28867794, + ), + Planet.uranus: _Elements( + semiMajorAxis: 19.18916464, + semiMajorAxisRate: -0.00196176, + eccentricity: 0.04725744, + eccentricityRate: -0.00004397, + inclination: 0.77263783, + inclinationRate: -0.00242939, + meanLongitude: 313.23810451, + meanLongitudeRate: 428.48202785, + perihelion: 170.95427630, + perihelionRate: 0.40805281, + node: 74.01692503, + nodeRate: 0.04240589, + ), + Planet.neptune: _Elements( + semiMajorAxis: 30.06992276, + semiMajorAxisRate: 0.00026291, + eccentricity: 0.00859048, + eccentricityRate: 0.00005105, + inclination: 1.77004347, + inclinationRate: 0.00035372, + meanLongitude: -55.12002969, + meanLongitudeRate: 218.45945325, + perihelion: 44.96476227, + perihelionRate: -0.32241464, + node: 131.78422574, + nodeRate: -0.00508664, + ), +}; diff --git a/lib/core/astro/satellite.dart b/lib/core/astro/satellite.dart new file mode 100644 index 000000000..3469f574f --- /dev/null +++ b/lib/core/astro/satellite.dart @@ -0,0 +1,676 @@ +/// Satellite passes — SGP4, the model the elements are actually defined by. +/// +/// A two-line element set is not a state vector. It is a set of *mean* +/// elements fitted so that one specific propagator reproduces the orbit, and +/// that propagator is SGP4. Feeding TLE numbers to a plain Keplerian +/// integrator gives an answer that looks reasonable and is kilometres wrong +/// within hours, so the model is implemented here in full rather than +/// approximated (Spacetrack Report #3, near-Earth case; the deep-space terms +/// are omitted, which excludes orbits with periods over 225 minutes — every +/// satellite anyone watches by eye is well inside that). +/// +/// **The honest caveat, and it is a real one.** TLEs go stale. Atmospheric +/// drag is not predictable, so the ISS's elements are good for days, not +/// months, and a pass computed from a month-old element set can be minutes +/// out. Everything else in this package is closed-form and correct for +/// centuries; this one is not, and the age of the elements is carried on the +/// result ([TleSet.ageAt]) so a caller can show it rather than imply a +/// precision it does not have. The bundled snapshot is a starting point, not a +/// promise — a fresher set can be handed in without touching the propagator. +/// +/// **Measured** against the SGP4 verification vectors from Spacetrack Report +/// #3 in `test/core/astro/`. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// Earth's equatorial radius in the WGS-72 system SGP4 is defined on, km. +const double _earthRadiusKm = 6378.135; + +/// √(GM) in earth-radii^1.5 per minute — SGP4's gravitational constant. +const double _ke = 0.0743669161; + +/// J2, J3 and J4 folded into the forms the model uses. +/// +/// `_a30` is A(3,0) = −J3·aE³. J3 is itself negative (−0.253881e-5), so the +/// constant here is **positive** — dropping that negation is a sign error that +/// leaves the in-plane velocity components right and the out-of-plane one +/// nearly three times too large. +const double _k2 = 5.413080e-4; +const double _k4 = 0.62098875e-6; +const double _a30 = 0.253881e-5; + +/// A parsed two-line element set. +class TleSet { + const TleSet({ + required this.name, + required this.catalogNumber, + required this.epoch, + required this.meanMotion, + required this.eccentricity, + required this.inclination, + required this.rightAscensionOfNode, + required this.argumentOfPerigee, + required this.meanAnomaly, + required this.bstar, + }); + + final String name; + final int catalogNumber; + + /// The instant the elements describe. + final DateTime epoch; + + /// Radians per minute. + final double meanMotion; + + final double eccentricity; + + /// Radians. + final double inclination; + final double rightAscensionOfNode; + final double argumentOfPerigee; + final double meanAnomaly; + + /// The drag term, in inverse earth radii. + final double bstar; + + /// How old the elements are at [utc] — the number that decides whether to + /// trust the answer. + Duration ageAt(DateTime utc) => utc.difference(epoch); + + /// Parses the standard three-line form (name, line 1, line 2). + /// + /// The columns are fixed-width by specification, so they are read by + /// position rather than by splitting: several fields can be blank or + /// signed-with-a-space and would vanish under a whitespace split. + factory TleSet.parse(String name, String line1, String line2) { + double implied(String field) { + // "89427-4" means 0.89427e-4; the exponent sign is the last character. + final mantissa = field.substring(0, 6).trim(); + final exponent = field.substring(6).trim(); + if (mantissa.isEmpty || mantissa == '00000') return 0; + final sign = mantissa.startsWith('-') ? -1 : 1; + final digits = mantissa.replaceAll(RegExp('[+-]'), ''); + return sign * + double.parse('0.$digits') * + math.pow(10, int.parse(exponent)).toDouble(); + } + + final epochField = double.parse(line1.substring(18, 32)); + final twoDigitYear = epochField ~/ 1000; + final year = twoDigitYear < 57 ? 2000 + twoDigitYear : 1900 + twoDigitYear; + final dayOfYear = epochField - twoDigitYear * 1000; + final epoch = DateTime.utc(year).add( + Duration( + microseconds: ((dayOfYear - 1) * Duration.microsecondsPerDay).round(), + ), + ); + + return TleSet( + name: name.trim(), + catalogNumber: int.parse(line1.substring(2, 7).trim()), + epoch: epoch, + // Revolutions per day → radians per minute. + meanMotion: + double.parse(line2.substring(52, 63).trim()) * 2 * math.pi / 1440, + eccentricity: double.parse('0.${line2.substring(26, 33).trim()}'), + inclination: double.parse(line2.substring(8, 16).trim()) * degrees, + rightAscensionOfNode: + double.parse(line2.substring(17, 25).trim()) * degrees, + argumentOfPerigee: double.parse(line2.substring(34, 42).trim()) * degrees, + meanAnomaly: double.parse(line2.substring(43, 51).trim()) * degrees, + bstar: implied(line1.substring(53, 61)), + ); + } + + /// Every element set in a standard TLE file. + static List parseAll(String text) { + final lines = text + .split('\n') + .map((line) => line.trimRight()) + .where((line) => line.isNotEmpty) + .toList(); + final sets = []; + for (var i = 0; i + 2 < lines.length; i += 3) { + if (!lines[i + 1].startsWith('1 ') || !lines[i + 2].startsWith('2 ')) { + continue; + } + sets.add(TleSet.parse(lines[i], lines[i + 1], lines[i + 2])); + } + return sets; + } +} + +/// A satellite's position and velocity in the TEME frame SGP4 works in. +class SatelliteState { + const SatelliteState({required this.position, required this.velocity}); + + /// Kilometres. + final (double, double, double) position; + + /// Kilometres per second. + final (double, double, double) velocity; +} + +/// The SGP4 propagator, set up once per element set. +/// +/// The expensive part is the initialisation — recovering the original mean +/// motion and the drag coefficients — so it is done in the constructor and a +/// propagation is then just arithmetic. A pass search evaluates this thousands +/// of times. +class Sgp4 { + Sgp4(this.elements) { + final cosInclination = math.cos(elements.inclination); + final theta2 = cosInclination * cosInclination; + final e0 = elements.eccentricity; + final betaSquared = 1 - e0 * e0; + final beta0 = math.sqrt(betaSquared); + + // Recover the original mean motion and semi-major axis from the Kozai + // element in the TLE (Spacetrack #3, eqns 1-6). + final a1 = math.pow(_ke / elements.meanMotion, 2 / 3).toDouble(); + final tempA = 1.5 * _k2 * (3 * theta2 - 1) / (betaSquared * beta0); + final delta1 = tempA / (a1 * a1); + final a0 = + a1 * + (1 - + delta1 / 3 - + delta1 * delta1 - + 134 / 81 * delta1 * delta1 * delta1); + final delta0 = tempA / (a0 * a0); + _meanMotion = elements.meanMotion / (1 + delta0); + _semiMajorAxis = a0 / (1 - delta0); + + // The atmospheric model is lowered for satellites that dip low. + final perigee = (_semiMajorAxis * (1 - e0) - 1) * _earthRadiusKm; + var s = 1.01222928; + var qoms24 = 1.88027916e-9; + if (perigee < 156) { + final sTemp = perigee < 98 ? 20.0 : perigee - 78.0; + qoms24 = math.pow((120 - sTemp) / _earthRadiusKm, 4).toDouble(); + s = sTemp / _earthRadiusKm + 1; + } + + final xi = 1 / (_semiMajorAxis - s); + final eta = _semiMajorAxis * e0 * xi; + final etaSquared = eta * eta; + final eeta = e0 * eta; + final psi = (1 - etaSquared).abs(); + final coef = qoms24 * math.pow(xi, 4).toDouble(); + final coef1 = coef / math.pow(psi, 3.5); + + final c2 = + coef1 * + _meanMotion * + (_semiMajorAxis * (1 + 1.5 * etaSquared + eeta * (4 + etaSquared)) + + 1.5 * + _k2 * + xi / + psi * + (-0.5 + 1.5 * theta2) * + (8 + 24 * etaSquared + 3 * etaSquared * etaSquared)); + _c1 = elements.bstar * c2; + _c3 = e0 > 1e-4 + ? coef * + xi * + _a30 * + _meanMotion * + math.sin(elements.inclination) / + (_k2 * e0) + : 0.0; + _c4 = + 2 * + _meanMotion * + coef1 * + _semiMajorAxis * + betaSquared * + (eta * (2 + 0.5 * etaSquared) + + e0 * (0.5 + 2 * etaSquared) - + 2 * + _k2 * + xi / + (_semiMajorAxis * psi) * + (-3 * + (3 * theta2 - 1) * + (1 + + 1.5 * etaSquared - + 2 * eeta - + 0.5 * eeta * etaSquared) + + 0.75 * + (1 - theta2) * + (2 * etaSquared - eeta - eeta * etaSquared) * + math.cos(2 * elements.argumentOfPerigee))); + _c5 = + 2 * + coef1 * + _semiMajorAxis * + betaSquared * + (1 + 2.75 * (etaSquared + eeta) + eeta * etaSquared); + + final beta4 = betaSquared * betaSquared; + final a2 = _semiMajorAxis * _semiMajorAxis; + final a4 = a2 * a2; + _meanAnomalyDot = + _meanMotion * + (1 + + 3 * _k2 * (-1 + 3 * theta2) / (2 * a2 * betaSquared * beta0) + + 3 * + _k2 * + _k2 * + (13 - 78 * theta2 + 137 * theta2 * theta2) / + (16 * a4 * beta4 * betaSquared * beta0)); + _perigeeDot = + _meanMotion * + (-3 * _k2 * (1 - 5 * theta2) / (2 * a2 * beta4) + + 3 * + _k2 * + _k2 * + (7 - 114 * theta2 + 395 * theta2 * theta2) / + (16 * a4 * beta4 * beta4) + + 5 * + _k4 * + (3 - 36 * theta2 + 49 * theta2 * theta2) / + (4 * a4 * beta4 * beta4)); + _nodeDot = + _meanMotion * + (-3 * _k2 * cosInclination / (a2 * beta4) + + 3 * + _k2 * + _k2 * + (4 * cosInclination - 19 * theta2 * cosInclination) / + (2 * a4 * beta4 * beta4) + + 5 * + _k4 * + cosInclination * + (3 - 7 * theta2) / + (2 * a4 * beta4 * beta4)); + + _d2 = 4 * _semiMajorAxis * xi * _c1 * _c1; + final temp = _d2 * xi * _c1 / 3; + _d3 = (17 * _semiMajorAxis + s) * temp; + _d4 = + 0.5 * + temp * + _semiMajorAxis * + xi * + (221 * _semiMajorAxis + 31 * s) * + _c1; + + _xi = xi; + _eta = eta; + _qoms24 = qoms24; + _theta = cosInclination; + _beta0 = beta0; + } + + final TleSet elements; + + late final double _meanMotion; + late final double _semiMajorAxis; + late final double _c1; + late final double _c3; + late final double _c4; + late final double _c5; + late final double _d2; + late final double _d3; + late final double _d4; + late final double _meanAnomalyDot; + late final double _perigeeDot; + late final double _nodeDot; + late final double _xi; + late final double _eta; + late final double _qoms24; + late final double _theta; + late final double _beta0; + + /// The satellite's TEME state at [utc]. + SatelliteState at(DateTime utc) => + propagate(utc.difference(elements.epoch).inMicroseconds / 6e7); + + /// The state [minutes] after the element epoch. + SatelliteState propagate(double minutes) { + final t = minutes; + final e0 = elements.eccentricity; + + // Secular effects of drag and gravity. + final meanAnomalyDf = elements.meanAnomaly + _meanAnomalyDot * t; + final perigeeDf = elements.argumentOfPerigee + _perigeeDot * t; + final nodeDf = elements.rightAscensionOfNode + _nodeDot * t; + + final deltaPerigee = + elements.bstar * _c3 * math.cos(elements.argumentOfPerigee) * t; + final deltaMean = e0 > 1e-4 + ? -2 / + 3 * + _qoms24 * + elements.bstar * + math.pow(_xi, 4) / + (e0 * _eta) * + (math.pow(1 + _eta * math.cos(meanAnomalyDf), 3) - + math.pow(1 + _eta * math.cos(elements.meanAnomaly), 3)) + : 0.0; + + final meanAnomaly = meanAnomalyDf + deltaPerigee + deltaMean; + final perigee = perigeeDf - deltaPerigee - deltaMean; + final node = + nodeDf - + 10.5 * + _meanMotion * + _k2 * + _theta / + (_semiMajorAxis * _semiMajorAxis * _beta0 * _beta0) * + _c1 * + t * + t; + + final eccentricity = + e0 - + elements.bstar * _c4 * t - + elements.bstar * + _c5 * + (math.sin(meanAnomaly) - math.sin(elements.meanAnomaly)); + final a = + _semiMajorAxis * + math.pow( + 1 - _c1 * t - _d2 * t * t - _d3 * t * t * t - _d4 * t * t * t * t, + 2, + ); + final l = + meanAnomaly + + perigee + + node + + _meanMotion * + (1.5 * _c1 * t * t + + (_d2 + 2 * _c1 * _c1) * t * t * t + + 0.25 * + (3 * _d3 + 12 * _c1 * _d2 + 10 * _c1 * _c1 * _c1) * + t * + t * + t * + t + + 0.2 * + (3 * _d4 + + 12 * _c1 * _d3 + + 6 * _d2 * _d2 + + 30 * _c1 * _c1 * _d2 + + 15 * _c1 * _c1 * _c1 * _c1) * + t * + t * + t * + t * + t); + final beta = math.sqrt(1 - eccentricity * eccentricity); + final n = _ke / math.pow(a, 1.5); + + // Long-period periodics. + final axn = eccentricity * math.cos(perigee); + final temp = 1 / (a * beta * beta); + final xll = + temp * + _a30 * + math.sin(elements.inclination) / + (8 * _k2) * + axn * + (3 + 5 * _theta) / + (1 + _theta); + final aynl = temp * _a30 * math.sin(elements.inclination) / (4 * _k2); + final ayn = eccentricity * math.sin(perigee) + aynl; + + // Kepler's equation for (E + ω). + final u = turn(l + xll - node); + var eccentricAnomaly = u; + for (var i = 0; i < 10; i++) { + final sinEw = math.sin(eccentricAnomaly); + final cosEw = math.cos(eccentricAnomaly); + var delta = + (u - ayn * cosEw + axn * sinEw - eccentricAnomaly) / + (1 - ayn * sinEw - axn * cosEw); + if (delta.abs() > 0.95) delta = delta.sign * 0.95; + eccentricAnomaly += delta; + if (delta.abs() < 1e-12) break; + } + + final sinEw = math.sin(eccentricAnomaly); + final cosEw = math.cos(eccentricAnomaly); + final ecosE = axn * cosEw + ayn * sinEw; + final esinE = axn * sinEw - ayn * cosEw; + final eSquared = axn * axn + ayn * ayn; + final pl = a * (1 - eSquared); + final r = a * (1 - ecosE); + final rdot = _ke * math.sqrt(a) / r * esinE; + final rfdot = _ke * math.sqrt(pl) / r; + final betaL = math.sqrt(1 - eSquared); + final temp3 = esinE / (1 + betaL); + final cosu = a / r * (cosEw - axn + ayn * temp3); + final sinu = a / r * (sinEw - ayn - axn * temp3); + final uAngle = math.atan2(sinu, cosu); + + final sin2u = 2 * sinu * cosu; + final cos2u = 1 - 2 * sinu * sinu; + final theta2 = _theta * _theta; + + // Short-period periodics. + final rk = + r * (1 - 1.5 * _k2 * betaL / (pl * pl) * (3 * theta2 - 1)) + + 0.5 * _k2 / pl * (1 - theta2) * cos2u; + final uk = uAngle - 0.25 * _k2 / (pl * pl) * (7 * theta2 - 1) * sin2u; + final nodek = node + 1.5 * _k2 * _theta / (pl * pl) * sin2u; + final inclinationk = + elements.inclination + + 1.5 * _k2 * _theta * math.sin(elements.inclination) / (pl * pl) * cos2u; + final rdotk = rdot - _k2 * n / pl * (1 - theta2) * sin2u; + final rfdotk = + rfdot + _k2 * n / pl * ((1 - theta2) * cos2u + 1.5 * (1 - 3 * theta2)); + + // Orientation vectors. + final sinuk = math.sin(uk); + final cosuk = math.cos(uk); + final sinik = math.sin(inclinationk); + final cosik = math.cos(inclinationk); + final sinnok = math.sin(nodek); + final cosnok = math.cos(nodek); + + final ux = -sinnok * cosik * sinuk + cosnok * cosuk; + final uy = cosnok * cosik * sinuk + sinnok * cosuk; + final uz = sinik * sinuk; + final vx = -sinnok * cosik * cosuk - cosnok * sinuk; + final vy = cosnok * cosik * cosuk - sinnok * sinuk; + final vz = sinik * cosuk; + + return SatelliteState( + position: ( + rk * ux * _earthRadiusKm, + rk * uy * _earthRadiusKm, + rk * uz * _earthRadiusKm, + ), + velocity: ( + (rdotk * ux + rfdotk * vx) * _earthRadiusKm / 60, + (rdotk * uy + rfdotk * vy) * _earthRadiusKm / 60, + (rdotk * uz + rfdotk * vz) * _earthRadiusKm / 60, + ), + ); + } + + /// Where the satellite appears from the ground at [utc]. + /// + /// TEME is an inertial frame, so the Earth is rotated under it by the + /// sidereal angle before the observer's position is subtracted. + Horizontal lookFrom( + DateTime utc, { + required double latitude, + required double longitude, + }) { + final state = at(utc); + final gmst = greenwichSiderealTime(utc); + final localSidereal = gmst + longitude * degrees; + + // The observer, in the same rotating-into-inertial sense. + final phi = latitude * degrees; + const flattening = 1 / 298.26; // WGS-72, to match SGP4's Earth. + final c = + 1 / + math.sqrt( + 1 + flattening * (flattening - 2) * math.pow(math.sin(phi), 2), + ); + final observerX = + _earthRadiusKm * c * math.cos(phi) * math.cos(localSidereal); + final observerY = + _earthRadiusKm * c * math.cos(phi) * math.sin(localSidereal); + final observerZ = + _earthRadiusKm * c * math.pow(1 - flattening, 2) * math.sin(phi); + + final rx = state.position.$1 - observerX; + final ry = state.position.$2 - observerY; + final rz = state.position.$3 - observerZ.toDouble(); + + // Rotate the range vector into the observer's south-east-zenith frame. + final sinPhi = math.sin(phi); + final cosPhi = math.cos(phi); + final sinTheta = math.sin(localSidereal); + final cosTheta = math.cos(localSidereal); + final south = sinPhi * cosTheta * rx + sinPhi * sinTheta * ry - cosPhi * rz; + final east = -sinTheta * rx + cosTheta * ry; + final zenith = + cosPhi * cosTheta * rx + cosPhi * sinTheta * ry + sinPhi * rz; + + final range = math.sqrt(south * south + east * east + zenith * zenith); + return Horizontal( + altitude: math.asin(zenith / range), + azimuth: turn(math.atan2(-east, south) + math.pi), + ); + } +} + +/// One visible pass. +class SatellitePass { + const SatellitePass({ + required this.rises, + required this.peaks, + required this.sets, + required this.peakAltitude, + required this.peakAzimuth, + }); + + final DateTime rises; + final DateTime peaks; + final DateTime sets; + + /// Radians. + final double peakAltitude; + final double peakAzimuth; + + Duration get length => sets.difference(rises); +} + +/// Finding passes. +abstract final class SatellitePasses { + /// Passes of [satellite] over [window] from [from] that reach at least + /// [minimumAltitude] (10° by default — lower than that and buildings win). + /// + /// Only geometric visibility: whether the satellite is above the horizon. + /// Whether it is *lit* — sunlit while the observer is in darkness — is the + /// other half, and [sunlitOnly] applies it. + static List find( + Sgp4 satellite, { + required DateTime from, + required double latitude, + required double longitude, + Duration window = const Duration(hours: 24), + double minimumAltitude = 10 * degrees, + bool sunlitOnly = true, + }) { + final passes = []; + const step = Duration(seconds: 30); + DateTime? rose; + var best = -math.pi; + var bestAt = from; + var bestAzimuth = 0.0; + + for (var at = from; at.isBefore(from.add(window)); at = at.add(step)) { + final look = satellite.lookFrom( + at, + latitude: latitude, + longitude: longitude, + ); + final visible = + look.altitude > 0 && + (!sunlitOnly || _isSunlit(satellite, at, latitude, longitude)); + if (visible) { + rose ??= at; + if (look.altitude > best) { + best = look.altitude; + bestAt = at; + bestAzimuth = look.azimuth; + } + } else if (rose != null) { + if (best >= minimumAltitude) { + passes.add( + SatellitePass( + rises: rose, + peaks: bestAt, + sets: at, + peakAltitude: best, + peakAzimuth: bestAzimuth, + ), + ); + } + rose = null; + best = -math.pi; + } + } + return passes; + } + + /// Whether the satellite is in sunlight while the ground is dark — the + /// condition that makes a pass actually visible to the eye. + static bool _isSunlit( + Sgp4 satellite, + DateTime at, + double latitude, + double longitude, + ) { + // The ground must be at least in civil twilight, or the sky outshines it. + final observer = Observer(latitude: latitude, longitude: longitude); + final sunAltitude = observer + .lookAt(SunEphemeris.at(at).equatorial, at) + .altitude; + if (sunAltitude > civilTwilight) return false; + final sun = _sunTeme(at); + + // And the satellite must be outside the Earth's shadow cylinder. + final position = satellite.at(at).position; + final dot = + position.$1 * sun.$1 + position.$2 * sun.$2 + position.$3 * sun.$3; + if (dot > 0) return true; + final distance = math.sqrt( + position.$1 * position.$1 + + position.$2 * position.$2 + + position.$3 * position.$3, + ); + final perpendicular = math.sqrt(distance * distance - dot * dot); + return perpendicular > _earthRadiusKm; + } + + /// A unit vector toward the Sun in the same frame the propagator uses. + static (double, double, double) _sunTeme(DateTime at) { + final t = julianCenturies(at); + final meanLongitude = (280.46 + 36000.77 * t) * degrees; + final meanAnomaly = (357.5277233 + 35999.05034 * t) * degrees; + final longitude = + meanLongitude + + (1.914666471 * math.sin(meanAnomaly) + + 0.019994643 * math.sin(2 * meanAnomaly)) * + degrees; + final obliquity = meanObliquity(t); + return ( + math.cos(longitude), + math.cos(obliquity) * math.sin(longitude), + math.sin(obliquity) * math.sin(longitude), + ); + } +} diff --git a/lib/core/astro/sky_position.dart b/lib/core/astro/sky_position.dart new file mode 100644 index 000000000..b66dea7c6 --- /dev/null +++ b/lib/core/astro/sky_position.dart @@ -0,0 +1,356 @@ +/// Where a body is in the sky, and when it crosses the horizon. +/// +/// One coordinate pipeline and one rise/set solver for everything. The Moon, +/// the Sun, a planet and a catalogue star differ only in how their [Equatorial] +/// position is produced and how high their horizon sits — so that is all a +/// caller supplies. Everything downstream (altitude, azimuth, rise, transit, +/// set, twilight) is shared, which is why the Sun's rise and the Moon's cannot +/// drift apart in their treatment of refraction, sidereal time or the day +/// boundary. +/// +/// Azimuth is measured **from north, increasing eastward** — the convention a +/// phone compass reports, so pointing instructions need no translation. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; + +/// Earth's equatorial radius, km — the scale of every parallax. +const double _earthRadiusKm = 6378.14; + +/// Refraction at the horizon: a body's *apparent* place is about 34′ above its +/// true one, so it is seen to rise while still geometrically below. +const double horizonRefraction = 34 * arcminutes; + +/// Standard altitude for a point source — a star or a planet. Refraction only: +/// it has no disc worth speaking of and its parallax is nil. +const double pointHorizon = -horizonRefraction; + +/// Standard altitude for sunrise and sunset: the refraction above, plus the +/// Sun's own 16′ semidiameter, because "sunrise" means the upper limb. +const double sunHorizon = -(horizonRefraction + 16 * arcminutes); + +/// Civil twilight — the Sun 6° down. Bright enough to work outdoors without +/// light, which is why this is the one that matters for an evacuation. +const double civilTwilight = -6 * degrees; + +/// Nautical twilight — 12° down; the horizon is no longer distinguishable. +const double nauticalTwilight = -12 * degrees; + +/// Astronomical twilight — 18° down; the sky is as dark as it will get. +const double astronomicalTwilight = -18 * degrees; + +/// A body's position on the celestial sphere, in the equinox of date. +class Equatorial { + const Equatorial({ + required this.rightAscension, + required this.declination, + this.distanceKm, + }); + + /// Right ascension α, radians. + final double rightAscension; + + /// Declination δ, radians. + final double declination; + + /// Distance from the Earth's centre in kilometres, where the body has a + /// meaningful one. Null for a star, whose parallax is not worth carrying. + final double? distanceKm; + + /// Builds from ecliptic longitude/latitude of date. + factory Equatorial.fromEcliptic({ + required double longitude, + required double latitude, + required double obliquity, + double? distanceKm, + }) { + final sinB = math.sin(latitude); + final cosB = math.cos(latitude); + final sinL = math.sin(longitude); + final sinE = math.sin(obliquity); + final cosE = math.cos(obliquity); + return Equatorial( + rightAscension: turn( + math.atan2(sinL * cosE - (sinB / cosB) * sinE, math.cos(longitude)), + ), + declination: math.asin(sinB * cosE + cosB * sinE * sinL), + distanceKm: distanceKm, + ); + } + + /// Angular separation from [other], radians — the quantity behind every + /// "conjunction", "elongation" and "how close will they be" question. + double separationFrom(Equatorial other) { + final d1 = declination; + final d2 = other.declination; + final cosine = + math.sin(d1) * math.sin(d2) + + math.cos(d1) * + math.cos(d2) * + math.cos(rightAscension - other.rightAscension); + return math.acos(cosine.clamp(-1.0, 1.0)); + } +} + +/// Where a body appears to an observer: how high, and which way. +class Horizontal { + const Horizontal({required this.altitude, required this.azimuth}); + + /// Altitude above the true horizon, radians. Negative is below. + final double altitude; + + /// Azimuth from north, increasing eastward, radians `[0, 2π)`. + final double azimuth; + + /// The compass point, as an index into the sixteen-way rose starting at N. + int get compassIndex => (azimuth / (2 * math.pi) * 16).round() % 16; + + /// The direction of [target] as seen from this position, measured from + /// straight up (the zenith) and increasing clockwise — which is the same as + /// increasing azimuth, because facing a bearing puts larger azimuths to your + /// right. + /// + /// This is the great-circle bearing on the horizontal sphere, with altitude + /// playing the part of latitude. Deriving orientation this way rather than + /// from a position-angle convention removes the whole class of sign errors: + /// "the crescent points at the Sun" is a fact, not a convention, and a + /// bearing computed from the two apparent places cannot get it backwards. + double bearingTo(Horizontal target) { + final deltaAzimuth = target.azimuth - azimuth; + return math.atan2( + math.cos(target.altitude) * math.sin(deltaAzimuth), + math.cos(altitude) * math.sin(target.altitude) - + math.sin(altitude) * + math.cos(target.altitude) * + math.cos(deltaAzimuth), + ); + } +} + +/// A place on the Earth. Degrees in, radians kept. +class Observer { + const Observer({required this.latitude, required this.longitude}); + + /// Latitude in **degrees**, north positive. + final double latitude; + + /// Longitude in **degrees**, east positive. + final double longitude; + + double get _phi => latitude * degrees; + + /// Local hour angle of [position] at [utc], radians, wrapped to `(-π, π]`. + /// Zero at upper transit; negative before it, positive after. + double hourAngle(Equatorial position, DateTime utc) => signedTurn( + greenwichSiderealTime(utc) + longitude * degrees - position.rightAscension, + ); + + /// Where [position] appears at [utc]. + Horizontal lookAt(Equatorial position, DateTime utc) { + final h = hourAngle(position, utc); + final sinDec = math.sin(position.declination); + final cosDec = math.cos(position.declination); + final sinPhi = math.sin(_phi); + final cosPhi = math.cos(_phi); + return Horizontal( + altitude: math.asin(sinPhi * sinDec + cosPhi * cosDec * math.cos(h)), + // Measured from north: the numerator is the east component, and the + // sign of the denominator is what puts a transiting body due south in + // the northern hemisphere rather than due north. + azimuth: turn( + math.atan2( + -cosDec * math.sin(h), + cosPhi * sinDec - sinPhi * cosDec * math.cos(h), + ), + ), + ); + } + + /// [position] as seen from this point on the surface rather than from the + /// Earth's centre (Meeus ch. 40). + /// + /// Only the Moon needs this — its parallax is nearly a degree, twice its own + /// diameter — but it is what makes a *local* solar eclipse computable + /// without Besselian elements: whether the Moon covers the Sun depends + /// entirely on where you are standing. + /// + /// The Earth's flattening is included; it moves the answer by a few + /// arcseconds, which matters when the question is whether two discs + /// 32′ across overlap. + Equatorial topocentric(Equatorial position, DateTime utc) { + final distance = position.distanceKm; + if (distance == null) return position; + final parallax = math.asin(_earthRadiusKm / distance); + + // The observer's distance from the Earth's centre, split into its + // equatorial and polar components (Meeus 11.1); sea level is assumed. + const flattening = 1 / 298.257223563; + final reduced = math.atan((1 - flattening) * math.tan(_phi)); + final rhoSin = (1 - flattening) * math.sin(reduced); + final rhoCos = math.cos(reduced); + + final h = hourAngle(position, utc); + final sinParallax = math.sin(parallax); + final denominator = + math.cos(position.declination) - rhoCos * sinParallax * math.cos(h); + final deltaRa = math.atan2( + -rhoCos * sinParallax * math.sin(h), + denominator, + ); + return Equatorial( + rightAscension: turn(position.rightAscension + deltaRa), + declination: math.atan2( + (math.sin(position.declination) - rhoSin * sinParallax) * + math.cos(deltaRa), + denominator, + ), + distanceKm: distance, + ); + } + + /// The **parallactic angle** at [position], radians — the tilt between "up" + /// on the sky (celestial north) and "up" for the observer (the zenith). + /// + /// Without it a crescent Moon is drawn with its horns pointing the way they + /// would from the north pole. At Taiwan's latitude the real crescent is + /// noticeably rolled, and near the horizon it lies almost on its back. + double parallacticAngle(Equatorial position, DateTime utc) { + final h = hourAngle(position, utc); + return math.atan2( + math.sin(h), + math.tan(_phi) * math.cos(position.declination) - + math.sin(position.declination) * math.cos(h), + ); + } +} + +/// A body's position as a function of time — the one thing each ephemeris has +/// to supply to use the shared solver. +typedef SkyTrack = Equatorial Function(DateTime utc); + +/// The altitude counted as "on the horizon" for this body at this instant. +/// A constant for the Sun and the stars; distance-dependent for the Moon, +/// whose parallax moves it by nearly a degree. +typedef HorizonAltitude = double Function(Equatorial position); + +/// Rise, transit and set within one window. +class RiseSet { + const RiseSet({this.rise, this.transit, this.set, required this.startsAbove}); + + /// When the body crosses the horizon upward, or null if it does not in the + /// window. Null is an ordinary answer — a body can be up the whole time, down + /// the whole time, or (for the Moon) simply have slipped past midnight. + final DateTime? rise; + + /// Upper transit — the highest point, and the best moment to look. + final DateTime? transit; + + /// When it crosses downward. + final DateTime? set; + + /// Whether it was already above the horizon when the window opened. This is + /// what disambiguates "never rose because it was up all day" from "never rose + /// because it was down all day". + final bool startsAbove; + + /// Neither a rise nor a set fell in the window. + bool get isAlwaysUp => rise == null && set == null && startsAbove; + + /// Below the horizon for the whole window. + bool get isAlwaysDown => rise == null && set == null && !startsAbove; + + /// Solves for the crossings of [track] over [window] starting at [from]. + /// + /// A scan, not a formula: the Moon moves half a degree an hour and the Sun's + /// declination drifts, so the geometry changes while the body crosses. The + /// sampling step has to be short enough to bracket the briefest appearance — + /// ten minutes holds even at 64°N, where the Moon skims the horizon rather + /// than cutting it. + static RiseSet solve({ + required DateTime from, + required Observer observer, + required SkyTrack track, + required HorizonAltitude horizon, + Duration window = const Duration(hours: 24), + Duration step = const Duration(minutes: 10), + }) { + double above(DateTime at) { + final position = track(at); + return observer.lookAt(position, at).altitude - horizon(position); + } + + DateTime bisect( + DateTime low, + DateTime high, + bool Function(double) isBelow, + ) { + var lo = low; + var hi = high; + for (var i = 0; i < 14; i++) { + final mid = lo.add( + Duration(microseconds: hi.difference(lo).inMicroseconds ~/ 2), + ); + if (isBelow(above(mid))) { + lo = mid; + } else { + hi = mid; + } + } + return hi; + } + + DateTime? rise; + DateTime? set; + DateTime? transit; + + final stepMinutes = step.inMinutes; + var previousAt = from; + var previousHeight = above(from); + var previousHourAngle = observer.hourAngle(track(from), from); + final startsAbove = !previousHeight.isNegative; + + for (var m = stepMinutes; m <= window.inMinutes; m += stepMinutes) { + final at = from.add(Duration(minutes: m)); + final height = above(at); + if (previousHeight.isNegative && !height.isNegative) { + rise ??= bisect(previousAt, at, (h) => h.isNegative); + } else if (!previousHeight.isNegative && height.isNegative) { + set ??= bisect(previousAt, at, (h) => !h.isNegative); + } + // Upper transit is the hour angle passing through zero. Taken on the + // signed hour angle, so the wrap from +π to -π (lower transit) is not + // mistaken for it. + final currentHourAngle = observer.hourAngle(track(at), at); + if (transit == null && + previousHourAngle < 0 && + currentHourAngle >= 0 && + currentHourAngle - previousHourAngle < math.pi) { + var lo = previousAt; + var hi = at; + for (var i = 0; i < 14; i++) { + final mid = lo.add( + Duration(microseconds: hi.difference(lo).inMicroseconds ~/ 2), + ); + if (observer.hourAngle(track(mid), mid) < 0) { + lo = mid; + } else { + hi = mid; + } + } + transit = hi; + } + previousAt = at; + previousHeight = height; + previousHourAngle = currentHourAngle; + } + + return RiseSet( + rise: rise, + transit: transit, + set: set, + startsAbove: startsAbove, + ); + } +} diff --git a/lib/core/astro/solar_terms.dart b/lib/core/astro/solar_terms.dart new file mode 100644 index 000000000..3d6e7b082 --- /dev/null +++ b/lib/core/astro/solar_terms.dart @@ -0,0 +1,141 @@ +/// The twenty-four solar terms — 二十四節氣. +/// +/// Not folklore dates: each term is the instant the Sun's apparent ecliptic +/// longitude reaches an exact multiple of 15°, starting from the vernal +/// equinox at 0°. So this is the same search the moon phases use, pointed at +/// the Sun — 春分 is *defined* as λ☉ = 0°, 冬至 as λ☉ = 270°. +/// +/// Two audiences, one calculation. The terms are the agricultural calendar +/// still printed on every Taiwanese wall calendar, and the four cardinal ones +/// are the solstices and equinoxes. They are also the backbone of the +/// lunisolar calendar in `lunisolar_calendar.dart`, whose leap-month rule is +/// stated entirely in terms of which 中氣 falls in which lunar month. +/// +/// **Measured** against the CWA's published 節氣 table in `test/core/astro/`. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// Mean days per tropical year — the coast rate for the search below. +const double tropicalYearDays = 365.242189; + +/// The twenty-four terms, in ecliptic order from the vernal equinox. +/// +/// [longitudeDegrees] is the definition, not a label: everything else about a +/// term is derived from the instant the Sun reaches it. +enum SolarTerm { + vernalEquinox(0), + pureBrightness(15), + grainRain(30), + startOfSummer(45), + grainFull(60), + grainInEar(75), + summerSolstice(90), + minorHeat(105), + majorHeat(120), + startOfAutumn(135), + endOfHeat(150), + whiteDew(165), + autumnalEquinox(180), + coldDew(195), + frostDescent(210), + startOfWinter(225), + minorSnow(240), + majorSnow(255), + winterSolstice(270), + minorCold(285), + majorCold(300), + startOfSpring(315), + rainWater(330), + awakeningOfInsects(345); + + const SolarTerm(this.longitudeDegrees); + + /// The Sun's apparent ecliptic longitude at this term, degrees. + final int longitudeDegrees; + + /// Whether this is a **中氣** (major term) — a multiple of 30°. + /// + /// The distinction is not decorative: the lunisolar leap month is the one + /// that contains no 中氣. + bool get isMajor => longitudeDegrees % 30 == 0; + + /// The four cardinal terms — the solstices and equinoxes. + bool get isCardinal => longitudeDegrees % 90 == 0; +} + +/// Finding solar terms. +abstract final class SolarTerms { + /// The next occurrence of [term] strictly after [utc]. + /// + /// The Sun's longitude advances about a degree a day and never reverses, so + /// there is nothing to bracket: coast to the target at the mean rate, then + /// re-measure and correct. The true rate strays only ±3.4% from the mean + /// (the Earth's orbital eccentricity), so each pass cuts the error thirty + /// fold and four passes land inside a second. + static DateTime next(DateTime utc, SolarTerm term) { + final target = term.longitudeDegrees * degrees; + final at = _settle(utc.add(_coast(turn(target - _longitude(utc)))), target); + // Asked from the term's own instant, the coast is zero and the search + // lands back on it; "next" then means a year later. + return at.isAfter(utc) ? at : _settle(at.add(_coast(2 * math.pi)), target); + } + + /// The term the Sun is currently in — the most recent one reached. + static SolarTerm at(DateTime utc) { + final index = (_longitude(utc) / degrees / 15).floor() % 24; + return SolarTerm.values[index]; + } + + /// The next term of any kind, with its instant. + static (SolarTerm, DateTime) upcoming(DateTime utc) { + final term = SolarTerm.values[(SolarTerm.values.indexOf(at(utc)) + 1) % 24]; + return (term, next(utc, term)); + } + + /// Every term whose instant falls in [year] (Taipei wall time is the + /// caller's business — this returns UTC instants), in chronological order. + /// + /// A term can drift either side of a year boundary, so the list is built by + /// walking forward from just before the year rather than by assuming each + /// term lands in its usual month. + static List<(SolarTerm, DateTime)> ofYear(int year, {Duration? offset}) { + final shift = offset ?? Duration.zero; + final start = DateTime.utc(year).subtract(shift); + final end = DateTime.utc(year + 1).subtract(shift); + final found = <(SolarTerm, DateTime)>[]; + for (final term in SolarTerm.values) { + var at = next(start.subtract(const Duration(days: 400)), term); + while (at.isBefore(start)) { + at = next(at, term); + } + if (at.isBefore(end)) found.add((term, at)); + } + found.sort((a, b) => a.$2.compareTo(b.$2)); + return found; + } + + /// The Sun's apparent longitude at [utc], radians. + static double _longitude(DateTime utc) => SunEphemeris.at(utc).longitude; + + static DateTime _settle(DateTime estimate, double target) { + var at = estimate; + for (var pass = 0; pass < 4; pass++) { + at = at.add(_coast(signedTurn(target - _longitude(at)))); + } + return at; + } + + /// How long the mean Sun takes to advance by [radians]. + static Duration _coast(double radians) => Duration( + milliseconds: + (radians / + (2 * math.pi) * + tropicalYearDays * + Duration.millisecondsPerDay) + .round(), + ); +} diff --git a/lib/core/astro/star_catalog.dart b/lib/core/astro/star_catalog.dart new file mode 100644 index 000000000..7c23c97a3 --- /dev/null +++ b/lib/core/astro/star_catalog.dart @@ -0,0 +1,129 @@ +/// The naked-eye sky: 5,044 stars to magnitude 6, and the constellation +/// figures that join them. +/// +/// Bundled, not fetched. A star chart that needs the network is useless on the +/// night you are out of range, which for this app is the whole point. Packed +/// binary rather than JSON: 25 KB and 3.6 KB gzipped, against 657 KB of JSON, +/// and it parses with no allocation per star. +/// +/// The packing is chosen against what the eye can resolve, not against what a +/// double can hold. Right ascension gets 16 bits over 360° (20″), declination +/// 16 bits over ±90° (10″), magnitude 8 bits over −2…8 (0.04 mag). At any +/// zoom a phone screen offers, those quantisations are far below one pixel. +/// +/// Positions are J2000 and are precessed on use, like the rest of `astro/`. +library; + +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:flutter/services.dart' show rootBundle; + +/// One catalogue star. J2000 degrees. +class CatalogStar { + const CatalogStar({ + required this.rightAscension, + required this.declination, + required this.magnitude, + }); + + final double rightAscension; + final double declination; + final double magnitude; +} + +/// A constellation figure — one stroke of the join-the-dots. +class ConstellationLine { + const ConstellationLine(this.points); + + /// J2000 `(RA, Dec)` in degrees, in stroke order. + final List<(double, double)> points; +} + +/// The bundled sky. +class StarCatalog { + const StarCatalog({required this.stars, required this.figures}); + + final List stars; + final List figures; + + /// Decodes both assets off the UI isolate — 5,000 stars is fast, but it is + /// still work that has no business on the frame thread. + static Future load() async { + final starBytes = await rootBundle.load('assets/astro/stars.bin.gz'); + final lineBytes = await rootBundle.load( + 'assets/astro/constellations.bin.gz', + ); + final decoded = await Isolate.run( + () => _decode( + starBytes.buffer.asUint8List(), + lineBytes.buffer.asUint8List(), + ), + ); + return decoded; + } + + static StarCatalog _decode(Uint8List starGz, Uint8List lineGz) { + final starData = ByteData.sublistView( + Uint8List.fromList(gzip.decode(starGz)), + ); + final count = starData.getUint32(0, Endian.little); + final stars = []; + for (var i = 0; i < count; i++) { + final offset = 4 + i * 5; + stars.add( + CatalogStar( + rightAscension: + starData.getUint16(offset, Endian.little) / 65535 * 360, + declination: + starData.getInt16(offset + 2, Endian.little) / 32767 * 90, + magnitude: starData.getUint8(offset + 4) / 25 - 2.0, + ), + ); + } + + final lineData = ByteData.sublistView( + Uint8List.fromList(gzip.decode(lineGz)), + ); + final segments = lineData.getUint32(0, Endian.little); + final figures = []; + var cursor = 4; + for (var i = 0; i < segments; i++) { + final length = lineData.getUint16(cursor, Endian.little); + cursor += 2; + final points = <(double, double)>[]; + for (var j = 0; j < length; j++) { + points.add(( + lineData.getUint16(cursor, Endian.little) / 65535 * 360, + lineData.getInt16(cursor + 2, Endian.little) / 32767 * 90, + )); + cursor += 4; + } + figures.add(ConstellationLine(points)); + } + return StarCatalog(stars: stars, figures: figures); + } + + /// A J2000 position precessed to the equinox of date. + static Equatorial precess(double ra, double dec, DateTime utc) { + final years = julianCenturies(utc) * 100; + final a = ra * degrees; + final d = dec * degrees; + return Equatorial( + rightAscension: turn( + a + + (3.07496 + 1.33621 * math.sin(a) * math.tan(d)) * + years * + 15 / + 3600 * + degrees, + ), + declination: d + 20.0431 * math.cos(a) * years / 3600 * degrees, + ); + } +} diff --git a/lib/core/astro/sun_ephemeris.dart b/lib/core/astro/sun_ephemeris.dart new file mode 100644 index 000000000..6f8a6a228 --- /dev/null +++ b/lib/core/astro/sun_ephemeris.dart @@ -0,0 +1,134 @@ +/// Where the Sun is — the other half of every question on this page. +/// +/// The Sun sets the phase of the Moon, the dates of the solar terms, the +/// length of the day and the depth of the night, so it is computed once here +/// and read everywhere, exactly as the Moon is. Meeus ch. 24 (low precision): +/// closed form, ~0.01° in longitude, which is a hundredth of the Sun's own +/// diameter and two orders finer than a rise time can be defined to. +/// +/// **Measured** against JPL Horizons over 2024–2027 in `test/core/astro/`. +/// +/// It matters beyond stargazing: civil twilight is when outdoor work stops +/// needing light, which is the number an evacuation or a search is planned +/// against. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; + +/// One astronomical unit in kilometres (IAU 2012 definition). +const double astronomicalUnitKm = 149597870.7; + +/// The Sun's radius, km — turns distance into apparent size. +const double _sunRadiusKm = 695700; + +/// The Sun's geocentric position at one instant. +class SunEphemeris { + const SunEphemeris({ + required this.longitude, + required this.distanceKm, + required this.meanAnomaly, + required this.meanLongitude, + required this.centuries, + }); + + /// Apparent geocentric ecliptic longitude λ of date, radians. Latitude is + /// under 1″ and is taken as zero — the ecliptic is, by definition, the + /// Sun's own path. + final double longitude; + + /// Earth–Sun distance, kilometres. + final double distanceKm; + + /// The Sun's mean anomaly M, radians — kept because the equation of time + /// and the seasons both want it and it is already computed. + final double meanAnomaly; + + /// Geometric mean longitude L₀, radians. + final double meanLongitude; + + /// Julian centuries of Terrestrial Time from J2000.0. + final double centuries; + + /// The Sun at [utc]. + factory SunEphemeris.at(DateTime utc) => + SunEphemeris.atCenturies(julianCenturies(utc)); + + /// The Sun at [t] Julian centuries TT — the form callers that already have + /// the time argument use, so it is never recomputed. + factory SunEphemeris.atCenturies(double t) { + final l0 = 280.46646 + 36000.76983 * t + 0.0003032 * t * t; + final m = (357.52911 + 35999.05029 * t - 0.0001537 * t * t) * degrees; + final eccentricity = 0.016708634 - 0.000042037 * t - 0.0000001267 * t * t; + // Equation of the centre: the correction from a circular orbit to the + // real elliptical one, which is why the Sun runs fast in January. + final centre = + (1.914602 - 0.004817 * t - 0.000014 * t * t) * math.sin(m) + + (0.019993 - 0.000101 * t) * math.sin(2 * m) + + 0.000289 * math.sin(3 * m); + final trueLongitude = l0 + centre; + final trueAnomaly = m + centre * degrees; + final radiusVector = + 1.000001018 * + (1 - eccentricity * eccentricity) / + (1 + eccentricity * math.cos(trueAnomaly)); + // Apparent longitude: the aberration of light (−20.5″) and the nutation + // in longitude, together about 0.006°. + final node = (125.04 - 1934.136 * t) * degrees; + final apparent = trueLongitude - 0.00569 - 0.00478 * math.sin(node); + + return SunEphemeris( + longitude: turn(apparent * degrees), + distanceKm: radiusVector * astronomicalUnitKm, + meanAnomaly: turn(m), + meanLongitude: turn(l0 * degrees), + centuries: t, + ); + } + + /// Equatorial coordinates of date. + Equatorial get equatorial => Equatorial.fromEcliptic( + longitude: longitude, + latitude: 0, + obliquity: meanObliquity(centuries), + distanceKm: distanceKm, + ); + + /// Apparent angular diameter, radians — 31.5′ in July, 32.5′ in January. + double get angularDiameter => 2 * math.asin(_sunRadiusKm / distanceKm); + + /// Apparent solar time minus mean solar time — the **equation of time** + /// (Meeus 28.3), the ±16-minute swing that makes a sundial disagree with a + /// clock and draws the analemma. + /// + /// Two causes, both visible in the terms: the Earth's orbit is elliptical + /// (so the Sun's apparent motion is uneven) and its axis is tilted (so that + /// motion projects unevenly onto the equator). + Duration get equationOfTime { + final y = math.pow(math.tan(meanObliquity(centuries) / 2), 2).toDouble(); + final eccentricity = + 0.016708634 - + 0.000042037 * centuries - + 0.0000001267 * centuries * centuries; + final value = + y * math.sin(2 * meanLongitude) - + 2 * eccentricity * math.sin(meanAnomaly) + + 4 * + eccentricity * + y * + math.sin(meanAnomaly) * + math.cos(2 * meanLongitude) - + 0.5 * y * y * math.sin(4 * meanLongitude) - + 1.25 * eccentricity * eccentricity * math.sin(2 * meanAnomaly); + // Radians of Earth rotation → seconds of clock time. + return Duration( + milliseconds: (value / degrees * 4 * Duration.millisecondsPerMinute) + .round(), + ); + } + + /// The Sun's track, for the shared rise/set solver. + static Equatorial track(DateTime utc) => SunEphemeris.at(utc).equatorial; +} diff --git a/lib/core/astro/sun_events.dart b/lib/core/astro/sun_events.dart new file mode 100644 index 000000000..8eb8aaa68 --- /dev/null +++ b/lib/core/astro/sun_events.dart @@ -0,0 +1,174 @@ +/// The shape of one day's light at one place. +/// +/// Sunrise, sunset, solar noon, the three twilights and the photographers' +/// golden and blue hours are all the same question asked at different +/// altitudes — so they are one scan of the Sun's track against six thresholds, +/// not six algorithms. +/// +/// The thresholds are conventions, and worth knowing which is which: +/// +/// * **Sunrise / sunset** — the *upper limb* on the horizon: 34′ of +/// refraction plus 16′ of semidiameter below geometric zero. +/// * **Civil twilight** (−6°) — the one that matters outside astronomy. It +/// is the boundary of being able to work outdoors without light, which is +/// what an evacuation window or a search is planned against. +/// * **Nautical** (−12°) — the horizon is no longer visible at sea. +/// * **Astronomical** (−18°) — the sky is as dark as it will get. +/// * **Golden hour** (−4° to +6°) and **blue hour** (−6° to −4°) — light +/// quality, not visibility. +/// +/// **Measured** against the US Naval Observatory and the CWA's published +/// sunrise/sunset timetable in `test/core/astro/`. +library; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// The upper end of golden hour — above this the light is ordinary daylight. +const double goldenHourTop = 6 * degrees; + +/// The boundary between golden and blue hour. +const double goldenHourBottom = -4 * degrees; + +/// One day of daylight at one place. +class SunEvents { + const SunEvents({ + required this.rise, + required this.set, + required this.noon, + required this.civilDawn, + required this.civilDusk, + required this.nauticalDawn, + required this.nauticalDusk, + required this.astronomicalDawn, + required this.astronomicalDusk, + required this.goldenMorningEnd, + required this.goldenEveningStart, + required this.blueMorningStart, + required this.blueEveningEnd, + required this.startsAbove, + required this.from, + required this.window, + }); + + /// Upper limb clears the horizon. Null above the Arctic and Antarctic + /// circles in season — polar day and polar night are answers, not errors. + final DateTime? rise; + final DateTime? set; + + /// Solar noon — the Sun's upper transit, which is *not* clock noon: the + /// equation of time moves it up to ±16 minutes across the year, plus the + /// offset between the place and its timezone meridian. + final DateTime? noon; + + final DateTime? civilDawn; + final DateTime? civilDusk; + final DateTime? nauticalDawn; + final DateTime? nauticalDusk; + final DateTime? astronomicalDawn; + final DateTime? astronomicalDusk; + + /// Golden hour runs from [blueMorningStart] up through sunrise to + /// [goldenMorningEnd], and mirrors in the evening. + final DateTime? goldenMorningEnd; + final DateTime? goldenEveningStart; + final DateTime? blueMorningStart; + final DateTime? blueEveningEnd; + + /// Whether the Sun was already up when the window opened. + final bool startsAbove; + + /// The UTC instant the window opened. + final DateTime from; + + /// The window this was solved over — how [dayLength] resolves the polar case. + final Duration window; + + /// How long the Sun is above the horizon. + /// + /// Defined even when a rise or a set is missing: polar day is the whole + /// window, polar night is none of it. A caller that just subtracted two + /// nullable times would have to invent one of those answers. + Duration get dayLength { + if (rise == null && set == null) { + return startsAbove ? window : Duration.zero; + } + if (rise == null) return set!.difference(from); + if (set == null) return from.add(window).difference(rise!); + return set!.isAfter(rise!) + ? set!.difference(rise!) + : window - rise!.difference(set!); + } + + /// The nightly window with no sunlight at all — astronomical dusk to the + /// next astronomical dawn. Null when the Sun never gets 18° down, which is + /// most of the summer above about 49° latitude. + /// + /// This is the first half of "when can I actually observe"; the other half + /// is whether the Moon is up, which the caller combines from + /// `MoonRiseSet`. + /// Null also when the window opened at midnight rather than midday: the + /// dawn found would then be *this* morning's, before the dusk, and a pair in + /// that order is not a night. Solve from noon to get a usable one. + (DateTime, DateTime)? get astronomicalNight => + astronomicalDusk != null && + astronomicalDawn != null && + astronomicalDawn!.isAfter(astronomicalDusk!) + ? (astronomicalDusk!, astronomicalDawn!) + : null; + + /// One day's events from [from] (the UTC instant the local day begins), for + /// the observer at [latitude] / [longitude] in degrees, east positive. + factory SunEvents.of( + DateTime from, { + required double latitude, + required double longitude, + Duration window = const Duration(hours: 24), + }) { + final observer = Observer(latitude: latitude, longitude: longitude); + RiseSet at(double altitude) => RiseSet.solve( + from: from, + observer: observer, + track: SunEphemeris.track, + horizon: (_) => altitude, + window: window, + ); + + final day = at(sunHorizon); + final civil = at(civilTwilight); + final nautical = at(nauticalTwilight); + final astronomical = at(astronomicalTwilight); + final goldenTop = at(goldenHourTop); + final goldenFloor = at(goldenHourBottom); + + return SunEvents( + rise: day.rise, + set: day.set, + noon: day.transit, + civilDawn: civil.rise, + civilDusk: civil.set, + nauticalDawn: nautical.rise, + nauticalDusk: nautical.set, + astronomicalDawn: astronomical.rise, + astronomicalDusk: astronomical.set, + goldenMorningEnd: goldenTop.rise, + goldenEveningStart: goldenTop.set, + blueMorningStart: goldenFloor.rise, + blueEveningEnd: goldenFloor.set, + startsAbove: day.startsAbove, + from: from, + window: window, + ); + } + + /// Where the Sun appears at [utc] — how high, and which way. + static Horizontal lookFrom( + DateTime utc, { + required double latitude, + required double longitude, + }) => Observer( + latitude: latitude, + longitude: longitude, + ).lookAt(SunEphemeris.at(utc).equatorial, utc); +} diff --git a/lib/core/astro/tidal_forcing.dart b/lib/core/astro/tidal_forcing.dart new file mode 100644 index 000000000..74ab0ee83 --- /dev/null +++ b/lib/core/astro/tidal_forcing.dart @@ -0,0 +1,225 @@ +/// The astronomical part of the tide. +/// +/// **This is not a tide table, and it does not pretend to be.** A real +/// prediction for a harbour needs that harbour's harmonic constants — the +/// ocean's local response to the forcing, which depends on the shape of the +/// coast and cannot be derived from astronomy. The CWA publishes those tables +/// and they are the authority. +/// +/// What *is* purely astronomical, and therefore computable offline from the +/// positions this package already has, is the **forcing** itself: the +/// equilibrium tide, the shape the ocean would take if it could respond +/// instantly. That gives the things a tide table's numbers do not explain — +/// +/// * **Spring and neap.** The Sun's pull is 46% of the Moon's. When they +/// line up (new and full moon) the ranges add; at the quarters they +/// partly cancel. This is a phase relationship, not a lookup. +/// * **Perigean springs.** The forcing goes as the *cube* of distance, so a +/// spring tide at lunar perigee is far stronger than one at apogee — a +/// 14% distance swing becomes a 48% swing in pull. A spring tide at +/// perigee during a storm is the combination that floods, which is why +/// this belongs in a disaster app at all. +/// +/// The two coefficients below are the standard equilibrium amplitudes: 0.358 m +/// for the Moon at mean distance and 0.164 m for the Sun. Their ratio, 0.46, +/// is the solar-to-lunar tidal ratio, and it is the one number here that can +/// be checked against a textbook without any ocean in the way. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/moon_phase.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; + +/// Equilibrium tide amplitude for the Moon at its mean distance, metres. +const double _lunarAmplitude = 0.358; + +/// The same for the Sun at 1 au. +const double _solarAmplitude = 0.164; + +/// Mean Earth–Moon distance, km — the reference the cube law scales from. +const double _meanLunarDistanceKm = 384400; + +/// How strong the tide-raising forces are, and why. +class TidalForcing { + const TidalForcing({ + required this.at, + required this.equilibriumMetres, + required this.lunarMetres, + required this.solarMetres, + required this.springNeap, + required this.lunarDistanceKm, + }); + + final DateTime at; + + /// The equilibrium tide height at this place, metres. Its *shape* over a day + /// is meaningful; its absolute value is not a water level. + final double equilibriumMetres; + + /// The two contributions, so the reason for a big tide is visible rather + /// than just its size. + final double lunarMetres; + final double solarMetres; + + /// 1 at spring (Sun and Moon aligned), 0 at neap (at right angles). + /// + /// Derived from the phase angle, because that *is* the alignment: springs + /// happen at new and full moon, neaps at the quarters. + final double springNeap; + + final double lunarDistanceKm; + + /// How much stronger the Moon's pull is than at its mean distance. The cube + /// law turns perigee into about +23% and apogee into about −18%. + double get distanceFactor => + math.pow(_meanLunarDistanceKm / lunarDistanceKm, 3).toDouble(); + + /// A spring tide near perigee — the combination that produces the highest + /// water of the year, and the one that matters alongside a storm surge. + bool get isPerigeanSpring => springNeap > 0.8 && distanceFactor > 1.15; + + /// Spring, neap, or between. + TidePhase get phase => springNeap > 0.75 + ? TidePhase.spring + : springNeap < 0.25 + ? TidePhase.neap + : TidePhase.middling; + + /// The forcing at [utc] for the observer at [latitude] / [longitude], + /// degrees, east positive. + factory TidalForcing.at( + DateTime utc, { + required double latitude, + required double longitude, + }) { + final observer = Observer(latitude: latitude, longitude: longitude); + final moon = MoonEphemeris.at(utc); + final sun = SunEphemeris.at(utc); + + // The second-degree tidal potential: (3cos²z − 1)/2, where z is the zenith + // distance. It peaks both under the body and directly opposite it, which + // is why there are two high tides a day and not one. + double potential(Equatorial position, DateTime at) { + final cosZenith = math.sin(observer.lookAt(position, at).altitude); + return (3 * cosZenith * cosZenith - 1) / 2; + } + + final lunar = + _lunarAmplitude * + math.pow(_meanLunarDistanceKm / moon.distanceKm, 3) * + potential(moon.equatorial, utc); + final solar = + _solarAmplitude * + math.pow(astronomicalUnitKm / sun.distanceKm, 3) * + potential(sun.equatorial, utc); + + // |cos| of the phase angle: 1 at new and full, 0 at the quarters. + final alignment = math.cos(moon.phaseAngle).abs(); + + return TidalForcing( + at: utc, + equilibriumMetres: (lunar + solar).toDouble(), + lunarMetres: lunar.toDouble(), + solarMetres: solar.toDouble(), + springNeap: alignment, + lunarDistanceKm: moon.distanceKm, + ); + } + + /// The highs and lows of the equilibrium tide over [window] from [from]. + /// + /// Turning points of a smooth curve, found by sampling and refining. These + /// are the times the *forcing* peaks; a real harbour lags them by a fixed + /// interval of its own (its 高潮間隙), which is exactly the piece this + /// cannot know. + static List extremes( + DateTime from, { + required double latitude, + required double longitude, + Duration window = const Duration(hours: 24), + }) { + double height(DateTime at) => TidalForcing.at( + at, + latitude: latitude, + longitude: longitude, + ).equilibriumMetres; + + final found = []; + const step = 10; + var previous = height(from); + var current = height(from.add(const Duration(minutes: step))); + for (var m = step; m < window.inMinutes; m += step) { + final at = from.add(Duration(minutes: m)); + final next = height(at.add(const Duration(minutes: step))); + final risingBefore = current > previous; + final risingAfter = next > current; + if (risingBefore != risingAfter) { + // Refine by golden-section on the bracketing interval. + var lo = at.subtract(const Duration(minutes: step)); + var hi = at.add(const Duration(minutes: step)); + for (var i = 0; i < 24; i++) { + final third = hi.difference(lo).inMilliseconds ~/ 3; + final a = lo.add(Duration(milliseconds: third)); + final b = hi.subtract(Duration(milliseconds: third)); + final better = risingBefore + ? height(a) > height(b) + : height(a) < height(b); + if (better) { + hi = b; + } else { + lo = a; + } + } + final peak = lo.add( + Duration(milliseconds: hi.difference(lo).inMilliseconds ~/ 2), + ); + found.add( + TidalExtreme(at: peak, metres: height(peak), isHigh: risingBefore), + ); + } + previous = current; + current = next; + } + return found; + } + + /// The next perigean spring tide after [utc] — the highest water of the + /// season, and the state to check a storm surge against. + static DateTime? nextPerigeanSpring(DateTime utc, {int withinDays = 400}) { + final limit = utc.add(Duration(days: withinDays)); + var syzygy = MoonPhase.nextNewMoon(utc); + var full = MoonPhase.nextFullMoon(utc); + while (syzygy.isBefore(limit) || full.isBefore(limit)) { + final next = syzygy.isBefore(full) ? syzygy : full; + final moon = MoonEphemeris.at(next); + if (math.pow(_meanLunarDistanceKm / moon.distanceKm, 3) > 1.15) { + return next; + } + if (syzygy.isBefore(full)) { + syzygy = MoonPhase.nextNewMoon(syzygy.add(const Duration(days: 1))); + } else { + full = MoonPhase.nextFullMoon(full.add(const Duration(days: 1))); + } + } + return null; + } +} + +/// A turning point of the equilibrium tide. +class TidalExtreme { + const TidalExtreme({ + required this.at, + required this.metres, + required this.isHigh, + }); + + final DateTime at; + final double metres; + final bool isHigh; +} + +/// Where in the fortnightly cycle the tide sits. +enum TidePhase { spring, middling, neap } diff --git a/lib/core/astro/tle_source.dart b/lib/core/astro/tle_source.dart new file mode 100644 index 000000000..b91443103 --- /dev/null +++ b/lib/core/astro/tle_source.dart @@ -0,0 +1,168 @@ +/// Where element sets come from, and how they stay fresh. +/// +/// Split from the propagator on purpose. `satellite.dart` is pure Dart with no +/// Flutter dependency — it can be unit-tested against the published SGP4 +/// vectors and run in an isolate — while *which* elements to use is a +/// data-freshness problem and lives here. +/// +/// Three tiers, each a real answer when the one above it is unavailable: +/// +/// 1. **Fetched** — the newest elements, at most once a day. +/// 2. **Cached** — the last successful fetch, kept in [Prefs]. +/// 3. **Bundled** — the snapshot shipped with the app. Always present, so a +/// device that has never had a network still predicts passes. +/// +/// **Why not ETag.** The obvious design is a conditional request, and it does +/// not work here: CelesTrak's `gp.php` is generated per request and returns no +/// `ETag`, no `Last-Modified` and no `Cache-Control` — an `If-None-Match` is +/// answered with a full 200 and the whole body. Measured, not assumed. +/// +/// So freshness is decided on the **epoch inside the elements** instead, which +/// is better than a validator anyway: it is the version number the data +/// already carries. Bytes can change without the orbit being newer (the file +/// is regenerated constantly), and a cache must never be replaced by *older* +/// elements, which a byte comparison cannot tell you. At 3.5 kB a day the +/// bandwidth an ETag would have saved is not worth having. +library; + +import 'package:dpip/core/astro/satellite.dart'; +import 'package:dpip/core/settings/preference_keys.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter/services.dart' show rootBundle; + +/// How long a fetched set is considered fresh. +/// +/// Elements are re-issued a few times a day, but a two-day-old set still +/// predicts an ISS pass to well inside a minute — the resolution a reader can +/// act on. Anything shorter is polling a public service for nothing, which +/// CelesTrak's usage guidelines explicitly ask clients not to do, and the +/// pass list is only ever drawn 48 hours ahead anyway. +/// +/// The trade is visible rather than buried: the page already shows the element +/// age, so a reader can see when the answer is resting on older data. +const Duration tleRefreshInterval = Duration(hours: 48); + +/// Fetches raw TLE text. Injected rather than implemented here so the +/// transport stays a seam: today the app has no route to CelesTrak, because +/// `ApiClient` is region-aware over ExpTech's own hosts and calling a foreign +/// host directly is exactly what the networking rule forbids. When the backend +/// mirrors the feed, this becomes one `ApiClient.get`. +typedef TleFetcher = Future Function(); + +/// A supply of two-line element sets. +abstract interface class TleSource { + Future> load(); +} + +/// The snapshot shipped with the app — the floor everything else falls back to. +class BundledTleSource implements TleSource { + const BundledTleSource(); + + @override + Future> load() async => TleSet.parseAll( + await rootBundle.loadString('assets/astro/satellites.tle'), + ); +} + +/// Cached elements, refreshed on a timer, never downgraded. +class CachedTleSource implements TleSource { + const CachedTleSource({ + required this.prefs, + required this.now, + this.fetch, + this.fallback = const BundledTleSource(), + this.refreshInterval = tleRefreshInterval, + }); + + final Prefs prefs; + final DateTime Function() now; + + /// Null until the app has a route to a feed; the cache and the bundle still + /// work, they simply stop getting newer. + final TleFetcher? fetch; + + final TleSource fallback; + final Duration refreshInterval; + + /// The best elements available, fetching first if the cache is stale. + /// + /// A failed fetch is not an error the caller sees: the cache — or failing + /// that the bundle — is still a usable answer, and its age is already on the + /// result for the page to show. + @override + Future> load() async { + if (_shouldRefresh) await _refresh(); + final cached = prefs.getString(PreferenceKeys.satelliteElements); + if (cached != null) { + final parsed = _parse(cached); + if (parsed.isNotEmpty) return parsed; + } + return fallback.load(); + } + + bool get _shouldRefresh { + if (fetch == null) return false; + final last = prefs.getInt(PreferenceKeys.satelliteElementsFetchedAt); + if (last == null) return true; + final since = now().difference( + DateTime.fromMillisecondsSinceEpoch(last, isUtc: true), + ); + // A clock that jumped backwards would otherwise freeze the refresh until + // it caught up. + return since.isNegative || since >= refreshInterval; + } + + Future _refresh() async { + final String text; + try { + text = await fetch!(); + } on Object { + // Leave the timestamp alone so the next call tries again rather than + // waiting out the whole interval on one failure. + return; + } + + final incoming = _parse(text); + if (incoming.isEmpty) return; + + // Only accept a genuinely newer set. The feed is regenerated constantly, + // so identical or older elements arrive routinely, and replacing a good + // cache with an older one would quietly make predictions worse. + final cached = _parse( + prefs.getString(PreferenceKeys.satelliteElements) ?? '', + ); + if (cached.isNotEmpty && !_isNewer(incoming, cached)) { + await _stamp(); + return; + } + + await prefs.setString(PreferenceKeys.satelliteElements, text); + await _stamp(); + } + + Future _stamp() => prefs.setInt( + PreferenceKeys.satelliteElementsFetchedAt, + now().toUtc().millisecondsSinceEpoch, + ); + + /// Whether [incoming] carries a later epoch than [cached] for any object + /// they share — the elements' own version number. + static bool _isNewer(List incoming, List cached) { + final previous = {for (final set in cached) set.catalogNumber: set.epoch}; + for (final set in incoming) { + final before = previous[set.catalogNumber]; + if (before == null || set.epoch.isAfter(before)) return true; + } + return false; + } + + /// Parsing must never throw here: a truncated or garbled cache is a reason + /// to fall back, not to take the page down. + static List _parse(String text) { + try { + return TleSet.parseAll(text); + } on Object { + return const []; + } + } +} diff --git a/lib/core/astro/tonight_report.dart b/lib/core/astro/tonight_report.dart new file mode 100644 index 000000000..d08392c82 --- /dev/null +++ b/lib/core/astro/tonight_report.dart @@ -0,0 +1,162 @@ +/// Everything the tonight page shows, computed off the UI thread. +/// +/// The satellite search is the reason this exists. Finding passes means +/// propagating SGP4 every thirty seconds across two days, twice over for the +/// sunlit test — tens of thousands of evaluations. Done inside `build()` that +/// is a visible freeze, and it would run again on every rebuild. `satellite.dart` +/// was deliberately kept free of Flutter so this can hand the whole search to +/// an isolate. +/// +/// The result carries its own failure mode. A page that renders nothing is +/// ambiguous — no passes tonight, still loading, and the asset failed to load +/// all look the same — so [TonightReport] distinguishes them and the page says +/// which one it is. +library; + +import 'dart:isolate'; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/deep_sky.dart'; +import 'package:dpip/core/astro/meteor_showers.dart'; +import 'package:dpip/core/astro/night_window.dart'; +import 'package:dpip/core/astro/satellite.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/tle_source.dart'; + +/// Below this a deep-sky object is in the haze and the rooftops. +const double usableAltitude = 30 * degrees; + +/// One pass, flattened for the UI. +class NamedPass { + const NamedPass({required this.name, required this.pass}); + + final String name; + final SatellitePass pass; +} + +/// A deep-sky object and how high it is at the reference time. +class TargetSighting { + const TargetSighting({required this.object, required this.altitude}); + + final DeepSkyObject object; + final double altitude; +} + +/// Tonight, resolved. +class TonightReport { + const TonightReport({ + required this.night, + required this.showers, + required this.targets, + required this.passes, + required this.elementAge, + required this.satellitesFailed, + }); + + final NightConditions night; + final List showers; + final List targets; + + /// Visible passes, soonest first. Empty is a real answer: the station can + /// spend days passing only in the Earth's shadow. + final List passes; + + /// How stale the bundled element set is. Null when it could not be read. + final Duration? elementAge; + + /// The element set could not be loaded at all — almost always a missing + /// asset after adding one without a full rebuild, which is worth saying out + /// loud rather than rendering as "no passes". + final bool satellitesFailed; + + /// Builds the whole report for [dayStart] (the UTC instant the local day + /// begins) at one place. + static Future build( + DateTime dayStart, { + required DateTime now, + required double latitude, + required double longitude, + TleSource source = const BundledTleSource(), + }) async { + final night = NightConditions.of( + dayStart, + latitude: latitude, + longitude: longitude, + ); + final observer = Observer(latitude: latitude, longitude: longitude); + final reference = + night.best?.from ?? dayStart.add(const Duration(hours: 22)); + + final showers = + MeteorShowerConditions.activeOn(now) + .map( + (shower) => MeteorShowerConditions.of( + shower, + now.year, + latitude: latitude, + longitude: longitude, + ), + ) + .toList() + ..sort((a, b) => b.visibleRate.compareTo(a.visibleRate)); + + final targets = + messierCatalogue + .map( + (object) => TargetSighting( + object: object, + altitude: observer + .lookAt(object.positionAt(reference), reference) + .altitude, + ), + ) + .where((sighting) => sighting.altitude > usableAltitude) + .toList() + ..sort((a, b) => a.object.magnitude.compareTo(b.object.magnitude)); + + List elements; + try { + elements = await source.load(); + } on Object { + return TonightReport( + night: night, + showers: showers, + targets: targets, + passes: const [], + elementAge: null, + satellitesFailed: true, + ); + } + + // The heavy part, and the only part that needs an isolate. + final found = await Isolate.run( + () => _search(elements, now, latitude, longitude), + ); + + return TonightReport( + night: night, + showers: showers, + targets: targets, + passes: found, + elementAge: elements.isEmpty ? null : elements.first.ageAt(now), + satellitesFailed: false, + ); + } + + static List _search( + List elements, + DateTime now, + double latitude, + double longitude, + ) => [ + for (final tle in elements) + for (final pass in SatellitePasses.find( + Sgp4(tle), + from: now, + latitude: latitude, + longitude: longitude, + window: const Duration(hours: 48), + )) + NamedPass(name: tle.name, pass: pass), + ]..sort((a, b) => a.pass.rises.compareTo(b.pass.rises)); +} diff --git a/lib/core/di/core_providers.dart b/lib/core/di/core_providers.dart index d53c0de6c..c3e85e2cd 100644 --- a/lib/core/di/core_providers.dart +++ b/lib/core/di/core_providers.dart @@ -3,6 +3,13 @@ import 'package:dpip/core/geo/location_monitor.dart'; import 'package:dpip/core/geo/location_service.dart'; import 'package:dpip/core/geo/town_boundaries.dart'; import 'package:dpip/core/geo/town_directory.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh_gateway.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/meshtastic/mesh_alerts.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; import 'package:dpip/core/network/api_client.dart'; import 'package:dpip/core/network/etag_cache_store.dart'; import 'package:dpip/core/network/network_usage_store.dart'; @@ -37,12 +44,19 @@ List coreProviders(SharedDeps deps) => [ ChangeNotifierProvider.value( value: deps.mapLayerOrder, ), + Provider.value(value: deps.prefs), Provider.value(value: deps.townDirectory), Provider>.value(value: deps.townBoundaries), Provider.value(value: deps.locationService), ChangeNotifierProvider.value(value: deps.locationMonitor), Provider.value(value: deps.realtimeService), Provider.value(value: deps.notificationService), + Provider.value(value: deps.meshtastic), + ChangeNotifierProvider.value(value: deps.meshLink), + ChangeNotifierProvider.value(value: deps.meshAlerts), + ChangeNotifierProvider.value(value: deps.meshNodes), + Provider.value(value: deps.meshStore), + Provider.value(value: deps.meshGateway), Provider.value(value: deps.apiClient), // Nullable — absent when the cache DB couldn't open; read by the Debug page. Provider.value(value: deps.etagCache), diff --git a/lib/core/di/shared_deps.dart b/lib/core/di/shared_deps.dart index 023b15725..c3a375efa 100644 --- a/lib/core/di/shared_deps.dart +++ b/lib/core/di/shared_deps.dart @@ -3,6 +3,12 @@ import 'package:dpip/core/geo/location_monitor.dart'; import 'package:dpip/core/geo/location_service.dart'; import 'package:dpip/core/geo/town_boundaries.dart'; import 'package:dpip/core/geo/town_directory.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh_gateway.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/meshtastic/mesh_alerts.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; import 'package:dpip/core/network/api_client.dart'; import 'package:dpip/core/network/etag_cache_store.dart'; import 'package:dpip/core/network/network_usage_store.dart'; @@ -50,6 +56,12 @@ class SharedDeps { required this.theme, required this.defaultMapLayer, required this.mapLayerOrder, + required this.meshtastic, + required this.meshLink, + required this.meshAlerts, + required this.meshNodes, + this.meshStore, + required this.meshGateway, this.etagCache, this.networkUsage, this.mapTileCache, @@ -118,6 +130,27 @@ class SharedDeps { /// User-customised map layer-picker order (also provided). final MapLayerOrderController mapLayerOrder; + /// LoRa mesh (Meshtastic) over BLE — off-grid emergency messaging. + final MeshtasticService meshtastic; + + /// Keeps the chosen radio attached across pages, drops and app restarts, and + /// provisions it for DPIP. + final MeshLink meshLink; + + /// Local notifications for mesh traffic (no push involved). + final MeshAlerts meshAlerts; + + /// The last known mesh node table — persisted, so the map and the node list + /// have something to show with no radio attached. + final MeshNodeStore meshNodes; + + /// The mesh conversation log and utilization history (SQLite). Null when + /// the database couldn't be opened — the log is then session-only. + final MeshStore? meshStore; + + /// DPIP disaster payloads in and out of the mesh — the seam feeds use. + final DpipMeshGateway meshGateway; + /// On-disk ETag HTTP cache (also provided) — null if the cache DB couldn't be /// opened. Exposed for the Debug page's cache stats. final EtagCacheStore? etagCache; diff --git a/lib/core/error/failure.dart b/lib/core/error/failure.dart index 732025732..6c9317489 100644 --- a/lib/core/error/failure.dart +++ b/lib/core/error/failure.dart @@ -36,3 +36,29 @@ final class NoDataFailure extends Failure { final class UnexpectedFailure extends Failure { const UnexpectedFailure(super.message); } + +/// A LoRa radio has no free channel slot left for DPIP's own channel. +/// +/// Its own type because the recovery is specific and human: DPIP never +/// overwrites a channel the user configured, so someone has to free a slot. +final class MeshChannelNoSlotFailure extends Failure { + const MeshChannelNoSlotFailure(super.message); +} + +/// A channel with DPIP's name already exists on the radio with a different key. +/// +/// Left for the user to resolve on purpose: writing a channel replaces the +/// whole slot, so "fixing" it would swap their key for the published default +/// one — and a licensed radio, which strips PSKs by itself, would turn that fix +/// into an endless rewrite-and-reboot loop. +final class MeshChannelConflictFailure extends Failure { + const MeshChannelConflictFailure(super.message); +} + +/// The OS refused a permission the operation needs (Bluetooth, location…). +/// +/// Distinct from other failures so a UI can guide the user to system settings +/// when the permission was permanently denied (which a plain retry can't fix). +final class PermissionDeniedFailure extends Failure { + const PermissionDeniedFailure(super.message); +} diff --git a/lib/core/geo/location_monitor.dart b/lib/core/geo/location_monitor.dart index 3afd6af41..92c0ba82d 100644 --- a/lib/core/geo/location_monitor.dart +++ b/lib/core/geo/location_monitor.dart @@ -93,7 +93,10 @@ class LocationMonitor extends ChangeNotifier with WidgetsBindingObserver { // presenting the last-known township as where the user is. Clearing the // code puts the whole app back in the "can't locate" state (nationwide map // + the header's notice) instead of a stale place. - if (wasUsable && !nowUsable) { + // The first refresh only seeds [_status] — its "previous" is the optimistic + // initial value, not a confirmed usable state, so it must never clear a + // township that a fix may already have published while it was in flight. + if (_seeded && wasUsable && !nowUsable) { _publishedCode = null; _regions.setCurrentCode(null); } diff --git a/lib/core/geo/town.freezed.dart b/lib/core/geo/town.freezed.dart index 965f6278c..fb93b0d52 100644 --- a/lib/core/geo/town.freezed.dart +++ b/lib/core/geo/town.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'town.dart'; @@ -9,6 +9,7 @@ part of 'town.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$TownCopyWithImpl<$Res> /// Create a copy of Town /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? code = null,Object? city = null,Object? town = null,Object? lat = null,Object? lng = null,Object? cityLevel = null,Object? townLevel = null,}) { - return _then(_self.copyWith( + return _then(Town( code: null == code ? _self.code : code // ignore: cast_nullable_to_non_nullable as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable as String,town: null == town ? _self.town : town // ignore: cast_nullable_to_non_nullable diff --git a/lib/core/geo/town_boundaries.dart b/lib/core/geo/town_boundaries.dart index 79f47f895..4a248370a 100644 --- a/lib/core/geo/town_boundaries.dart +++ b/lib/core/geo/town_boundaries.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:math' as math; import 'dart:typed_data'; @@ -59,7 +60,27 @@ class TownBoundaries { /// Builds from the bundled compact binary (`gzip` → delta-varint; see the /// class doc and `tool/build_town_boundaries.dart`). The bounding box is /// recomputed from the vertices rather than stored. - factory TownBoundaries.fromBinary(Uint8List bytes) { + factory TownBoundaries.fromBinary(Uint8List bytes) => + TownBoundaries.fromDecoded(_decodeTable(bytes)); + + /// Loads and decodes the bundled boundaries in a **background isolate** + /// (`gzip` → delta-varint → plain decoded map), then assembles the index on + /// the UI isolate. The binary is ~1.5 MB when inflated and pure Dart to + /// parse — doing it on the UI isolate stalls the first frames on a slow + /// phone. + static Future load() async { + final bytes = await rootBundle.load('assets/map/town_boundaries.bin.gz'); + final decoded = await Isolate.run( + () => _decodeTable( + Uint8List.fromList(gzip.decode(bytes.buffer.asUint8List())), + ), + ); + return TownBoundaries.fromDecoded(decoded); + } + + /// Parses the delta + zig-zag varint binary into the plain decoded map + /// [fromDecoded] consumes — pure data, so it runs inside [Isolate.run]. + static Map _decodeTable(Uint8List bytes) { var pos = 0; int readVarint() { var result = 0; @@ -74,7 +95,7 @@ class TownBoundaries { int unzigzag(int z) => (z & 1) == 0 ? z >> 1 : -((z + 1) >> 1); - final shapes = {}; + final towns = {}; final townCount = readVarint(); for (var t = 0; t < townCount; t++) { final codeLen = readVarint(); @@ -106,23 +127,12 @@ class TownBoundaries { } polygons.add(rings); } - shapes[code] = _TownShape( - minLng: minLng, - minLat: minLat, - maxLng: maxLng, - maxLat: maxLat, - polygons: polygons, - ); + towns[code] = { + 'b': [minLng, minLat, maxLng, maxLat], + 'p': polygons, + }; } - return TownBoundaries._(shapes, _GridIndex.build(shapes)); - } - - /// Loads and decodes the bundled boundaries (`gzip` → delta-varint binary). - static Future load() async { - final bytes = await rootBundle.load('assets/map/town_boundaries.bin.gz'); - return TownBoundaries.fromBinary( - Uint8List.fromList(gzip.decode(bytes.buffer.asUint8List())), - ); + return towns; } /// The code of the township containing ([lat], [lng]), or null if the point is diff --git a/lib/core/geo/town_directory.dart b/lib/core/geo/town_directory.dart index 06ecbcaf5..b8f3272cd 100644 --- a/lib/core/geo/town_directory.dart +++ b/lib/core/geo/town_directory.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:math' as math; import 'package:dpip/core/geo/geo_math.dart'; @@ -29,12 +30,15 @@ class TownDirectory { }), }); - /// Loads and decodes the bundled directory (`gzip` → JSON). + /// Loads and decodes the bundled directory in a background isolate + /// (`gzip` → JSON); the towns are then built on the UI isolate. static Future load() async { final bytes = await rootBundle.load('assets/location.json.gz'); - final json = - jsonDecode(utf8.decode(gzip.decode(bytes.buffer.asUint8List()))) - as Map; + final json = await Isolate.run( + () => + jsonDecode(utf8.decode(gzip.decode(bytes.buffer.asUint8List()))) + as Map, + ); return TownDirectory.fromJson(json); } diff --git a/lib/core/logging/log.dart b/lib/core/logging/log.dart index 1d0543ce0..62c4963d7 100644 --- a/lib/core/logging/log.dart +++ b/lib/core/logging/log.dart @@ -9,6 +9,10 @@ import 'package:talker_flutter/talker_flutter.dart'; /// backed by Talker, which keeps a history for the in-app log screen and /// captures uncaught Flutter/async errors. abstract final class Log { + /// Monotonic stopwatch started when the app boots — lets any code report + /// "how long after launch" (e.g. bootstrap-ready and first-frame markers). + static final Stopwatch sinceStart = Stopwatch()..start(); + /// The underlying Talker instance — used by the log screen and error hooks. static final Talker talker = Talker( settings: TalkerSettings(useConsoleLogs: kDebugMode), diff --git a/lib/core/meshtastic/data/dpip_mesh_gateway_impl.dart b/lib/core/meshtastic/data/dpip_mesh_gateway_impl.dart new file mode 100644 index 000000000..c44198484 --- /dev/null +++ b/lib/core/meshtastic/data/dpip_mesh_gateway_impl.dart @@ -0,0 +1,79 @@ +/// [DpipMeshGateway] over the BLE [MeshtasticService]. +/// +/// Thin by design: envelope in, envelope out, plus the two guards that keep +/// foreign traffic off the feed. The DPIP channel index is resolved lazily +/// through a callback because it is discovered at provisioning time (see +/// `MeshLink`) and changes when the radio changes. +library; + +import 'package:dpip/core/error/failure.dart'; +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh_gateway.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; + +class DpipMeshGatewayImpl implements DpipMeshGateway { + DpipMeshGatewayImpl(this._service, this._dpipChannel); + + final MeshtasticService _service; + + /// The provisioned DPIP channel index, or null before provisioning. + final int? Function() _dpipChannel; + + @override + Stream get inbound => _service.dataStream + .where((packet) => packet.portnum == MeshPorts.private) + .where(_onDpipChannel) + .map( + (packet) => DpipMeshCodec.decode( + packet.payload, + from: packet.from, + receivedAt: packet.timestamp, + ), + ) + .where((packet) => packet != null) + .cast() + .map((packet) { + Log.info('dpip mesh: received $packet'); + return packet; + }); + + /// Before provisioning resolves we can't tell which index is DPIP, so let + /// the envelope check be the filter; afterwards, hold the line. + bool _onDpipChannel(MeshDataPacket packet) { + final channel = _dpipChannel(); + if (channel == null || packet.channel == channel) return true; + Log.debug( + 'dpip mesh: ignoring private-port packet on channel ${packet.channel}', + ); + return false; + } + + @override + bool get isReady => _service.isConnected && _dpipChannel() != null; + + @override + Future> broadcast(DpipMeshPacket packet) async { + final channel = _dpipChannel(); + if (channel == null) { + return const Err(UnexpectedFailure('The DPIP channel is not ready')); + } + if (!_service.isConnected) { + return const Err(UnexpectedFailure('No radio connected')); + } + final List bytes; + try { + bytes = DpipMeshCodec.encode(packet); + } on ArgumentError catch (error) { + Log.warning('dpip mesh: refusing to send $packet — ${error.message}'); + return Err(UnexpectedFailure('${error.message}')); + } + Log.info('dpip mesh: broadcasting $packet on channel $channel'); + return _service.sendData( + portnum: MeshPorts.private, + payload: bytes, + channel: channel, + ); + } +} diff --git a/lib/core/meshtastic/data/mesh_log_migration.dart b/lib/core/meshtastic/data/mesh_log_migration.dart new file mode 100644 index 000000000..e3324d30c --- /dev/null +++ b/lib/core/meshtastic/data/mesh_log_migration.dart @@ -0,0 +1,49 @@ +/// One-time move of the conversation log from `SharedPreferences` to SQLite. +/// +/// The log used to be a list of JSON strings under one prefs key. Dropping it +/// would have thrown away the user's messages on upgrade, so it is imported +/// once and the key removed — which is also what makes this self-terminating: +/// after the first successful run there is nothing left to find. +library; + +import 'dart:convert'; + +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/settings/preference_keys.dart'; +import 'package:dpip/core/settings/prefs.dart'; + +Future migrateLegacyMeshLog(Prefs prefs, MeshStore store) async { + final stored = prefs.getStringList(PreferenceKeys.meshMessages); + if (stored == null || stored.isEmpty) { + // Still clear the key: an empty list is residue too. + if (stored != null) await prefs.remove(PreferenceKeys.meshMessages); + return; + } + var imported = 0; + for (final entry in stored) { + final message = _decode(entry); + if (message == null) continue; + if (await store.addMessage(message)) imported++; + } + await prefs.remove(PreferenceKeys.meshMessages); + Log.info('mesh log: migrated $imported message(s) to SQLite'); +} + +MeshStoredMessage? _decode(String encoded) { + try { + final json = jsonDecode(encoded); + if (json is! Map) return null; + return MeshStoredMessage( + from: (json['f'] as num?)?.toInt() ?? 0, + channel: (json['c'] as num?)?.toInt() ?? 0, + text: json['t'] as String? ?? '', + timestamp: DateTime.fromMillisecondsSinceEpoch( + (json['ts'] as num?)?.toInt() ?? 0, + ), + outgoing: json['o'] as bool? ?? false, + ); + } catch (_) { + return null; + } +} diff --git a/lib/core/meshtastic/data/mesh_store.dart b/lib/core/meshtastic/data/mesh_store.dart new file mode 100644 index 000000000..55552ddc2 --- /dev/null +++ b/lib/core/meshtastic/data/mesh_store.dart @@ -0,0 +1,236 @@ +/// SQLite storage for the mesh: the conversation log and the radio's +/// utilization history. +/// +/// **Not the HTTP cache database.** That one lives in the platform *cache* +/// directory, which the OS may purge whenever it wants space — correct for +/// re-fetchable bytes, wrong for a conversation. Mesh data is the opposite: it +/// exists precisely because it cannot be fetched again. So this opens its own +/// file in the application-support directory. +/// +/// SQLite rather than the prefs list the log used to be: prefs is a +/// read-whole-file/write-whole-file key-value store, so every incoming message +/// re-serialised the entire log, the whole thing lived in memory, and asking +/// for "this channel, newest 50" meant filtering in Dart. Here that is an +/// indexed query, retention is one `DELETE`, and duplicate suppression is a +/// unique index instead of a linear scan. +library; + +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:sqflite/sqflite.dart'; + +/// One line of the conversation log. +class MeshStoredMessage { + const MeshStoredMessage({ + required this.from, + required this.channel, + required this.text, + required this.timestamp, + required this.outgoing, + }); + + final int from; + final int channel; + final String text; + final DateTime timestamp; + final bool outgoing; +} + +/// One utilization sample, as the radio reported it. +class MeshMetricSample { + const MeshMetricSample({ + required this.at, + this.channelUtilization, + this.airUtilTx, + this.batteryPercent, + }); + + final DateTime at; + + /// Share of airtime the radio saw busy, and the share it spent transmitting. + final double? channelUtilization; + final double? airUtilTx; + final int? batteryPercent; +} + +class MeshStore { + MeshStore(this._db, {DateTime Function()? now}) + : _now = now ?? (() => AppTime.utc.toLocal()); + + static const String _messages = 'mesh_messages'; + static const String _metrics = 'mesh_metrics'; + + /// How long the conversation log is kept. Generous because the whole point + /// of the mesh is the times you cannot reach anything else; SQLite makes the + /// size a non-issue where the old prefs blob did not. + static const Duration messageRetention = Duration(days: 30); + + /// How long utilization samples are kept — what the chart plots. + static const Duration metricRetention = Duration(hours: 24); + + final Database _db; + final DateTime Function() _now; + + static Future createSchema(Database db) async { + await db.execute(''' + CREATE TABLE IF NOT EXISTS $_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + node INTEGER NOT NULL, + channel INTEGER NOT NULL, + text TEXT NOT NULL, + outgoing INTEGER NOT NULL DEFAULT 0 + ) + '''); + // Duplicate suppression as a constraint, not a scan: a reconnect replays + // packets the log may already hold, and `INSERT OR IGNORE` drops them at + // the storage layer. + await db.execute( + 'CREATE UNIQUE INDEX IF NOT EXISTS ${_messages}_identity ' + 'ON $_messages (node, channel, ts, text)', + ); + // The read is always "this channel, newest first". + await db.execute( + 'CREATE INDEX IF NOT EXISTS ${_messages}_channel_ts ' + 'ON $_messages (channel, ts DESC)', + ); + await db.execute(''' + CREATE TABLE IF NOT EXISTS $_metrics ( + ts INTEGER PRIMARY KEY, + channel_util REAL, + air_util REAL, + battery INTEGER + ) + '''); + } + + /// Appends [message], ignoring one the log already holds. Returns whether it + /// was new — the caller uses that to decide whether to notify or re-render. + Future addMessage(MeshStoredMessage message) async { + try { + final id = await _db.insert(_messages, { + 'ts': message.timestamp.millisecondsSinceEpoch, + 'node': message.from, + 'channel': message.channel, + 'text': message.text, + 'outgoing': message.outgoing ? 1 : 0, + }, conflictAlgorithm: ConflictAlgorithm.ignore); + return id != 0; + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store addMessage'); + return false; + } + } + + /// The newest [limit] messages, newest first; [channel] narrows to one + /// conversation. + Future> messages({ + int? channel, + int limit = 200, + }) async { + try { + final rows = await _db.query( + _messages, + where: channel == null ? null : 'channel = ?', + whereArgs: channel == null ? null : [channel], + orderBy: 'ts DESC, id DESC', + limit: limit, + ); + return [for (final row in rows) _readMessage(row)]; + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store messages'); + return const []; + } + } + + /// How many messages each channel holds — what the channel picker badges. + Future> messageCountsByChannel() async { + try { + final rows = await _db.rawQuery( + 'SELECT channel, COUNT(*) AS n FROM $_messages GROUP BY channel', + ); + return { + for (final row in rows) (row['channel']! as int): (row['n']! as int), + }; + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store counts'); + return const {}; + } + } + + Future clearMessages() async { + try { + await _db.delete(_messages); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store clearMessages'); + } + } + + /// Records a utilization sample, keyed by the moment the radio reported it + /// so the same telemetry can't be stored twice. + Future addMetric(MeshMetricSample sample) async { + try { + await _db.insert(_metrics, { + 'ts': sample.at.millisecondsSinceEpoch, + 'channel_util': sample.channelUtilization, + 'air_util': sample.airUtilTx, + 'battery': sample.batteryPercent, + }, conflictAlgorithm: ConflictAlgorithm.replace); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store addMetric'); + } + } + + /// Utilization samples inside [metricRetention], oldest first — chart order. + Future> metrics() async { + try { + final since = _now().subtract(metricRetention).millisecondsSinceEpoch; + final rows = await _db.query( + _metrics, + where: 'ts >= ?', + whereArgs: [since], + orderBy: 'ts ASC', + ); + return [ + for (final row in rows) + MeshMetricSample( + at: DateTime.fromMillisecondsSinceEpoch(row['ts']! as int), + channelUtilization: (row['channel_util'] as num?)?.toDouble(), + airUtilTx: (row['air_util'] as num?)?.toDouble(), + batteryPercent: (row['battery'] as num?)?.toInt(), + ), + ]; + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store metrics'); + return const []; + } + } + + /// Drops anything past its retention window. Cheap enough to run on every + /// app start; there is no schedule to get wrong. + Future prune() async { + try { + final now = _now(); + await _db.delete( + _messages, + where: 'ts < ?', + whereArgs: [now.subtract(messageRetention).millisecondsSinceEpoch], + ); + await _db.delete( + _metrics, + where: 'ts < ?', + whereArgs: [now.subtract(metricRetention).millisecondsSinceEpoch], + ); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh store prune'); + } + } + + MeshStoredMessage _readMessage(Map row) => MeshStoredMessage( + from: row['node']! as int, + channel: row['channel']! as int, + text: row['text']! as String, + timestamp: DateTime.fromMillisecondsSinceEpoch(row['ts']! as int), + outgoing: (row['outgoing']! as int) == 1, + ); +} diff --git a/lib/core/meshtastic/data/mesh_traffic_counter.dart b/lib/core/meshtastic/data/mesh_traffic_counter.dart new file mode 100644 index 000000000..644dc6191 --- /dev/null +++ b/lib/core/meshtastic/data/mesh_traffic_counter.dart @@ -0,0 +1,58 @@ +/// The packet counters behind [MeshtasticService.traffic]. +/// +/// Its own class so the accounting can be tested without a radio, and so the +/// transport has exactly one place that decides what counts as traffic. +library; + +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/realtime/app_time.dart'; + +class MeshTrafficCounter { + MeshTrafficCounter({DateTime Function()? now}) + : _now = now ?? (() => AppTime.utc.toLocal()); + + final DateTime Function() _now; + + int _rxPackets = 0; + int _txPackets = 0; + int _rxBytes = 0; + int _txBytes = 0; + int _rxUndecoded = 0; + final Map _rxByPort = {}; + DateTime? _lastRx; + DateTime? _lastTx; + + /// Records an inbound packet. [portnum] is null when the radio could not + /// decrypt it — those still count, because they are still proof the link is + /// delivering. + void recordRx({required int? portnum, required int bytes}) { + _rxPackets++; + _rxBytes += bytes; + _lastRx = _now(); + if (portnum == null) { + _rxUndecoded++; + return; + } + _rxByPort[portnum] = (_rxByPort[portnum] ?? 0) + 1; + } + + void recordTx({required int bytes}) { + _txPackets++; + _txBytes += bytes; + _lastTx = _now(); + } + + /// An immutable view for the UI. Counters are **session** totals — they + /// deliberately survive a reconnect, so a link that keeps dropping doesn't + /// keep resetting the evidence of it. + MeshTraffic get snapshot => MeshTraffic( + rxPackets: _rxPackets, + txPackets: _txPackets, + rxBytes: _rxBytes, + txBytes: _txBytes, + rxUndecoded: _rxUndecoded, + rxByPort: Map.unmodifiable(_rxByPort), + lastRx: _lastRx, + lastTx: _lastTx, + ); +} diff --git a/lib/core/meshtastic/data/meshtastic_client_impl.dart b/lib/core/meshtastic/data/meshtastic_client_impl.dart new file mode 100644 index 000000000..023a6bdde --- /dev/null +++ b/lib/core/meshtastic/data/meshtastic_client_impl.dart @@ -0,0 +1,783 @@ +/// BLE transport for [MeshtasticService] backed by the `meshtastic_flutter` +/// package (`MeshtasticClient` over `flutter_blue_plus`). +/// +/// Owns every mapping between the package's types and the domain models in +/// `domain/meshtastic_service.dart`, and converts transport exceptions into +/// typed [Failure]s — no `MeshtasticException` or protobuf type leaks above +/// this file. The client is created lazily on first use so importing the +/// transport costs nothing at startup; a device handle maps by its BLE +/// `remoteId`, which [scanForDevices] captures while scanning. +/// +/// `meshtastic_flutter`'s own `initialize()` requests all four permissions in +/// a chain and throws on the first denial — with no way to tell a one-off +/// denial from a permanently-denied one. So this impl requests the +/// permissions itself first and maps a denial to a typed +/// [PermissionDeniedFailure]; once granted, the package's re-request is a +/// no-op. +library; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:dpip/core/error/failure.dart'; +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/data/mesh_traffic_counter.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/platform/device_info.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform; +import 'package:flutter_blue_plus/flutter_blue_plus.dart' + show BluetoothAdapterState, BluetoothDevice, FlutterBluePlus, Guid; +import 'package:logging/logging.dart' as logging; +import 'package:meshtastic_flutter/meshtastic_flutter.dart' as mesh; +import 'package:permission_handler/permission_handler.dart' as ph; + +/// The Meshtastic BLE service, needed to ask iOS which peripherals are already +/// connected (it refuses an unfiltered query for privacy reasons). +const String _meshtasticServiceUuid = '6ba1b218-15a8-461f-9fa8-5dcae273eafd'; + +/// Channel slots every Meshtastic radio reports (`MAX_NUM_CHANNELS`). +const int _channelSlots = 8; + +/// Production impl: talks BLE to a Meshtastic radio. +class MeshtasticClientImpl implements MeshtasticService { + mesh.MeshtasticClient? _client; + bool _initialized = false; + int? _cachedSdk; + bool _logBridgeInstalled = false; + final Map _devicesById = {}; + String? _lastBridgedMessage; + int _lastBridgedCount = 0; + MeshConnectionStatus? _lastConnectionLog; + + final StreamController _trafficController = + StreamController.broadcast(); + final MeshTrafficCounter _counter = MeshTrafficCounter(); + + mesh.MeshtasticClient get _c { + _installLogBridge(); + final existing = _client; + if (existing != null) return existing; + final created = mesh.MeshtasticClient(); + _client = created; + // Counted here, from one permanent subscription — not inside the mapped + // public streams, which run once per listener (and not at all when nobody + // is listening). + // Never cancelled: the client lives as long as the app does, and the + // counters are session totals. + created.packetStream.listen(_countRx); + return created; + } + + void _countRx(mesh.MeshPacketWrapper packet) { + final decoded = packet.decoded; + _counter.recordRx( + portnum: decoded?.portnum.value, + bytes: decoded?.payload.length ?? packet.encrypted?.length ?? 0, + ); + _publishTraffic(); + } + + void _countTx(int bytes) { + _counter.recordTx(bytes: bytes); + _publishTraffic(); + } + + void _publishTraffic() { + if (_trafficController.hasListener) _trafficController.add(traffic); + } + + /// Bridges the package's `package:logging` records into [Log], so its + /// per-step BLE telemetry (connect, service discovery, config download, + /// packet TX/RX) shows up in the app log. + /// + /// Records below INFO are dropped: the package logs every raw `FromRadio` + /// protobuf dump (and every `/prefs/*.proto` file header) at FINE — with the + /// config download reading ~20 packets in 1.5 s that floods the log with + /// near-identical blocks. INFO keeps one line per node/config step. + /// + /// Two repeat-suppressions on top: identical consecutive records collapse + /// to `(×N)` (the config download emits `Received Config`/`ModuleConfig` + /// once per packet), and the per-advertisement scan lines are dropped + /// because this impl logs the same discovery once. + void _installLogBridge() { + if (_logBridgeInstalled) return; + _logBridgeInstalled = true; + logging.Logger.root.level = logging.Level.INFO; + logging.Logger.root.onRecord.listen(_handleLogRecord); + } + + void _handleLogRecord(logging.LogRecord record) { + final text = record.message; + if (text.startsWith('Found Meshtastic device:') || + text.startsWith('Scanning for Meshtastic devices')) { + return; // covered by `meshtastic scan: found/starting` from this impl + } + final message = '[meshtastic] ${record.loggerName}: $text'; + if (message == _lastBridgedMessage) { + _lastBridgedCount++; + return; + } + if (_lastBridgedCount > 1) { + Log.info('$_lastBridgedMessage (×$_lastBridgedCount)'); + } + _lastBridgedMessage = message; + _lastBridgedCount = 1; + switch (record.level) { + case >= logging.Level.SEVERE: + Log.error(message); + case >= logging.Level.WARNING: + Log.warning(message); + default: + Log.info(message); + } + } + + @override + Future> initialize() async { + if (_initialized) return const Ok(null); + Log.debug('meshtastic initialize: requesting permissions'); + try { + final denied = await _ensurePermissions(); + if (denied != null) { + Log.warning( + 'meshtastic initialize: ${denied.permission} denied' + '${denied.permanent ? ' (permanent)' : ''}', + ); + return Err( + PermissionDeniedFailure( + denied.permanent + ? '${denied.permission} is permanently denied — enable it ' + 'in system settings, then scan again' + : '${denied.permission} was denied — allow it and scan again', + ), + ); + } + // Never call the package's own `initialize()`: it re-requests all four + // Android permissions, and on iOS `permission_handler` hardcodes + // bluetoothConnect/bluetoothScan as permanently denied, so it always + // throws there. Permissions are handled above; do its remaining + // environment checks here. + if (!await FlutterBluePlus.isSupported) { + Log.warning('meshtastic initialize: bluetooth not supported'); + return const Err( + UnexpectedFailure('Bluetooth is not supported on this device'), + ); + } + final adapter = await _ensureAdapterReady(); + if (adapter != null) return Err(adapter); + _initialized = true; + Log.info('meshtastic initialize: ready'); + return const Ok(null); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic initialize'); + return Err(_mapFailure(error)); + } + } + + /// Requests the runtime permissions the radio needs, one at a time, so a + /// denial (or a permanent denial) is attributed precisely instead of the + /// package's blanket throw. Returns the first un-granted permission, or + /// `null`. + /// + /// **Android only.** Which permissions are runtime permissions depends on + /// the API level, and asking for the wrong ones fails in ways the user + /// cannot fix: + /// + /// - API 31+: `BLUETOOTH_CONNECT` / `BLUETOOTH_SCAN`. `BLUETOOTH` is + /// declared `maxSdkVersion="30"`, so on 31+ the platform drops it from the + /// manifest and `permission_handler` reports it **denied with no dialog** — + /// asking for it (as this used to, first in the list) bricks mesh on every + /// modern Android device. Location is not needed either: `BLUETOOTH_SCAN` + /// is declared `neverForLocation`. + /// - API ≤30: `BLUETOOTH` / `BLUETOOTH_ADMIN` are install-time permissions + /// and cannot be requested; a BLE scan there really does need location. + /// + /// iOS goes through CoreBluetooth instead — see [_ensureAdapterReady]. + Future<({String permission, bool permanent})?> _ensurePermissions() async { + if (defaultTargetPlatform != TargetPlatform.android) return null; + final sdk = await _androidSdk(); + final required = sdk >= 31 + ? const [ph.Permission.bluetoothConnect, ph.Permission.bluetoothScan] + : const [ph.Permission.locationWhenInUse]; + for (final permission in required) { + final status = await permission.request(); + Log.debug('meshtastic permission $permission → $status'); + if (!status.isGranted) { + return ( + permission: permission.toString(), + permanent: status.isPermanentlyDenied, + ); + } + } + return null; + } + + /// Waits for a usable Bluetooth adapter, or returns why it isn't. + /// + /// This is also **iOS's authorisation check**, on purpose. `permission_ + /// handler`'s iOS Bluetooth strategy reads `CBCentralManager.authorization` + /// without ever triggering the system prompt, and maps `notDetermined` to + /// *denied* — so asking it before any CoreBluetooth work (which is when a + /// fresh install always is) reports a denial the user was never given the + /// chance to grant. Instead we let CoreBluetooth itself answer: the adapter + /// settles on `unauthorized` only after a real refusal, and the prompt is + /// raised by the first scan/connect. + Future _ensureAdapterReady() async { + // The state settles asynchronously — CBCentralManager starts at `unknown` + // and reports `on` only after init, so a bare `.first` reads too early and + // falsely reports Bluetooth off. + final BluetoothAdapterState state; + try { + state = await FlutterBluePlus.adapterState + .firstWhere((s) => s != BluetoothAdapterState.unknown) + .timeout(const Duration(seconds: 8)); + } on TimeoutException { + Log.warning('meshtastic initialize: adapter state never settled'); + return const UnexpectedFailure( + 'Bluetooth is not responding — check it is enabled and allowed for ' + 'this app, then try again', + ); + } + Log.debug('meshtastic initialize: adapter state = $state'); + return switch (state) { + BluetoothAdapterState.on => null, + // iOS-only state: the user refused the Bluetooth prompt. Telling them to + // "turn Bluetooth on" would be wrong — it already is. + BluetoothAdapterState.unauthorized => const PermissionDeniedFailure( + 'Bluetooth access is off for this app — enable it in system settings', + ), + _ => const UnexpectedFailure('Bluetooth is not enabled'), + }; + } + + /// Android API level, cached. Falls back to 31 (the modern permission model) + /// if the platform channel can't answer — the safer guess, because asking + /// for `BLUETOOTH` on a modern device is unrecoverable while asking for + /// CONNECT/SCAN on an old one merely prompts. + Future _androidSdk() async { + if (_cachedSdk != null) return _cachedSdk!; + try { + final details = await DeviceInfoService.load(); + return _cachedSdk = details.sdkInt ?? 31; + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic android sdk'); + return _cachedSdk = 31; + } + } + + @override + Stream scanForDevices({ + Duration timeout = const Duration(seconds: 10), + }) async* { + final init = await initialize(); + if (init case Err(:final failure)) { + throw StateError(failure.message); + } + Log.info('meshtastic scan: starting (timeout ${timeout.inSeconds}s)'); + try { + // The package re-advertises each radio on every scan batch — report a + // device once per scan, or the log and UI get the same radio N times. + final seen = {}; + await for (final device in _c.scanForDevices(timeout: timeout)) { + final id = device.remoteId.toString(); + if (!seen.add(id)) continue; + _devicesById[id] = device; + Log.info('meshtastic scan: found "${device.platformName}" ($id)'); + yield MeshDevice(id: id, name: device.platformName); + } + Log.info('meshtastic scan: finished'); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic scan'); + rethrow; + } + } + + @override + Future> connect(MeshDevice device) async { + // A device from a scan carries its handle; anything else (a saved radio + // being picked back up) goes through the id path. + final handle = _devicesById[device.id]; + if (handle == null) return connectToId(device.id); + Log.info( + 'meshtastic connect: connecting to "${device.name}" (${device.id})', + ); + return _connect(() => _c.connectToDevice(handle), device.id); + } + + @override + Future> connectToId(String id) { + Log.info('meshtastic connect: connecting by id ($id)'); + return _connect(() => _c.connectToId(id), id); + } + + Future> _connect( + Future Function() attempt, + String id, + ) async { + // Permissions and a settled, powered-on adapter first: a connect issued + // before CoreBluetooth is up fails in a way that looks like a missing + // radio. Matters most on the bootstrap reconnect, which runs while the + // Bluetooth stack is still starting. + final init = await initialize(); + if (init case Err()) return init; + try { + // A link this process still holds (a previous session, a half-torn-down + // connection) makes the GATT calls fail in confusing ways — drop it + // first. A link held by *another app* can't be dropped from here; that + // is what `linkOwner` reports so the UI can tell the user. + await _dropOurLink(id); + await attempt(); + Log.info('meshtastic connect: done'); + return const Ok(null); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic connect'); + return Err(_mapFailure(error)); + } + } + + Future _dropOurLink(String id) async { + for (final device in FlutterBluePlus.connectedDevices) { + if (device.remoteId.toString() != id) continue; + Log.info('meshtastic connect: dropping our stale link to $id'); + try { + await device.disconnect(); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic drop stale link'); + } + } + } + + @override + Future linkOwner(String deviceId) async { + if (FlutterBluePlus.connectedDevices.any( + (d) => d.remoteId.toString() == deviceId, + )) { + return MeshLinkOwner.thisApp; + } + try { + // Both platforms report links opened by *any* app here (iOS needs the + // service filter for privacy; Android ignores it). + final system = await FlutterBluePlus.systemDevices([ + Guid(_meshtasticServiceUuid), + ]); + final held = system.any((d) => d.remoteId.toString() == deviceId); + if (held) { + Log.warning('meshtastic: $deviceId is already held by another app'); + } + return held ? MeshLinkOwner.otherApp : MeshLinkOwner.free; + } catch (error, stackTrace) { + // Never let a diagnostic block a connection attempt. + Log.handle(error, stackTrace, 'meshtastic linkOwner'); + return MeshLinkOwner.free; + } + } + + @override + Future> disconnect() async { + try { + await _c.disconnect(); + return const Ok(null); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic disconnect'); + return Err(_mapFailure(error)); + } + } + + @override + Future> sendText(String text, {int channel = 0}) async { + try { + final bytes = utf8.encode(text); + Log.info('meshtastic send: channel=$channel bytes=${bytes.length}'); + await _c.sendTextMessage(text, channel: channel); + _countTx(bytes.length); + Log.info('meshtastic send: done'); + return const Ok(null); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic sendText'); + return Err(_mapFailure(error)); + } + } + + @override + Stream get connectionStream => _c.connectionStream.map(( + status, + ) { + final state = switch (status.state) { + mesh.MeshtasticConnectionState.disconnected => + MeshConnectionState.disconnected, + mesh.MeshtasticConnectionState.connecting => + MeshConnectionState.connecting, + mesh.MeshtasticConnectionState.configuring => + MeshConnectionState.configuring, + mesh.MeshtasticConnectionState.connected => MeshConnectionState.connected, + mesh.MeshtasticConnectionState.error => MeshConnectionState.error, + }; + final mapped = MeshConnectionStatus( + state: state, + deviceName: status.deviceName, + errorMessage: status.errorMessage, + ); + // The package emits `disconnected` twice back-to-back on connect (its + // own disconnect() plus the BLE state listener) — log the transition + // once; the UI still receives every event. + final last = _lastConnectionLog; + if (last == null || + last.state != mapped.state || + last.errorMessage != mapped.errorMessage) { + _lastConnectionLog = mapped; + Log.info( + 'meshtastic connection: ${state.name}' + '${status.deviceName != null ? ' (${status.deviceName})' : ''}' + '${status.errorMessage != null ? ' — ${status.errorMessage}' : ''}', + ); + } + return mapped; + }); + + @override + Stream get nodeStream => _c.nodeStream.map((node) { + return MeshNode( + num: node.num, + displayName: node.displayName, + isOnline: node.isOnline, + batteryLevel: node.batteryLevel, + lastHeard: node.lastHeard, + latitude: node.latitude, + longitude: node.longitude, + snr: node.snr, + viaMqtt: node.viaMqtt, + ); + }); + + @override + Stream get messageStream => + _c.packetStream.where((packet) => packet.isTextMessage).map((packet) { + final payload = packet.decoded?.payload ?? const []; + final text = utf8.decode(payload, allowMalformed: true); + // `bytes` distinguishes "the radio sent an empty body" from "the body + // arrived but the UI didn't render it" — a blank row is otherwise + // indistinguishable from a rendering bug. + Log.info( + 'meshtastic message: from #${packet.from.toRadixString(16)} ' + 'ch=${packet.channel} bytes=${payload.length} rxTime=${packet.rxTime} ' + '"$text"', + ); + return MeshMessage( + from: packet.from, + channel: packet.channel, + // utf8, not String.fromCharCodes — multi-byte CJK survives. + text: text, + // A radio with no time source stamps `rxTime` 0; that would date a + // message to 1970 instead of "just now" — most visible on the + // backlog replayed after a reconnect. + timestamp: packet.rxTime > 0 + ? DateTime.fromMillisecondsSinceEpoch(packet.rxTime * 1000) + : AppTime.utc.toLocal(), + ); + }); + + @override + Stream get dataStream => + _c.packetStream.where((packet) => packet.decoded != null).map((packet) { + return MeshDataPacket( + from: packet.from, + channel: packet.channel, + portnum: packet.decoded!.portnum.value, + payload: packet.decoded!.payload, + timestamp: packet.rxTime > 0 + ? DateTime.fromMillisecondsSinceEpoch(packet.rxTime * 1000) + : AppTime.utc.toLocal(), + ); + }); + + @override + Future> sendData({ + required int portnum, + required List payload, + int channel = 0, + bool wantAck = false, + }) async { + final port = mesh.PortNum.valueOf(portnum); + if (port == null) { + return Err(UnexpectedFailure('Unknown Meshtastic port $portnum')); + } + if (payload.length > MeshPorts.maxPayloadBytes) { + return Err( + UnexpectedFailure( + 'Payload of ${payload.length} B exceeds the ' + '${MeshPorts.maxPayloadBytes} B mesh frame', + ), + ); + } + try { + await _c.sendData( + portnum: port, + payload: payload, + channel: channel, + wantAck: wantAck, + ); + _countTx(payload.length); + return const Ok(null); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic sendData'); + return Err(_mapFailure(error)); + } + } + + @override + MeshTraffic get traffic => _counter.snapshot; + + @override + Stream get trafficStream => _trafficController.stream; + + @override + MeshRadioInfo? get radioInfo { + final nodeNum = _c.myNodeNum; + if (nodeNum == null) return null; + final node = _c.localNode; + // Live telemetry first: the node-DB copy is a snapshot from connect time + // and is regularly stale or absent for the radio's own entry, which is + // exactly the reading a user checks. + final metrics = _c.metricsFor(nodeNum) ?? node?.deviceMetrics; + final metadata = _c.metadata; + final lora = _c.loraConfig; + return MeshRadioInfo( + nodeNum: nodeNum, + longName: node?.longName ?? _c.localUser?.longName, + shortName: node?.shortName ?? _c.localUser?.shortName, + hardware: (metadata?.hwModel ?? node?.hwModel)?.name, + firmware: metadata?.firmwareVersion, + role: (metadata?.role ?? node?.role)?.name, + region: region, + modemPreset: lora?.modemPreset.name, + hopLimit: lora?.hopLimit, + txPower: lora?.txPower, + batteryPercent: metrics?.hasBatteryLevel() ?? false + ? metrics!.batteryLevel + : null, + voltage: metrics?.hasVoltage() ?? false ? metrics!.voltage : null, + channelUtilization: metrics?.hasChannelUtilization() ?? false + ? metrics!.channelUtilization + : null, + airUtilTx: metrics?.hasAirUtilTx() ?? false ? metrics!.airUtilTx : null, + uptime: metrics?.hasUptimeSeconds() ?? false + ? Duration(seconds: metrics!.uptimeSeconds) + : null, + metricsAt: _c.metricsAgeFor(nodeNum), + isLicensed: node?.isLicensed ?? false, + hasWifi: metadata?.hasWifi ?? false, + hasBluetooth: metadata?.hasBluetooth ?? false, + ); + } + + @override + List get channels => [ + for (final (index, channel) in _c.channels.indexed) + MeshChannel( + index: index, + name: channel.settings.name, + psk: channel.settings.psk, + enabled: channel.role != mesh.Channel_Role.DISABLED, + ), + ]; + + @override + String? get region { + final code = _c.loraConfig?.region; + if (code == null) return null; + // The two values anything branches on are pinned to literals rather than + // `enum.name`: protobuf can be built with `protobuf.omit_enum_names`, which + // would make every name empty and every radio look mis-regioned. + return switch (code) { + mesh.Config_LoRaConfig_RegionCode.TW => 'TW', + mesh.Config_LoRaConfig_RegionCode.UNSET => 'UNSET', + _ => code.name, + }; + } + + @override + int? get myNodeNum => _c.myNodeNum; + + @override + Future> ensureChannel(MeshChannelSpec spec) async { + if (!isConnected) { + return const Err(UnexpectedFailure('Radio not connected')); + } + final table = channels; + // The firmware always sends all 8 slots during the config download, so a + // short table means the download was cut off. Choosing a "free" slot from + // a partial view could hand out one that is actually in use — and would + // report "no free slot" on a radio that has five. + if (table.length < _channelSlots) { + Log.warning('meshtastic channel: only ${table.length} slots known'); + return const Err( + UnexpectedFailure('The radio has not reported its channels yet'), + ); + } + + for (final channel in table) { + if (!channel.enabled || channel.name != spec.name) continue; + if (_samePsk(channel.psk, spec.psk)) { + Log.info( + 'meshtastic channel: "${spec.name}" ready at ${channel.index}', + ); + return Ok(channel.index); + } + // Same name, different key. **Never** rewrite it: a `set_channel` is a + // whole-struct overwrite, so this would replace the user's key with the + // published default one (cutting them off from their own mesh and + // exposing it), and at index 0 it would also demote their primary + // channel. A licensed radio also strips PSKs by itself, which would turn + // "fix it up" into an endless rewrite-and-reboot loop. Report it. + Log.warning( + 'meshtastic channel: "${spec.name}" at ${channel.index} has a ' + 'different key — leaving it alone', + ); + return Err( + MeshChannelConflictFailure( + 'A channel named "${spec.name}" already exists on the radio with a ' + 'different key — rename or remove it, or set its key to AQ==', + ), + ); + } + + // Index 0 is the user's primary channel and is never a candidate. + for (final channel in table) { + if (channel.index == 0 || channel.enabled) continue; + return _writeChannel(channel.index, spec); + } + Log.warning('meshtastic channel: no free slot for "${spec.name}"'); + return const Err( + MeshChannelNoSlotFailure('The radio has no free channel slot'), + ); + } + + /// Writes one **free** channel slot, then reads it back — a write the radio + /// silently dropped must not look like success. + /// + /// Deliberately *not* wrapped in a `begin_edit_settings` / + /// `commit_edit_settings` transaction, even though the reference Python + /// client uses one. `commit_edit_settings` makes the firmware call + /// `disableBluetooth()` and reboot, so the link would be gone before any + /// confirmation could arrive and every first-time provision would look like + /// a failure. A bare `set_channel` already persists on its own + /// (`saveChanges(SEGMENT_CHANNELS, false)` — save, no reboot) and leaves the + /// link up, which is what makes the read-back possible at all. It also can't + /// leave a half-open transaction behind, which would silently suppress every + /// later config save on that radio — including the official app's. + Future> _writeChannel(int index, MeshChannelSpec spec) async { + StreamSubscription? replies; + try { + Log.info('meshtastic channel: writing "${spec.name}" to slot $index'); + final confirmation = Completer(); + replies = _c.adminStream + .where((m) => m.hasGetChannelResponse()) + .map((m) => m.getChannelResponse) + .where((c) => c.index == index) + .listen((c) { + if (!confirmation.isCompleted) confirmation.complete(c); + }); + + await _c.sendAdmin( + mesh.AdminMessage( + setChannel: mesh.Channel( + index: index, + role: mesh.Channel_Role.SECONDARY, + settings: mesh.ChannelSettings(name: spec.name, psk: spec.psk), + ), + ), + ); + // `get_channel_request` is 1-based: 1 asks for index 0. A radio in + // managed mode drops local admin without replying, so the timeout is + // also how that is detected. + await _c.sendAdmin( + mesh.AdminMessage(getChannelRequest: index + 1), + wantResponse: true, + ); + final written = await confirmation.future.timeout( + const Duration(seconds: 8), + ); + if (written.settings.name != spec.name || + !_samePsk(written.settings.psk, spec.psk)) { + Log.warning('meshtastic channel: slot $index did not take the write'); + return const Err( + UnexpectedFailure('The radio did not accept the channel'), + ); + } + // Keep the cached table honest, or the next provisioning pass in this + // session would see the pre-write state and write the same slot again. + _c.cacheChannel(written); + Log.info('meshtastic channel: "${spec.name}" created at $index'); + return Ok(index); + } on TimeoutException { + Log.warning('meshtastic channel: no confirmation from the radio'); + return const Err( + UnexpectedFailure( + 'The radio did not confirm the channel — it may be in managed mode', + ), + ); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic ensureChannel'); + return Err(_mapFailure(error)); + } finally { + await replies?.cancel(); + } + } + + @override + Future> applyRegion(String region) async { + if (!isConnected) { + return const Err(UnexpectedFailure('Radio not connected')); + } + final code = region == 'TW' + ? mesh.Config_LoRaConfig_RegionCode.TW + : mesh.Config_LoRaConfig_RegionCode.values + .where((r) => r.name == region) + .firstOrNull; + if (code == null) { + return Err(UnexpectedFailure('Unknown LoRa region "$region"')); + } + // Read-modify-write, and **only** that. `set_config(lora)` replaces the + // whole LoRaConfig struct, so writing a default-constructed one would ship + // `tx_enabled = false` with no modem parameters — a radio that cannot + // transmit, recoverable only over serial. If we never read the radio's + // config, we have nothing safe to modify. + final current = _c.loraConfig; + if (current == null) { + Log.warning('meshtastic region: refusing to write without a base config'); + return const Err( + UnexpectedFailure('The radio has not reported its LoRa settings yet'), + ); + } + try { + // No read-back: the firmware disables Bluetooth and reboots as soon as + // it applies a radio-parameter change, so the link is gone before any + // confirmation could arrive. The reconnect verifies it instead. + Log.warning('meshtastic region: setting $region — the radio will reboot'); + await _c.sendAdmin( + mesh.AdminMessage( + setConfig: mesh.Config(lora: current.deepCopy()..region = code), + ), + ); + return const Ok(null); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'meshtastic applyRegion'); + return Err(_mapFailure(error)); + } + } + + bool _samePsk(List a, List b) => + a.length == b.length && !a.indexed.any((e) => b[e.$1] != e.$2); + + @override + bool get isConnected => _c.isConnected && _c.isConfigured; + + /// Maps any transport error to a user-surfaced [Failure]. + Failure _mapFailure(Object error) => switch (error) { + mesh.MeshtasticException(:final message) => UnexpectedFailure(message), + _ => UnexpectedFailure('$error'), + }; +} diff --git a/lib/core/meshtastic/domain/dpip_mesh.dart b/lib/core/meshtastic/domain/dpip_mesh.dart new file mode 100644 index 000000000..66ea78062 --- /dev/null +++ b/lib/core/meshtastic/domain/dpip_mesh.dart @@ -0,0 +1,188 @@ +/// The DPIP-over-Meshtastic wire contract: what a DPIP packet looks like on +/// the mesh, and the channel it travels on. +/// +/// DPIP rides `PRIVATE_APP` (256), never `TEXT_MESSAGE_APP` — disaster +/// payloads must never land in anyone's chat, and other Meshtastic clients +/// must be able to ignore them by port alone. +/// +/// The frame is deliberately tiny. One LoRa frame carries +/// [MeshPorts.maxPayloadBytes]; at long-range presets airtime is the scarcest +/// resource on the mesh and a node may only transmit a few percent of the +/// time, so an envelope of five bytes is the whole budget that can be spent on +/// framing: +/// +/// ```text +/// ┌──────┬──────┬─────────┬──────┬────────┬───────────────────────────┐ +/// │ 'D' │ 'P' │ version │ kind │ schema │ body (≤ maxBodyBytes) │ +/// └──────┴──────┴─────────┴──────┴────────┴───────────────────────────┘ +/// 0 1 2 3 4 5… +/// ``` +/// +/// - `version` versions **this envelope**. A receiver drops an envelope it +/// doesn't know rather than guessing. +/// - `kind` says which disaster feed the body belongs to ([DpipMeshKind]). +/// - `schema` versions **that kind's body** independently, so one feed's +/// payload can evolve without a flag day across the mesh. +/// +/// The transport never fragments: a body that doesn't fit is that kind's +/// problem to solve (send a digest and a fetch id, not the whole report). +library; + +import 'dart:typed_data'; + +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; + +/// Which DPIP feed a packet carries. The numbers are **wire codes** — they are +/// the protocol, so never renumber one; retire it and add a new code. +enum DpipMeshKind { + /// Earthquake early warning. + eew(1), + + /// Earthquake report (post-event). + report(2), + + /// Tsunami advisory. + tsunami(3), + + /// Weather alert. + weather(4), + + /// Liveness probe — carries no meaning beyond "a DPIP node is here". + ping(9); + + const DpipMeshKind(this.code); + + /// The byte written to the wire. + final int code; + + /// The kind for a wire [code], or null when this build doesn't know it (a + /// newer app on the mesh sending a feed we don't handle yet). + static DpipMeshKind? fromCode(int code) => + DpipMeshKind.values.where((k) => k.code == code).firstOrNull; +} + +/// One DPIP payload, in or out. +class DpipMeshPacket { + const DpipMeshPacket({ + required this.kind, + required this.body, + this.schema = 1, + this.from, + this.receivedAt, + }); + + final DpipMeshKind kind; + + /// Version of [body]'s layout, owned by [kind]. + final int schema; + + /// The kind-specific payload, opaque to the transport. + final Uint8List body; + + /// Sending node — inbound only. + final int? from; + + /// When this device received it — inbound only. + final DateTime? receivedAt; + + @override + String toString() => + 'DpipMeshPacket(${kind.name} v$schema, ${body.length} B' + '${from != null ? ', from 0x${from!.toRadixString(16)}' : ''})'; +} + +/// Encodes and decodes the envelope above. Pure — no I/O, no logging, so it +/// can be golden-tested against fixed bytes. +abstract final class DpipMeshCodec { + const DpipMeshCodec._(); + + /// `'D'`, `'P'` — cheap rejection of anything else riding `PRIVATE_APP`. + static const int magic0 = 0x44; + static const int magic1 = 0x50; + + /// Envelope version this build speaks. + static const int version = 1; + + /// Bytes before the body. + static const int headerBytes = 5; + + /// The largest body one frame can carry. + static const int maxBodyBytes = MeshPorts.maxPayloadBytes - headerBytes; + + /// Serialises [packet]. Throws [ArgumentError] for an over-long body — + /// truncating a disaster payload would be worse than failing loudly. + static Uint8List encode(DpipMeshPacket packet) { + if (packet.body.length > maxBodyBytes) { + throw ArgumentError.value( + packet.body.length, + 'body', + 'exceeds the $maxBodyBytes B DPIP body budget', + ); + } + if (packet.schema < 0 || packet.schema > 0xFF) { + throw ArgumentError.value(packet.schema, 'schema', 'must fit in a byte'); + } + final bytes = Uint8List(headerBytes + packet.body.length) + ..[0] = magic0 + ..[1] = magic1 + ..[2] = version + ..[3] = packet.kind.code + ..[4] = packet.schema + ..setRange(headerBytes, headerBytes + packet.body.length, packet.body); + return bytes; + } + + /// Parses [bytes], or returns null when they are not a DPIP envelope this + /// build understands (foreign private-app traffic, a newer envelope + /// version, an unknown kind, a truncated frame). + static DpipMeshPacket? decode( + List bytes, { + int? from, + DateTime? receivedAt, + }) { + if (bytes.length < headerBytes) return null; + if (bytes[0] != magic0 || bytes[1] != magic1) return null; + if (bytes[2] != version) return null; + final kind = DpipMeshKind.fromCode(bytes[3]); + if (kind == null) return null; + return DpipMeshPacket( + kind: kind, + schema: bytes[4], + body: Uint8List.fromList(bytes.sublist(headerBytes)), + from: from, + receivedAt: receivedAt, + ); + } +} + +/// The channel DPIP traffic travels on. +/// +/// Fixed by product decision, not by the user: every DPIP node must land on +/// the same channel or nothing decrypts. The name and the key together define +/// the channel hash the radios match on, so both are part of the contract. +abstract final class DpipMeshChannel { + const DpipMeshChannel._(); + + /// Channel name, as it appears on the radio. + static const String name = 'DPIP'; + + /// The pre-shared key, base64 `AQ==`. A single-byte PSK is Meshtastic + /// shorthand for a well-known key (`0x01` = the default key) — this channel + /// is about reaching every DPIP node, not about secrecy. + /// + /// **Open decision:** that key is published, so this channel gives no + /// authenticity. Anyone within radio range can encrypt a well-formed + /// [DpipMeshKind.eew] packet and every DPIP app that hears it will accept + /// it. Fixing that needs a signature the app can verify (a public key + /// shipped in the app, private keys held by DPIP's gateways), which fits in + /// a new [DpipMeshPacket.schema] for each kind without touching the + /// envelope. Until then, treat a mesh-sourced alert as unauthenticated. + static const List psk = [0x01]; + + /// The LoRa region every DPIP radio in Taiwan must be on. A radio on the + /// wrong region transmits on frequencies its neighbours never hear. + static const String region = 'TW'; + + /// What [MeshtasticService.ensureChannel] provisions. + static const MeshChannelSpec spec = MeshChannelSpec(name: name, psk: psk); +} diff --git a/lib/core/meshtastic/domain/dpip_mesh_gateway.dart b/lib/core/meshtastic/domain/dpip_mesh_gateway.dart new file mode 100644 index 000000000..025830373 --- /dev/null +++ b/lib/core/meshtastic/domain/dpip_mesh_gateway.dart @@ -0,0 +1,44 @@ +/// The seam between DPIP's disaster feeds and the mesh. +/// +/// Both directions in one interface, deliberately: +/// +/// - **App → mesh** ([broadcast]): a device that still has internet re-emits +/// an alert onto the mesh, so nodes that have none still get it. Whoever +/// owns a feed (EEW, tsunami, …) encodes its own body and hands over a +/// [DpipMeshPacket] — the gateway knows nothing about the contents. +/// - **Mesh → app** ([inbound]): every DPIP packet the radio decodes, already +/// unwrapped from its envelope. A feed subscribes, decodes its own body, and +/// decides what to show. The gateway never notifies or renders. +/// +/// Keeping this an interface (with the transport behind it) means a feed +/// depends on "DPIP packets in and out", not on Bluetooth: the same seam can +/// later carry a serial, TCP or MQTT link, and tests can pump packets through +/// without a radio. +/// +/// **The gateway is not a delivery guarantee.** LoRa is a lossy, duty-cycle +/// limited broadcast medium with no acknowledgement in this direction; a +/// safety-critical feed must treat mesh delivery as best-effort — one more +/// path, never the only one, and never proof that a peer received anything. +library; + +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; + +abstract class DpipMeshGateway { + /// DPIP packets decoded from the mesh, in arrival order. + /// + /// Broadcast: several feeds can listen at once. Packets that fail the + /// envelope check (foreign traffic on the private port, an unknown kind, a + /// newer envelope version) never reach here. + Stream get inbound; + + /// Broadcasts [packet] on the DPIP channel. + /// + /// Fails when the radio is not connected, the DPIP channel isn't + /// provisioned yet, or the body exceeds [DpipMeshCodec.maxBodyBytes]. + Future> broadcast(DpipMeshPacket packet); + + /// Whether a broadcast could go out right now — the radio is connected and + /// the DPIP channel exists on it. + bool get isReady; +} diff --git a/lib/core/meshtastic/domain/meshtastic_service.dart b/lib/core/meshtastic/domain/meshtastic_service.dart new file mode 100644 index 000000000..348fd9d6c --- /dev/null +++ b/lib/core/meshtastic/domain/meshtastic_service.dart @@ -0,0 +1,419 @@ +/// Domain surface for the LoRa mesh (Meshtastic) transport. +/// +/// The interface and its models are **package-free**: they deliberately expose +/// none of `meshtastic_flutter` / `flutter_blue_plus` types, so the +/// presentation layer depends only on this file and the transport can be +/// swapped without touching a widget. The BLE impl lives in +/// `data/meshtastic_client_impl.dart`. +/// +/// Connection and messaging are streams (push-style, like every other +/// realtime surface in the app); one-shot operations return [Result] so a +/// failure is explicit. Scanning is a stream whose error events carry +/// transport failures. +library; + +import 'package:dpip/core/error/result.dart'; + +abstract class MeshtasticService { + /// Requests Bluetooth/location permissions and verifies the adapter is on. + /// + /// Idempotent — the first scan or connect calls it implicitly, so a page + /// that only sends can skip it. The OS may show a permission dialog. + Future> initialize(); + + /// Yields nearby Meshtastic radios until [timeout] elapses. + /// + /// Errors (permission denied, adapter off) surface as stream errors; the + /// stream always ends after the timeout even if devices keep arriving. + Stream scanForDevices({Duration timeout}); + + /// Connects to a scanned device and downloads its configuration. + /// + /// Replaces any existing connection. Configuration can take a few seconds; + /// watch [connectionStream] for `connecting` → `configuring` → `connected`. + Future> connect(MeshDevice device); + + /// Connects to a radio by its BLE id without scanning first — how a saved + /// radio is picked back up after a drop or an app restart. + /// + /// The id is only guaranteed meaningful to this app on iOS, so a failure + /// here is normal and the caller should fall back to [scanForDevices]. + Future> connectToId(String id); + + /// Who currently holds the BLE link to [deviceId]. + /// + /// A radio speaks its phone protocol to **one** client at a time: two apps + /// draining the same mailbox steal each other's packets. This is the only + /// signal either platform gives us about that. + Future linkOwner(String deviceId); + + /// Drops the connection and clears cached nodes. + Future> disconnect(); + + /// Broadcasts [text] on [channel] (0 = the primary channel). + /// + /// Fails with a typed failure when not connected or not configured. + Future> sendText(String text, {int channel = 0}); + + /// Connection lifecycle: connecting / configuring / connected / error … + Stream get connectionStream; + + /// Every node known to the mesh (including the local radio). + Stream get nodeStream; + + /// Incoming mesh packets that carry text. + Stream get messageStream; + + /// Every decoded packet the radio delivers, whatever its app port — the + /// seam the DPIP data plane listens on (see `dpip_mesh_gateway.dart`). + Stream get dataStream; + + /// Broadcasts [payload] on [portnum] over [channel] (the counterpart of + /// [dataStream]). + /// + /// [portnum] is a Meshtastic app port; DPIP traffic uses + /// [MeshPorts.private]. Payloads are capped by the LoRa frame — see + /// [MeshPorts.maxPayloadBytes]. + Future> sendData({ + required int portnum, + required List payload, + int channel = 0, + bool wantAck = false, + }); + + /// Packet counters for the current session. + /// + /// Exists because a healthy mesh link is mostly **silent**: between events + /// there is nothing on screen to distinguish "connected and listening" from + /// "the link died ten minutes ago". A rising receive count is the cheapest + /// honest proof that the radio is still talking to us. + MeshTraffic get traffic; + + /// [traffic] on every packet in or out — what a heartbeat indicator watches. + Stream get trafficStream; + + /// Everything the attached radio has told us about itself, or null before + /// the config download finishes. + MeshRadioInfo? get radioInfo; + + /// The radio's channel table as last downloaded (empty before that). + List get channels; + + /// The radio's LoRa region code (`TW`, `EU_868`, `UNSET`…), or null before + /// the config download. + String? get region; + + /// The attached radio's own node number, or null before the download. + int? get myNodeNum; + + /// Makes sure a channel matching [spec] exists on the radio, creating it in + /// a free slot when it doesn't; returns the channel index. + /// + /// Never overwrites a channel the user already uses — if every secondary + /// slot is taken it fails rather than clobbering one. + Future> ensureChannel(MeshChannelSpec spec); + + /// Sets the LoRa region. + /// + /// **Disruptive**: the firmware disables Bluetooth and reboots the radio + /// when radio parameters change, so the link drops for ~10 s and every + /// other Meshtastic client of that radio is affected too. Ask first. + Future> applyRegion(String region); + + /// Whether a radio is connected and configured. + bool get isConnected; +} + +/// Who holds the BLE link to a radio. +enum MeshLinkOwner { + /// Nobody on this phone — a normal connect. + free, + + /// This app already has a link (a stale one is dropped before reconnecting). + thisApp, + + /// Another app on this phone (typically the official Meshtastic client). + /// Neither platform lets us close it; the user has to. + otherApp, +} + +/// Meshtastic app ports DPIP speaks, and the frame budget they share. +abstract final class MeshPorts { + const MeshPorts._(); + + /// `PRIVATE_APP` — the port range Meshtastic reserves for private + /// application traffic. DPIP's disaster payloads ride here so they never + /// land in anyone's chat. + static const int private = 256; + + /// `TEXT_MESSAGE_APP`. + static const int text = 1; + + /// Largest payload one mesh frame carries (`DATA_PAYLOAD_LEN`). A DPIP + /// packet that doesn't fit must be split by its own schema — the transport + /// never fragments. + static const int maxPayloadBytes = 233; + + /// Share of [maxPayloadBytes] held back from anything a user composes. + /// + /// The transport only measures the payload, but what actually goes on air + /// carries the `Data` and `MeshPacket` framing around it, and a radio may + /// refuse a frame that lands right on the edge. Spending the last few bytes + /// buys nothing and risks a message that silently doesn't send. + static const double payloadHeadroom = 0.05; + + /// What a composed message may occupy, in **UTF-8 bytes** — not characters. + /// One Chinese character costs three of these, so a character count would + /// promise roughly three times the room that exists. + static const int maxTextBytes = 221; // (233 * 0.95).floor() +} + +/// Packets in and out since the app started, counted at the transport — one +/// place, whether or not anything is listening to a stream. +class MeshTraffic { + const MeshTraffic({ + this.rxPackets = 0, + this.txPackets = 0, + this.rxBytes = 0, + this.txBytes = 0, + this.rxUndecoded = 0, + this.rxByPort = const {}, + this.lastRx, + this.lastTx, + }); + + final int rxPackets; + final int txPackets; + + /// Payload bytes, not frame bytes — the transport never sees the on-air + /// size, so this is what it can honestly report. + final int rxBytes; + final int txBytes; + + /// Packets the radio couldn't decrypt (a channel this radio has no key + /// for). They still prove the link is alive, which is why they're counted. + final int rxUndecoded; + + /// Received packet count per app port, for the diagnostics panel. + /// + /// Inbound only, deliberately: what arrives comes from every app on the + /// mesh, so the breakdown says something. What leaves is only ever ours — + /// chat and DPIP — and the totals already cover it. + final Map rxByPort; + + final DateTime? lastRx; + final DateTime? lastTx; + + bool get isEmpty => rxPackets == 0 && txPackets == 0; +} + +/// A snapshot of the attached radio: who it is, what it runs, how it's doing. +class MeshRadioInfo { + const MeshRadioInfo({ + required this.nodeNum, + this.longName, + this.shortName, + this.hardware, + this.firmware, + this.role, + this.region, + this.modemPreset, + this.hopLimit, + this.txPower, + this.batteryPercent, + this.voltage, + this.channelUtilization, + this.airUtilTx, + this.uptime, + this.metricsAt, + this.isLicensed = false, + this.hasWifi = false, + this.hasBluetooth = false, + }); + + /// The radio's own node number. + final int nodeNum; + + final String? longName; + final String? shortName; + + /// Board model (`HELTEC_V3`, `TBEAM`…) and firmware version string. + final String? hardware; + final String? firmware; + + /// Device role (`CLIENT`, `ROUTER`…). + final String? role; + + /// LoRa region and modem preset — what decides who can hear this radio. + final String? region; + final String? modemPreset; + final int? hopLimit; + final int? txPower; + + /// Battery charge. A mains-powered radio reports 101; callers should show + /// that as "plugged in" rather than a percentage. + final int? batteryPercent; + final double? voltage; + + /// Share of airtime seen busy, and share this radio spent transmitting — + /// the two numbers that say whether the mesh around it is congested. + final double? channelUtilization; + final double? airUtilTx; + + final Duration? uptime; + + /// When the battery/airtime figures above were last reported. A charge + /// reading without its age is worse than none — the radio only broadcasts + /// telemetry every few minutes, so a stale number looks live. + final DateTime? metricsAt; + + final bool isLicensed; + final bool hasWifi; + final bool hasBluetooth; + + /// Whether the radio is running off external power rather than a battery. + bool get isPluggedIn => (batteryPercent ?? 0) > 100; +} + +/// One decoded packet, whatever its app port. +class MeshDataPacket { + const MeshDataPacket({ + required this.from, + required this.channel, + required this.portnum, + required this.payload, + required this.timestamp, + }); + + final int from; + final int channel; + final int portnum; + final List payload; + final DateTime timestamp; +} + +/// A channel slot on the radio. +class MeshChannel { + const MeshChannel({ + required this.index, + required this.name, + required this.psk, + required this.enabled, + }); + + final int index; + final String name; + + /// Pre-shared key. A single byte is Meshtastic shorthand for a well-known + /// key (`0x01` = the default key, base64 `AQ==`). + final List psk; + + /// Whether the slot is in use (role != DISABLED). + final bool enabled; +} + +/// The channel DPIP wants to exist on the radio. +class MeshChannelSpec { + const MeshChannelSpec({required this.name, required this.psk}); + + final String name; + final List psk; +} + +/// Connection lifecycle of the attached radio. +enum MeshConnectionState { + /// Not connected to any radio. + disconnected, + + /// BLE link up, configuration download in progress. + connecting, + + /// Connected and receiving configuration. + configuring, + + /// Connected and ready for communication. + connected, + + /// Connection lost or a fatal error. + error, +} + +/// A radio discovered by [MeshtasticService.scanForDevices]. +class MeshDevice { + const MeshDevice({required this.id, required this.name}); + + /// BLE address (`remoteId`), stable across scans — the handle [connect] + /// uses. + final String id; + + /// Advertised platform name (empty on some radios). + final String name; + + @override + bool operator ==(Object other) => other is MeshDevice && other.id == id; + + @override + int get hashCode => id.hashCode; + + @override + String toString() => 'MeshDevice($id, $name)'; +} + +/// Connection status of the attached radio. +class MeshConnectionStatus { + const MeshConnectionStatus({ + required this.state, + this.deviceName, + this.errorMessage, + }); + + final MeshConnectionState state; + final String? deviceName; + final String? errorMessage; +} + +/// A node on the mesh (the local radio or any heard neighbor). +class MeshNode { + const MeshNode({ + required this.num, + required this.displayName, + required this.isOnline, + this.batteryLevel, + this.lastHeard, + this.latitude, + this.longitude, + this.snr = 0, + this.viaMqtt = false, + }); + + /// Node id (radio number, hex elsewhere in the mesh UI). + final int num; + + final String displayName; + final bool isOnline; + final int? batteryLevel; + final DateTime? lastHeard; + final double? latitude; + final double? longitude; + final double snr; + + /// Heard only through an MQTT bridge — over the internet rather than over + /// the air. Such a node may be on the other side of the world, so it is not + /// evidence of radio reach. + final bool viaMqtt; +} + +/// A received text packet from the mesh. +class MeshMessage { + const MeshMessage({ + required this.from, + required this.channel, + required this.text, + required this.timestamp, + }); + + final int from; + final int channel; + final String text; + final DateTime timestamp; +} diff --git a/lib/core/meshtastic/mesh_alerts.dart b/lib/core/meshtastic/mesh_alerts.dart new file mode 100644 index 000000000..32f512002 --- /dev/null +++ b/lib/core/meshtastic/mesh_alerts.dart @@ -0,0 +1,278 @@ +/// Raises **local** notifications for mesh traffic — no server, no push. +/// +/// Everything here is posted by the app itself from the BLE link, which is the +/// point: the mesh exists for when there is no internet, so an alert that +/// depended on FCM/APNs would be silent exactly when it mattered. The cost is +/// that the app must be running to raise one (see the platform note below). +/// +/// Two rules keep this from becoming noise, and both matter more than the +/// feature itself: +/// +/// - **Never announce what the user is already reading.** A message arriving in +/// the conversation on screen is not news. +/// - **Never announce the node-DB dump.** Connecting to a busy mesh delivers +/// twenty-odd nodes in two seconds; none of them are "new", they are just +/// being introduced. Only nodes first heard well after the link settled count. +/// +/// **Platform reality**: Android keeps the process (and so the BLE link) alive +/// in the background for a long while, so these arrive there. iOS suspends the +/// app without the `bluetooth-central` background mode, and a suspended app +/// receives no BLE events — so on iOS today these are foreground-only. +library; + +import 'dart:async'; + +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/notifications/notification_channels.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/core/settings/preference_keys.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter/widgets.dart'; + +/// One notification this class decided to raise. +@immutable +class MeshAlert { + const MeshAlert({ + required this.channelKey, + required this.id, + required this.title, + required this.body, + }); + + final String channelKey; + final int id; + final String title; + final String body; +} + +class MeshAlerts extends ChangeNotifier { + /// [post] and [now] are injectable so the suppression rules — which are the + /// whole value here — can be tested without an OS notification channel or a + /// twenty-second wait. + MeshAlerts( + this._service, + this._prefs, { + Future Function(MeshAlert alert)? post, + DateTime Function()? now, + }) : _post = post ?? _postToOs, + _now = now ?? (() => AppTime.utc.toLocal()); + + /// How long after the link comes up before a node counts as newly *heard* + /// rather than newly *introduced*. The config download hands over the whole + /// node DB in the first seconds of a connection. + static const Duration _nodeSettleDelay = Duration(seconds: 20); + + /// Notification titles are prefixed with this so a mesh alert is + /// recognisable at a glance in a crowded shade. A product name, not prose — + /// it reads the same in every language. + static const String _appName = 'Meshtastic'; + + /// Ceiling on node notifications per minute — a mesh that suddenly hears a + /// dozen neighbours must not produce a dozen notifications. + static const int _nodeBurstLimit = 3; + + final MeshtasticService _service; + final Prefs _prefs; + final Future Function(MeshAlert alert) _post; + final DateTime Function() _now; + + StreamSubscription? _messageSub; + StreamSubscription? _nodeSub; + StreamSubscription? _statusSub; + AppLifecycleListener? _lifecycle; + + final Set _knownNodes = {}; + + /// Node number → display name, accumulated from the node stream so a + /// notification can name a sender instead of showing a hex id. + final Map _nodeNames = {}; + DateTime? _linkReadyAt; + final List _recentNodeAlerts = []; + + bool _messagesEnabled = true; + bool _nodesEnabled = false; + + /// Which conversation is on screen right now, set by the mesh page. Null + /// when the page isn't showing. + int? _visibleChannel; + bool _appInForeground = true; + + /// Whether an incoming mesh message raises a notification. + bool get messagesEnabled => _messagesEnabled; + + /// Whether a newly heard node raises one. Off by default: on a busy mesh + /// new neighbours appear all day and almost none of them are worth an alert. + bool get nodesEnabled => _nodesEnabled; + + void start() { + _messagesEnabled = + _prefs.getBool(PreferenceKeys.meshNotifyMessages) ?? true; + _nodesEnabled = _prefs.getBool(PreferenceKeys.meshNotifyNodes) ?? false; + _messageSub ??= _service.messageStream.listen(_onMessage); + _nodeSub ??= _service.nodeStream.listen(_onNode); + _statusSub ??= _service.connectionStream.listen(_onStatus); + // Backgrounded, the page may still be "showing" a channel it can no longer + // show anyone — so foreground is part of the suppression rule, not the + // visible channel alone. + _lifecycle ??= AppLifecycleListener( + onResume: () => setForeground(foreground: true), + onHide: () => setForeground(foreground: false), + onPause: () => setForeground(foreground: false), + ); + } + + Future setMessagesEnabled({required bool enabled}) async { + _messagesEnabled = enabled; + notifyListeners(); + await _prefs.setBool(PreferenceKeys.meshNotifyMessages, enabled); + } + + Future setNodesEnabled({required bool enabled}) async { + _nodesEnabled = enabled; + notifyListeners(); + await _prefs.setBool(PreferenceKeys.meshNotifyNodes, enabled); + } + + /// Called by the mesh page: [channel] while it is showing that conversation, + /// null when it goes away. + void setVisibleChannel(int? channel) => _visibleChannel = channel; + + /// Called by the app shell so a backgrounded app still notifies for the + /// conversation that was last on screen. + void setForeground({required bool foreground}) => + _appInForeground = foreground; + + void _onStatus(MeshConnectionStatus status) { + if (status.state == MeshConnectionState.connected) { + _linkReadyAt ??= _now(); + return; + } + if (status.state == MeshConnectionState.disconnected || + status.state == MeshConnectionState.error) { + // The next connection dumps the node DB again; start the clock over. + _linkReadyAt = null; + } + } + + void _onMessage(MeshMessage message) { + if (!_messagesEnabled) return; + if (message.text.isEmpty) return; + // Already on screen, in that channel, with the app in front: not news. + if (_appInForeground && _visibleChannel == message.channel) return; + unawaited( + _post( + MeshAlert( + channelKey: 'mesh_message', + // Notification ids must fit a 32-bit int; the timestamp keeps + // consecutive messages from collapsing onto one another. + id: message.timestamp.millisecondsSinceEpoch & 0x7FFFFFFF, + title: '$_appName - ${_channelLabel(message.channel)}', + body: + '${_senderName(message.from)} - ${_clockLabel(message.timestamp)}' + '\n${message.text}', + ), + ), + ); + } + + /// What to call a channel in a notification. + /// + /// Channel *names* only exist while a radio is attached — they come from its + /// channel table. With no link (or for a slot the table never described) + /// the index is all that is known, so the label degrades to `CH0`, `CH1`… + /// rather than inventing a name or leaving the title half-empty. + String _channelLabel(int index) { + for (final channel in _service.channels) { + if (channel.index == index && channel.name.isNotEmpty) { + return channel.name; + } + } + return 'CH$index'; + } + + /// `HH:mm:ss`, built by hand rather than through `intl`: this runs in + /// `core/` with no `BuildContext`, and a mesh timestamp is a clock reading, + /// not a localised date. + String _clockLabel(DateTime at) { + String two(int value) => value.toString().padLeft(2, '0'); + return '${two(at.hour)}:${two(at.minute)}:${two(at.second)}'; + } + + /// The sender's name once the mesh has introduced it, else its id. + String _senderName(int from) { + final name = _nodeNames[from]; + return (name != null && name.isNotEmpty) + ? name + : '0x${from.toRadixString(16)}'; + } + + void _onNode(MeshNode node) { + // Recorded before any early return: a node the user doesn't want announced + // is still a node whose name a *message* notification will want. + if (node.displayName.isNotEmpty) _nodeNames[node.num] = node.displayName; + final firstTime = _knownNodes.add(node.num); + if (!firstTime || !_nodesEnabled) return; + final readyAt = _linkReadyAt; + if (readyAt == null) return; // still downloading the node DB + if (_now().difference(readyAt) < _nodeSettleDelay) return; + if (!_allowNodeAlert()) return; + unawaited( + _post( + MeshAlert( + channelKey: 'mesh_node', + id: node.num & 0x7FFFFFFF, + title: _appName, + body: _senderName(node.num), + ), + ), + ); + } + + bool _allowNodeAlert() { + final now = _now(); + _recentNodeAlerts.removeWhere( + (at) => now.difference(at) > const Duration(minutes: 1), + ); + if (_recentNodeAlerts.length >= _nodeBurstLimit) return false; + _recentNodeAlerts.add(now); + return true; + } + + static Future _postToOs(MeshAlert alert) async { + try { + await AwesomeNotifications().createNotification( + content: NotificationContent( + id: alert.id, + channelKey: alert.channelKey, + title: alert.title, + body: alert.body, + icon: NotificationChannels.icon, + // Grouped so a burst of mesh traffic collapses into one stack + // rather than a column of separate notifications. + groupKey: alert.channelKey, + payload: {'channel': alert.channelKey}, + // The two platforms disagree about multi-line bodies. iOS renders a + // `\n` as-is; Android collapses a notification to a single line + // unless it is told the body is long-form, so without `BigText` the + // second line would simply never be seen. Setting both keeps one + // string rendering the same way on each. + notificationLayout: NotificationLayout.BigText, + wakeUpScreen: false, + ), + ); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh notification'); + } + } + + @override + void dispose() { + unawaited(_messageSub?.cancel()); + unawaited(_nodeSub?.cancel()); + unawaited(_statusSub?.cancel()); + _lifecycle?.dispose(); + super.dispose(); + } +} diff --git a/lib/core/meshtastic/mesh_link.dart b/lib/core/meshtastic/mesh_link.dart new file mode 100644 index 000000000..6b1995f95 --- /dev/null +++ b/lib/core/meshtastic/mesh_link.dart @@ -0,0 +1,538 @@ +/// Keeps a radio attached for as long as the app is running, and makes sure it +/// is provisioned for DPIP once it is. +/// +/// Lives in `core/` and is created at bootstrap, not by a page: the mesh is a +/// reception path for disaster information, so the link must outlive whatever +/// screen the user is on. Leaving the mesh page changes nothing; only +/// [detach] (the user asking) or the app dying ends a session. +/// +/// What it owns, and why each piece exists: +/// +/// - **Intent.** The chosen radio's id is persisted, and its presence *is* the +/// intent to be connected. Nothing reconnects after [detach]. +/// - **Reconnection.** BLE links drop constantly — the user walks away, the +/// radio reboots, the OS reclaims the connection. A drop schedules a retry +/// with a capped backoff; a resume from background retries at once. +/// - **Single flight.** Drops, resumes and manual taps all want to connect. +/// Exactly one attempt runs at a time, and the transport's own +/// disconnect-before-connect can't be mistaken for a link loss. +/// - **Provisioning.** After each successful connect, the DPIP channel is +/// verified/created and the LoRa region checked, so a radio that reboots or +/// gets reconfigured elsewhere heals on reconnect. +library; + +import 'dart:async'; + +import 'package:dpip/core/error/failure.dart'; +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/settings/preference_keys.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter/widgets.dart'; + +/// How far provisioning the DPIP channel got on the attached radio. +enum MeshProvisionState { + /// No radio, or nothing tried yet. + idle, + + /// Reading/writing the channel table. + working, + + /// The DPIP channel is present with the right key. + ready, + + /// Every secondary slot is taken by the user's own channels. We never + /// overwrite one, so this needs a human. + noFreeSlot, + + /// A channel already carries DPIP's name but a different key. Also a human's + /// call — rewriting it would replace the user's key with the public default. + conflict, + + /// The radio refused or never confirmed the write. + failed, +} + +/// What the radio's LoRa region means for DPIP. +enum MeshRegionState { + /// Not known yet (no config download). + unknown, + + /// Already on the DPIP region. + ok, + + /// A brand-new radio with no region — safe to set without asking. + unset, + + /// A different region, deliberately chosen by someone. Changing it reboots + /// the radio and moves every one of the user's other channels with it, so + /// this one only changes on an explicit confirmation. + mismatch, +} + +class MeshLink extends ChangeNotifier { + MeshLink(this._service, this._prefs); + + /// What [attach] returns when another app on this phone holds the radio — + /// not a message, a signal for the UI to ask whether to connect anyway. + static const String busySentinel = '__mesh_link_busy__'; + + /// Reconnect delays, in seconds; the last one repeats forever. Front-loaded + /// because most drops are momentary, capped because a radio that is out of + /// range or powered off must not keep the radio hardware busy. + static const List _backoffSeconds = [2, 5, 10, 20, 40, 60]; + + /// A radio that just took a region change reboots; skip straight to a slower + /// retry instead of hammering a device that is deliberately away. + static const Duration _rebootGrace = Duration(seconds: 12); + + /// How long a link must hold before the backoff counts as recovered. + /// + /// Resetting on `connected` alone is what turns an edge-of-range radio into + /// a permanent 2-second connect/drop loop: every cycle "succeeds" long + /// enough to zero the counter, so the backoff never escalates past its first + /// step. + static const Duration _stableAfter = Duration(seconds: 45); + + /// Ceiling on one connect attempt, including the rediscovery scan. A stuck + /// attempt would otherwise hold `_attempting` forever, and that flag gates + /// both retry scheduling and the resume-from-background path. + static const Duration _attemptBudget = Duration(seconds: 50); + + final MeshtasticService _service; + final Prefs _prefs; + + StreamSubscription? _statusSub; + AppLifecycleListener? _lifecycle; + Timer? _retryTimer; + Timer? _stabilityTimer; + bool _disposed = false; + + /// Bumped by every [attach] / [detach]. An attempt (or a provisioning run) + /// whose generation is stale must not write state or prefs — it is working + /// on a radio the user has already moved on from. + int _generation = 0; + Future? _inFlight; + + MeshConnectionStatus _status = const MeshConnectionStatus( + state: MeshConnectionState.disconnected, + ); + String? _deviceId; + String? _deviceName; + bool _attempting = false; + int _failures = 0; + bool _regionApplied = false; + bool _everConnected = false; + String? _lastError; + + MeshProvisionState _provision = MeshProvisionState.idle; + String? _provisionError; + int? _dpipChannel; + int? _lastDpipChannel; + + /// The transport's current connection state. + MeshConnectionStatus get status => _status; + + /// Whether a radio is connected and configured. + bool get isConnected => _status.state == MeshConnectionState.connected; + + /// The radio this app will keep reconnecting to, if any. + String? get savedRadioId => _deviceId; + String? get savedRadioName => _deviceName; + + /// Whether the link is being *re*-established — a first connect is reported + /// through [status] instead, so the UI doesn't say "reconnecting" to someone + /// who has never connected. + bool get reconnecting => + _everConnected && + _deviceId != null && + !isConnected && + (_attempting || _retryTimer != null); + + /// Whether another attempt is queued. True even before the first successful + /// connection, where [reconnecting] is deliberately false. + bool get willRetry => _retryTimer != null; + + /// Why the last automatic attempt failed, if one did. The automatic paths + /// have no caller to return to, so this is how a silent stop (a revoked + /// permission, a radio that never answers) becomes visible. + String? get lastError => _lastError; + + /// The DPIP channel's index on the attached radio, once provisioned — what + /// the gateway sends and filters on. Cleared on a drop, because an index + /// from the previous radio must never be transmitted on. + int? get dpipChannel => _dpipChannel; + + /// Where DPIP was last seen, kept across a drop. + /// + /// Display only — never send on this. It exists so a disconnected UI can + /// still default to the DPIP conversation instead of falling back to + /// whatever channel happens to sort first. + int? get lastKnownDpipChannel => _dpipChannel ?? _lastDpipChannel; + + MeshProvisionState get provision => _provision; + + /// Why provisioning failed, when it did. + String? get provisionError => _provisionError; + + /// The radio's LoRa region as last read. + String? get region => _service.region; + + MeshRegionState get regionState { + final current = _service.region; + if (!isConnected || current == null) return MeshRegionState.unknown; + if (current == DpipMeshChannel.region) return MeshRegionState.ok; + if (current == 'UNSET') return MeshRegionState.unset; + return MeshRegionState.mismatch; + } + + /// Wires the link up at bootstrap and, if a radio was chosen in an earlier + /// session, starts reconnecting to it. + void start() { + _statusSub ??= _service.connectionStream.listen(_onStatus); + _lifecycle ??= AppLifecycleListener(onResume: _onResume); + _deviceId = _prefs.getString(PreferenceKeys.meshDeviceId); + _deviceName = _prefs.getString(PreferenceKeys.meshDeviceName); + if (_deviceId == null) return; + Log.info('mesh link: resuming saved radio ${_deviceName ?? _deviceId}'); + unawaited(_attempt()); + } + + /// Adopts [device] as *the* radio: remembered, connected, provisioned, and + /// reconnected to until [detach]. + /// + /// Returns null on success, else a message for the user. When another app on + /// this phone already holds the radio the attempt stops with an explanation + /// unless [force] is set — neither iOS nor Android can evict another app's + /// link, and two clients draining one radio steal each other's packets. + Future attach(MeshDevice device, {bool force = false}) async { + if (!force) { + final owner = await _service.linkOwner(device.id); + if (owner == MeshLinkOwner.otherApp) return busySentinel; + } + // Supersede whatever is going on. Without this the single-flight guard in + // `_attempt` would drop the request and *report success*, so picking a + // second radio while connected (or mid-reconnect) would silently keep the + // old one — and a stale attempt could even write its own radio back into + // prefs afterwards. + // The choice is recorded **synchronously**, before any await, so two taps + // resolve in a defined order (the later one wins) rather than racing each + // other's writes. + final generation = ++_generation; + _deviceId = device.id; + _deviceName = device.name; + _failures = 0; + _regionApplied = false; + _lastError = null; + _cancelRetry(); + _notify(); + + if (_attempting || _service.isConnected) { + Log.info( + 'mesh link: superseding the current radio with "${device.name}"', + ); + await _service.disconnect(); + await _inFlight; + } + // Only the newest choice reaches storage — an overtaken attach must not + // leave its radio behind as the one to reconnect to. + if (generation != _generation) return null; + await _prefs.setString(PreferenceKeys.meshDeviceId, device.id); + await _prefs.setString(PreferenceKeys.meshDeviceName, device.name); + if (generation != _generation) return null; + return _attempt(); + } + + /// Forgets the radio and disconnects. Nothing reconnects afterwards. + Future detach() async { + Log.info('mesh link: detaching ${_deviceName ?? _deviceId}'); + _generation++; // an attempt still in flight is now working for nobody + _cancelRetry(); + _stabilityTimer?.cancel(); + _deviceId = null; + _deviceName = null; + _dpipChannel = null; + _provision = MeshProvisionState.idle; + _provisionError = null; + _lastError = null; + await _prefs.remove(PreferenceKeys.meshDeviceId); + await _prefs.remove(PreferenceKeys.meshDeviceName); + await _service.disconnect(); + notifyListeners(); + } + + /// Applies the DPIP LoRa region after the user confirmed it. + /// + /// The link *will* drop: the firmware turns Bluetooth off and reboots when + /// radio parameters change. That drop is expected, and the normal reconnect + /// path picks the radio back up once it is booted. + Future applyRegion() async { + final result = await _service.applyRegion(DpipMeshChannel.region); + if (result case Err(:final failure)) return failure.message; + _regionApplied = true; + // Give the radio its reboot window instead of retrying into a dead link. + _cancelRetry(); + _retryTimer = Timer(_rebootGrace, () { + _retryTimer = null; + unawaited(_attempt()); + }); + notifyListeners(); + return null; + } + + Future _attempt() { + final id = _deviceId; + // Guarded on the transport, not on [status]: the status is an echo that + // can still read `connected` right after we dropped a link ourselves, and + // trusting it there would skip the connect to the radio just chosen. + if (id == null || _attempting || _service.isConnected) { + return Future.value(); + } + _cancelRetry(); + _attempting = true; + notifyListeners(); + // Bounded, and the timeout is *inside* the tracked future: an attempt that + // never returns would pin `_attempting` true forever, and that flag gates + // retry scheduling and the resume path both. + final attempt = _run(id, _generation) + .timeout( + _attemptBudget, + onTimeout: () { + Log.warning('mesh link: connect attempt timed out'); + _failures++; + _scheduleRetry(); + return 'The radio did not answer in time'; + }, + ) + .whenComplete(() { + _attempting = false; + _inFlight = null; + _notify(); + }); + return _inFlight = attempt; + } + + Future _run(String id, int generation) async { + final result = await _service.connectToId(id); + if (result case Err(:final failure)) { + // Retrying a denied permission just re-prompts (or silently fails) + // forever — that one needs the user, not a timer. + if (failure is PermissionDeniedFailure) { + Log.warning('mesh link: stopping retries — ${failure.message}'); + return _fail(failure.message, retry: false); + } + Log.warning('mesh link: connect by id failed (${failure.message})'); + final rediscovered = await _rediscover(id); + if (rediscovered == null || generation != _generation) { + return _fail(failure.message); + } + final retry = await _service.connect(rediscovered); + if (retry case Err(:final failure)) return _fail(failure.message); + if (generation != _generation) return null; + await _remember(rediscovered); + } + if (generation != _generation) { + // The user attached a different radio (or detached) while this attempt + // was in flight — this link is nobody's, so don't keep it. + Log.info('mesh link: attempt superseded, dropping the link'); + await _service.disconnect(); + return null; + } + // Every `disconnected` seen during the attempt was written off as the + // transport's own teardown. If one of them was a real drop — a radio that + // accepts the link then goes away — nothing else will report it, and the + // link would sit down forever. Ask the transport directly. + if (!_service.isConnected) { + Log.warning('mesh link: connected then lost immediately'); + return _fail('The radio dropped the connection'); + } + _lastError = null; + return null; + } + + /// Records why an attempt failed and (unless told otherwise) queues another. + String _fail(String message, {bool retry = true}) { + _lastError = message; + if (retry) { + _failures++; + _scheduleRetry(); + } + return message; + } + + /// One short scan looking for the saved radio. + /// + /// Matches on the id **or the saved name**, because the id is exactly what + /// may have gone stale: an iOS peripheral identifier is per-install and + /// rotates when the system forgets the peripheral, so it comes back under a + /// new UUID. Matching only the id — the id that just failed — would make + /// this whole path unreachable for the case it exists to repair. The name is + /// weaker (two radios can share one) but it is what the user recognises, and + /// the new id is written back on success. + Future _rediscover(String id) async { + final name = _deviceName; + MeshDevice? byName; + try { + await for (final device in _service.scanForDevices( + timeout: const Duration(seconds: 6), + )) { + if (device.id == id) return device; + if (name != null && name.isNotEmpty && device.name == name) { + byName ??= device; + } + } + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh link rediscover'); + return byName; + } + if (byName != null) { + Log.info('mesh link: "$name" came back under a new id (${byName.id})'); + } + return byName; + } + + Future _remember(MeshDevice device) async { + _deviceId = device.id; + _deviceName = device.name; + await _prefs.setString(PreferenceKeys.meshDeviceId, device.id); + await _prefs.setString(PreferenceKeys.meshDeviceName, device.name); + } + + void _onStatus(MeshConnectionStatus status) { + _status = status; + switch (status.state) { + case MeshConnectionState.connected: + if (_deviceId == null) { + // A link that finished landing after the user forgot the radio. Left + // alone it would show as "connected" forever with nothing watching + // it — drop it instead of adopting it. + Log.info('mesh link: connected to a forgotten radio — dropping'); + unawaited(_service.disconnect()); + break; + } + _everConnected = true; + _cancelRetry(); + // The backoff is only forgiven once the link has *held*. Zeroing it on + // `connected` alone lets a radio that connects and drops every second + // sit at the 2 s step forever. + _stabilityTimer?.cancel(); + _stabilityTimer = Timer(_stableAfter, () { + _stabilityTimer = null; + _failures = 0; + }); + unawaited(_provisionRadio(_generation)); + case MeshConnectionState.disconnected: + case MeshConnectionState.error: + _stabilityTimer?.cancel(); + _stabilityTimer = null; + _dpipChannel = null; + _provision = MeshProvisionState.idle; + // The transport disconnects before it connects, so a drop reported + // while our own attempt is running is not a link loss. (The attempt + // re-checks liveness when it finishes, so a real drop in that window + // is still caught.) + if (_deviceId != null && !_attempting) { + Log.info('mesh link: link lost — scheduling reconnect'); + _scheduleRetry(); + } + case MeshConnectionState.connecting: + case MeshConnectionState.configuring: + break; + } + _notify(); + } + + void _onResume() { + if (_deviceId == null || isConnected || _attempting) return; + // Coming back to the app is the one moment the user is watching, so spend + // the retry now instead of sitting out the rest of a 60 s backoff. The + // failure count is deliberately *not* reset — otherwise app-switching + // would hold a hopeless radio at the shortest retry interval forever. + Log.info('mesh link: app resumed — reconnecting now'); + _cancelRetry(); + unawaited(_attempt()); + } + + void _scheduleRetry() { + if (_retryTimer != null || _deviceId == null || _disposed) return; + final index = (_failures - 1).clamp(0, _backoffSeconds.length - 1); + final delay = Duration(seconds: _backoffSeconds[index]); + Log.info('mesh link: retrying in ${delay.inSeconds}s'); + _retryTimer = Timer(delay, () { + _retryTimer = null; + unawaited(_attempt()); + }); + _notify(); + } + + void _cancelRetry() { + _retryTimer?.cancel(); + _retryTimer = null; + } + + /// [notifyListeners] that tolerates a disposed link — an attempt or a + /// provisioning run can outlive it. + void _notify() { + if (_disposed) return; + notifyListeners(); + } + + /// Verifies the radio carries the DPIP channel, creating it when it doesn't, + /// and reports what the region needs. Runs after every successful connect — + /// a radio reconfigured elsewhere heals on the next reconnect. + Future _provisionRadio(int generation) async { + _provision = MeshProvisionState.working; + _provisionError = null; + _notify(); + + final result = await _service.ensureChannel(DpipMeshChannel.spec); + // A run whose generation is stale belongs to a link that has since gone + // (or to a radio the user replaced). Letting it write would let a slow, + // failed pass overwrite the state a later, successful one already set. + if (generation != _generation || !isConnected) { + Log.debug('mesh link: discarding a stale provisioning result'); + return; + } + switch (result) { + case Ok(:final value): + _dpipChannel = value; + _lastDpipChannel = value; + _provision = MeshProvisionState.ready; + case Err(:final failure): + _dpipChannel = null; + _provisionError = failure.message; + _provision = switch (failure) { + MeshChannelNoSlotFailure() => MeshProvisionState.noFreeSlot, + MeshChannelConflictFailure() => MeshProvisionState.conflict, + _ => MeshProvisionState.failed, + }; + Log.warning('mesh link: provisioning failed — ${failure.message}'); + } + _notify(); + + // A radio that has never been configured has no region; setting it is what + // the user came here for and costs them nothing. Any *other* region is + // someone's deliberate choice — that one waits for a confirmation. + if (regionState == MeshRegionState.unset && !_regionApplied) { + Log.info('mesh link: radio has no region — setting TW'); + await applyRegion(); + } + } + + @override + void dispose() { + // Stops the machine, not just the listeners: an attempt still in flight + // would otherwise schedule a retry that fires against a dead object and + // reconnects BLE nobody is watching. + _disposed = true; + _generation++; + _deviceId = null; + _cancelRetry(); + _stabilityTimer?.cancel(); + unawaited(_statusSub?.cancel()); + _lifecycle?.dispose(); + super.dispose(); + } +} diff --git a/lib/core/meshtastic/mesh_metrics_recorder.dart b/lib/core/meshtastic/mesh_metrics_recorder.dart new file mode 100644 index 000000000..ce3470a1b --- /dev/null +++ b/lib/core/meshtastic/mesh_metrics_recorder.dart @@ -0,0 +1,60 @@ +/// Records the radio's utilization readings so they can be plotted over time. +/// +/// The radio broadcasts its own telemetry every few minutes and the transport +/// stamps each reading with when it arrived — so this needs no timer of its +/// own. It watches the traffic stream (which fires on every packet, telemetry +/// included) and writes a row whenever the reading's timestamp changes. +/// Sampling on a clock instead would either miss readings or duplicate them, +/// and would keep working — writing the same value forever — after the link +/// died. +library; + +import 'dart:async'; + +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; + +class MeshMetricsRecorder { + MeshMetricsRecorder(this._service, this._store); + + final MeshtasticService _service; + final MeshStore _store; + + StreamSubscription? _sub; + DateTime? _lastRecorded; + + void start() { + _sub ??= _service.trafficStream.listen((_) => _sample()); + } + + void _sample() { + final radio = _service.radioInfo; + final at = radio?.metricsAt; + if (radio == null || at == null || at == _lastRecorded) return; + // Nothing to plot: a radio that reports neither figure would otherwise + // fill the history with empty rows. + if (radio.channelUtilization == null && radio.airUtilTx == null) return; + _lastRecorded = at; + unawaited( + _store + .addMetric( + MeshMetricSample( + at: at, + channelUtilization: radio.channelUtilization, + airUtilTx: radio.airUtilTx, + batteryPercent: radio.batteryPercent, + ), + ) + .catchError( + (Object error, StackTrace stackTrace) => + Log.handle(error, stackTrace, 'mesh metrics record'), + ), + ); + } + + Future dispose() async { + await _sub?.cancel(); + _sub = null; + } +} diff --git a/lib/core/meshtastic/mesh_node_store.dart b/lib/core/meshtastic/mesh_node_store.dart new file mode 100644 index 000000000..1ad6eef2b --- /dev/null +++ b/lib/core/meshtastic/mesh_node_store.dart @@ -0,0 +1,331 @@ +/// The mesh's node table, kept across reconnects and app restarts. +/// +/// The radio hands over its whole node DB on every connect, so nothing here is +/// needed to *see* nodes while attached. It exists for the times you are not: +/// after a drop, before the first connect of a session, or with no radio at +/// all, the last known mesh is still the most useful thing the app can show — +/// which repeaters exist, roughly where they are, and when each was last heard. +/// +/// Lives in `core/` because two unrelated surfaces consume it (the mesh page's +/// node list and the map's node layer) and neither may reach into the other's +/// feature. +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:dpip/core/geo/geo_math.dart'; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/core/settings/preference_keys.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter/foundation.dart'; + +/// One point in a node's telemetry history — what the sheet's trend charts +/// plot. Kept in memory only (see [MeshNodeStore.historyLimit]). +class MeshNodeSample { + const MeshNodeSample({required this.time, required this.snr, this.battery}); + + final DateTime time; + final double snr; + final int? battery; +} + +class MeshNodeStore extends ChangeNotifier { + MeshNodeStore(this._service, this._prefs, {DateTime Function()? now}) + : _now = now ?? (() => AppTime.utc.toLocal()); + + /// How many nodes are kept. A busy region's mesh runs to a few hundred; the + /// least-recently-heard are dropped first, because a node nobody has heard + /// from in weeks is the one least worth remembering. + static const int maxNodes = 250; + + /// How long after its last transmission a node still counts as online. The + /// firmware uses the same window for its own node list. + static const Duration onlineWindow = Duration(minutes: 15); + + /// Node bursts arrive twenty at a time during a config download; writing on + /// each one would mean twenty serialisations of the whole table. + static const Duration _writeDebounce = Duration(seconds: 2); + + final MeshtasticService _service; + final Prefs _prefs; + final DateTime Function() _now; + + StreamSubscription? _sub; + Timer? _writeTimer; + final Map _nodes = {}; + bool _excludeMqtt = true; + + /// Whether MQTT-only nodes are kept off the map. + /// + /// **On by default**, and that default is the honest one: a node heard only + /// through an MQTT bridge arrived over the internet, so its marker says + /// nothing about what this radio can actually reach. Left in, a Taiwanese + /// mesh sprouts nodes in Japan and the United States and the map stops + /// answering the question it exists to answer. + bool get excludeMqtt => _excludeMqtt; + + Future setExcludeMqtt({required bool exclude}) async { + if (_excludeMqtt == exclude) return; + _excludeMqtt = exclude; + notifyListeners(); + await _prefs.setBool(PreferenceKeys.meshExcludeMqtt, exclude); + } + + /// Every node known, online first, then most-recently-heard. + List get nodes { + final all = _nodes.values.toList() + ..sort((a, b) { + final aOnline = isOnline(a); + final bOnline = isOnline(b); + if (aOnline != bOnline) return aOnline ? -1 : 1; + final aHeard = a.lastHeard, bHeard = b.lastHeard; + if (aHeard != null && bHeard != null) return bHeard.compareTo(aHeard); + if (aHeard != null) return -1; + if (bHeard != null) return 1; + return a.displayName.compareTo(b.displayName); + }); + return List.unmodifiable(all); + } + + /// The nodes the map can draw: they have a position, and — unless + /// [excludeMqtt] is off — they were heard over the air rather than through + /// an MQTT bridge. + List get positioned => [ + for (final node in _nodes.values) + if (node.latitude != null && node.longitude != null) + if (!_excludeMqtt || !node.viaMqtt) node, + ]; + + /// How many positioned nodes [excludeMqtt] is currently hiding — so the UI + /// can say what it left out instead of silently showing less. + int get hiddenMqttCount { + if (!_excludeMqtt) return 0; + return _nodes.values + .where((n) => n.latitude != null && n.longitude != null && n.viaMqtt) + .length; + } + + MeshNode? byNum(int num) => _nodes[num]; + + /// Whether [node] counts as online **right now**. + /// + /// Derived, never read from storage: a node persisted as online yesterday is + /// not online today, and a stored flag would say otherwise for as long as + /// the app went without hearing from it. + bool isOnline(MeshNode node) { + final heard = node.lastHeard; + if (heard == null) return false; + return _now().difference(heard) < onlineWindow; + } + + void start() { + _excludeMqtt = _prefs.getBool(PreferenceKeys.meshExcludeMqtt) ?? true; + _restore(); + _sub ??= _service.nodeStream.listen(_onNode); + } + + /// Forgets every node, on screen and on disk. + Future clear() async { + if (_nodes.isEmpty) return; + _nodes.clear(); + notifyListeners(); + _writeTimer?.cancel(); + await _persist(); + } + + void _onNode(MeshNode node) { + final existing = _nodes[node.num]; + // A node re-emitted from a telemetry packet carries fresh metrics but may + // carry no position; keep the last one we were told rather than dropping + // the node off the map. + _nodes[node.num] = existing == null + ? node + : MeshNode( + num: node.num, + displayName: node.displayName.isNotEmpty + ? node.displayName + : existing.displayName, + isOnline: node.isOnline, + batteryLevel: node.batteryLevel ?? existing.batteryLevel, + lastHeard: node.lastHeard ?? existing.lastHeard, + latitude: node.latitude ?? existing.latitude, + longitude: node.longitude ?? existing.longitude, + snr: node.snr != 0 ? node.snr : existing.snr, + viaMqtt: node.viaMqtt, + ); + _recordSample(node); + notifyListeners(); + _scheduleWrite(); + } + + /// Keeps a node's recent telemetry so the sheet can draw trends. + /// + /// Memory-only on purpose: a ring of numbers is worth showing, not worth + /// persisting — the radio re-sends the whole node DB on every connect + /// anyway, and the sample cadence is the node's own broadcast rate + /// (seconds to minutes), so a full ring is a long recent past. + static const int historyLimit = 60; + + final Map> _history = {}; + + /// This node's recent (time, SNR, battery) samples, oldest first. + List historyOf(int num) => + List.unmodifiable(_history[num] ?? const []); + + void _recordSample(MeshNode node) { + final samples = _history.putIfAbsent(node.num, () => []); + final last = samples.isNotEmpty ? samples.last : null; + final time = _now(); + // A burst arrives twenty nodes at a time and the radio re-emits the same + // telemetry repeatedly; a sample that changes nothing just moves the last + // one's time instead of piling up identical points. + if (last != null && + last.snr == node.snr && + last.battery == node.batteryLevel) { + samples[samples.length - 1] = MeshNodeSample( + time: time, + snr: last.snr, + battery: last.battery, + ); + return; + } + samples.add( + MeshNodeSample(time: time, snr: node.snr, battery: node.batteryLevel), + ); + if (samples.length > historyLimit) { + samples.removeRange(0, samples.length - historyLimit); + } + } + + /// Straight-line distance from this node to the radio's own node, km. + /// + /// The map's "how far is it" — computed here so the sheet stays a view. + /// Null when either side has no position yet. + double? distanceToMyRadioKm(MeshNode node) { + final mine = _service.myNodeNum == null + ? null + : _nodes[_service.myNodeNum!]; + final a = mine; + if (a == null || + a.latitude == null || + a.longitude == null || + node.latitude == null || + node.longitude == null) { + return null; + } + return _haversineKm( + a.latitude!, + a.longitude!, + node.latitude!, + node.longitude!, + ); + } + + /// Great-circle distance between two points (km) — short-range accuracy is + /// all a mesh needs, and the formula stays cheap on every sample. + static double _haversineKm( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + const earthRadiusKm = 6371.0; + final dLat = degToRad(lat2 - lat1); + final dLon = degToRad(lon2 - lon1); + final a = + math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(degToRad(lat1)) * + math.cos(degToRad(lat2)) * + math.sin(dLon / 2) * + math.sin(dLon / 2); + return earthRadiusKm * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + } + + void _scheduleWrite() { + _writeTimer?.cancel(); + _writeTimer = Timer(_writeDebounce, () { + _writeTimer = null; + unawaited(_persist()); + }); + } + + void _restore() { + final stored = _prefs.getStringList(PreferenceKeys.meshNodes); + if (stored == null) return; + for (final entry in stored) { + final node = _decode(entry); + if (node != null) _nodes[node.num] = node; + } + Log.debug('mesh nodes: restored ${_nodes.length}'); + } + + Future _persist() async { + try { + // Keep the most recently heard when trimming: an old node is the one the + // mesh has already forgotten. + final ordered = _nodes.values.toList() + ..sort((a, b) { + final aHeard = a.lastHeard, bHeard = b.lastHeard; + if (aHeard == null && bHeard == null) return 0; + if (aHeard == null) return 1; + if (bHeard == null) return -1; + return bHeard.compareTo(aHeard); + }); + await _prefs.setStringList(PreferenceKeys.meshNodes, [ + for (final node in ordered.take(maxNodes)) _encode(node), + ]); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh nodes persist'); + } + } + + String _encode(MeshNode node) => jsonEncode({ + 'n': node.num, + 'd': node.displayName, + if (node.batteryLevel != null) 'b': node.batteryLevel, + if (node.lastHeard != null) 'h': node.lastHeard!.millisecondsSinceEpoch, + if (node.latitude != null) 'la': node.latitude, + if (node.longitude != null) 'lo': node.longitude, + if (node.snr != 0) 's': node.snr, + if (node.viaMqtt) 'm': true, + }); + + MeshNode? _decode(String encoded) { + try { + final json = jsonDecode(encoded); + if (json is! Map) return null; + final nodeNum = (json['n'] as num?)?.toInt(); + if (nodeNum == null) return null; + final heard = (json['h'] as num?)?.toInt(); + final lastHeard = heard == null + ? null + : DateTime.fromMillisecondsSinceEpoch(heard); + return MeshNode( + num: nodeNum, + displayName: json['d'] as String? ?? '', + // Recomputed from `lastHeard`, never restored — see [isOnline]. + isOnline: + lastHeard != null && _now().difference(lastHeard) < onlineWindow, + batteryLevel: (json['b'] as num?)?.toInt(), + lastHeard: lastHeard, + latitude: (json['la'] as num?)?.toDouble(), + longitude: (json['lo'] as num?)?.toDouble(), + snr: (json['s'] as num?)?.toDouble() ?? 0, + viaMqtt: json['m'] as bool? ?? false, + ); + } catch (_) { + return null; + } + } + + @override + void dispose() { + _writeTimer?.cancel(); + unawaited(_sub?.cancel()); + super.dispose(); + } +} diff --git a/lib/core/network/etag_cache_store.dart b/lib/core/network/etag_cache_store.dart index 477ce3d1a..e4c8c53c6 100644 --- a/lib/core/network/etag_cache_store.dart +++ b/lib/core/network/etag_cache_store.dart @@ -16,12 +16,14 @@ /// gzip work for the whole batch done in a single isolate hop rather than one /// per row. /// -/// Eviction: last-used (`time`) older than [maxAge], then LRU trim to -/// [maxBytes]. Age expiry is one indexed `DELETE` per write (a batch counts as -/// one); the byte budget is **amortized**, because knowing the total means -/// summing every blob in the table — running that per tile written made a scrub -/// quadratic on the UI isolate. Last-used bumps are likewise **buffered** and -/// flushed in batches (same idea as [NetworkUsageStore]). All ops best-effort. +/// Eviction: byte budget only. Entries live until the store's total body size +/// passes [maxBytes]; then the least-recently-used rows drop, oldest (`time`) +/// first, until the total is back under the ceiling. Nothing expires by age — +/// a 30-day-old map tile that is still being hit keeps its slot. The byte +/// budget is **amortized**, because knowing the total means summing every blob +/// in the table — running that per tile written made a scrub quadratic on the +/// UI isolate. Last-used bumps are likewise **buffered** and flushed in +/// batches (same idea as [NetworkUsageStore]). All ops best-effort. /// /// When a [NetworkUsageStore] is wired, every successful [readBytes] serve /// records one hit + saved wire bytes — callers must not also meter those @@ -93,12 +95,7 @@ typedef _EncodedWrite = ({ /// See library doc. class EtagCacheStore { - EtagCacheStore( - this._db, { - this.maxAge = const Duration(days: 7), - this.maxBytes = defaultMaxBytes, - this._usage, - }); + EtagCacheStore(this._db, {this.maxBytes = defaultMaxBytes, this._usage}); final Database _db; @@ -107,14 +104,13 @@ class EtagCacheStore { /// at the interceptor / MapLibre put path. final NetworkUsageStore? _usage; - /// Entries whose **last-used** is older than this are swept on the next write. - final Duration maxAge; - - /// Soft ceiling on `SUM(LENGTH(body))` — least-recently-used rows drop first. + /// Ceiling on `SUM(LENGTH(body))` — least-recently-used rows drop first, + /// oldest (`time`) first, only once the store is over it. Entries never + /// expire by age. final int maxBytes; - /// Default size budget (~150 MB of body blobs on disk). - static const int defaultMaxBytes = 150 * 1024 * 1024; + /// Default size budget (~350 MB of body blobs on disk). + static const int defaultMaxBytes = 350 * 1024 * 1024; /// SQLite page-cache size in kibibytes (negative PRAGMA = KiB, not pages). static const int defaultPageCacheKiB = 25 * 1024; @@ -433,14 +429,12 @@ class EtagCacheStore { /// Post-write maintenance, split by what it actually costs. /// - /// **Age expiry** is one indexed `DELETE` and normally removes nothing, so it - /// runs every write — and because tile traffic goes through - /// [writeBytesBatch], "every write" already means once per burst. - /// /// **The byte budget** is the expensive half: knowing the total means summing /// every blob in the table. That is amortized over [_sweepEvery] writes, or /// forced the moment the running total says the store is over budget. Doing - /// it per write made a viewport of tiles a full-table scan per tile. + /// it per write made a viewport of tiles a full-table scan per tile. Nothing + /// expires by age — only the budget trims, and only once the store is over + /// it. Future _noteWrite(int addedBytes, int rows) async { _writesSinceSweep += rows; final tracked = _trackedBytes; @@ -453,16 +447,9 @@ class EtagCacheStore { return; } - // Eviction orders by `time` (last-used) — land buffered bumps first, or a + // Trim orders by `time` (last-used) — land buffered bumps first, or a // just-read entry would look untouched and be swept. await _flushTouches(); - await _db.delete( - _table, - where: 'time < ?', - whereArgs: [ - DateTime.now().millisecondsSinceEpoch - maxAge.inMilliseconds, - ], - ); if (tracked != null && _trackedBytes! <= maxBytes && @@ -472,8 +459,10 @@ class EtagCacheStore { await _trimToBudget(); } - /// Recounts stored bytes and drops least-recently-used rows until the total - /// is within [maxBytes]. + /// Recounts stored bytes and drops least-recently-used rows — oldest + /// (`time`) first — until the total is within [maxBytes]. A no-op (beyond + /// the recount) when the store is under the ceiling: the budget trims only + /// when it is actually over. Future _trimToBudget() async { _writesSinceSweep = 0; var total = await _measureBytes(); @@ -516,6 +505,15 @@ class EtagCacheStore { } catch (_) {} } + /// Shrinks the database file back to its contents (free pages from cleared + /// rows return to the OS — a body budget of 350 MB does not shrink the file + /// on its own). Costly: only call after a full [clear], never per-write. + Future compact() async { + try { + await _db.execute('VACUUM'); + } catch (_) {} + } + /// Row count and total stored body bytes — for the Debug page. Future stats() async { try { diff --git a/lib/core/notifications/notification_channels.dart b/lib/core/notifications/notification_channels.dart index a28b96908..459a0e714 100644 --- a/lib/core/notifications/notification_channels.dart +++ b/lib/core/notifications/notification_channels.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; /// /// Ported from the legacy app: 21 alert channels across 5 groups (EEW, /// earthquake, weather, tsunami, other) — each with its own importance, critical- -/// alert flag, sound (`resource://raw/` — Android `.ogg` / iOS `.aiff`), +/// alert flag, sound (`resource://raw/` — Android `.mp3` / iOS `.aiff`), /// and vibration — plus a standalone `background` service channel. Android caches /// a channel's settings after first creation, so [version] is bumped whenever a /// definition changes to force a re-create on next launch. (Adding a brand-new @@ -37,6 +37,10 @@ abstract final class NotificationChannels { channelGroupKey: 'group_tsunami', channelGroupName: '海嘯', ), + NotificationChannelGroup( + channelGroupKey: 'group_mesh', + channelGroupName: 'LoRa 網狀網路', + ), NotificationChannelGroup( channelGroupKey: 'group_other', channelGroupName: '其他', @@ -356,6 +360,32 @@ abstract final class NotificationChannels { enableVibration: true, vibrationPattern: lowVibrationPattern, ), + // Locally raised, never pushed: the mesh is an off-grid path, so these two + // are posted by the app itself from the BLE link (see `MeshAlerts`) — there + // is no server involved and no internet needed for them to arrive. + NotificationChannel( + channelGroupKey: 'group_mesh', + channelKey: 'mesh_message', + channelName: 'Meshtastic 訊息', + channelDescription: '從 LoRa 網狀網路收到的訊息', + importance: NotificationImportance.High, + defaultColor: const Color(0xFF4CAF50), + ledColor: const Color(0xFF4CAF50), + playSound: true, + vibrationPattern: lowVibrationPattern, + ), + NotificationChannel( + channelGroupKey: 'group_mesh', + channelKey: 'mesh_node', + channelName: 'Meshtastic 新節點', + channelDescription: '第一次聽到某個節點時通知', + importance: NotificationImportance.Low, + defaultColor: const Color(0xFF4CAF50), + channelShowBadge: false, + playSound: false, + enableVibration: false, + enableLights: false, + ), // Standalone (no group): a low-importance, silent channel for any background // service notice. Kept for parity with the legacy catalogue; the rewrite's // background location is geofence-based (no persistent notification), so it diff --git a/lib/core/platform/device_info.dart b/lib/core/platform/device_info.dart index 1d94623ad..34fce111d 100644 --- a/lib/core/platform/device_info.dart +++ b/lib/core/platform/device_info.dart @@ -10,6 +10,7 @@ class DeviceDetails { required this.osVersion, this.sdkInt, this.identifier, + this.totalMemoryMb, }); /// Device maker / OEM — `Build.MANUFACTURER` (Android) or `Apple` (iOS). @@ -29,6 +30,9 @@ class DeviceDetails { /// Vendor identifier (iOS `identifierForVendor`) or Android ID; may be null. final String? identifier; + + /// Total physical RAM in MiB; null if the platform couldn't report it. + final int? totalMemoryMb; } /// Native device-info accessor backed by a platform [MethodChannel], replacing @@ -52,6 +56,7 @@ abstract final class DeviceInfoService { final osVersion = (raw['osVersion'] as String?) ?? ''; final sdkInt = (raw['sdkInt'] as num?)?.toInt(); final identifier = raw['identifier'] as String?; + final totalMemoryMb = (raw['totalMemoryMb'] as num?)?.toInt(); return DeviceDetails( manufacturer: manufacturer, @@ -59,6 +64,7 @@ abstract final class DeviceInfoService { osVersion: osVersion, sdkInt: sdkInt, identifier: identifier, + totalMemoryMb: totalMemoryMb, ); } diff --git a/lib/core/platform/render_tier.dart b/lib/core/platform/render_tier.dart new file mode 100644 index 000000000..2d9e6725b --- /dev/null +++ b/lib/core/platform/render_tier.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'package:dpip/core/platform/device_info.dart'; + +/// How much GPU/CPU a device can spend on decorative animation. +/// +/// The animated weather backdrop (full-screen fragment shaders, ~1800 rain +/// particles) is authored for a mid-range phone. On the low end it should keep +/// the same look at a lower resolution and particle budget rather than drop +/// frames — a softer upscale reads as the same sky, a stutter does not. +enum RenderTier { + /// Full-quality: the authored resolution and particle pools. + high, + + /// Reduced: render at a lower internal scale and smaller particle pools. + low, +} + +/// Picks the tier from a device snapshot. +/// +/// RAM is the cheap proxy for GPU class on Android — the devices that can't +/// drive a full-screen shader stack at 60 fps are the same 2–4 GB ones. iOS is +/// never downgraded (its oldest supported devices still outdraw them), and an +/// unknown/absent reading stays high — never degrade an experience on a +/// measurement we didn't get. +/// +/// [isAndroid] is injectable so the host platform (`dart:io`) doesn't leak +/// into tests; production callers omit it. +RenderTier renderTierFor(DeviceDetails device, {bool? isAndroid}) { + final totalMb = device.totalMemoryMb; + if ((isAndroid ?? Platform.isAndroid) == false || totalMb == null) { + return RenderTier.high; + } + return totalMb < 4096 ? RenderTier.low : RenderTier.high; +} diff --git a/lib/core/platform/screen_wake.dart b/lib/core/platform/screen_wake.dart new file mode 100644 index 000000000..83541dd2a --- /dev/null +++ b/lib/core/platform/screen_wake.dart @@ -0,0 +1,68 @@ +/// Keeps the display from sleeping while a screen needs to stay readable. +/// +/// A platform channel rather than a package, like the rest of `core/platform/`: +/// each side is two lines of native code (`FLAG_KEEP_SCREEN_ON` on Android, +/// `isIdleTimerDisabled` on iOS) and both are scoped to the foreground app, so +/// neither can leak a held screen into the background. +/// +/// Every caller must pair [enable] with [disable] — use [ScreenWakeScope], +/// which does it for you around a widget's lifetime. +library; + +import 'package:dpip/core/logging/log.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +abstract final class ScreenWake { + const ScreenWake._(); + + static const MethodChannel _channel = MethodChannel( + 'com.exptech.dpip/screen_wake', + ); + + /// Holds the screen on. Best-effort: a platform that can't do it logs and + /// carries on rather than failing the screen that asked. + static Future enable() => _set(true); + + /// Releases the hold. + static Future disable() => _set(false); + + static Future _set(bool keepAwake) async { + try { + await _channel.invokeMethod(keepAwake ? 'enable' : 'disable'); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'screen wake'); + } + } +} + +/// Holds the screen awake for as long as [child] is mounted. +/// +/// The pairing lives here so no screen can forget the release half — the +/// failure mode of a forgotten `disable` is a phone that never sleeps again, +/// which the user would have no way to connect back to this app. +class ScreenWakeScope extends StatefulWidget { + const ScreenWakeScope({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _ScreenWakeScopeState(); +} + +class _ScreenWakeScopeState extends State { + @override + void initState() { + super.initState(); + ScreenWake.enable(); + } + + @override + void dispose() { + ScreenWake.disable(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/lib/core/realtime/realtime_service.dart b/lib/core/realtime/realtime_service.dart index c2a0a22dc..92f2cfa62 100644 --- a/lib/core/realtime/realtime_service.dart +++ b/lib/core/realtime/realtime_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dpip/core/realtime/realtime_channel.dart'; import 'package:dpip/core/realtime/server_clock.dart'; import 'package:dpip/core/realtime/ticker.dart'; @@ -32,9 +34,21 @@ class RealtimeService { /// Starts every registered channel and the periodic clock resync (call after /// the first frame). The initial sync is kicked off at bootstrap; the ticker /// then re-anchors every [_clockSyncInterval]. - void startAll() { + /// + /// [stagger] offsets each channel's first poll by one interval, so a burst + /// of feeds doesn't all hit the network (and the UI isolate's JSON decode) + /// on the same post-first-frame tick — a 250 ms lead on EEW is nothing, but + /// it smooths the first seconds on a slow phone. Zero (the default) keeps + /// [start] synchronous, which the service tests rely on. + void startAll({Duration stagger = Duration.zero}) { + var delay = Duration.zero; for (final channel in _channels) { - channel.start(); + if (stagger == Duration.zero) { + channel.start(); + } else { + unawaited(Future.delayed(delay, channel.start)); + delay += stagger; + } } _startClockSync(); } diff --git a/lib/core/settings/preference_keys.dart b/lib/core/settings/preference_keys.dart index af7605952..a877245b3 100644 --- a/lib/core/settings/preference_keys.dart +++ b/lib/core/settings/preference_keys.dart @@ -109,6 +109,46 @@ abstract final class PreferenceKeys { 'location.deviceLocationUpdatedAtMs', ); + /// The radio `MeshLink` keeps reconnecting to (BLE id), and its name for + /// display. Their presence *is* the intent to stay connected — removed by + /// `MeshLink.detach()`, which is the only thing that stops reconnection. + static const PrefKey meshDeviceId = PrefKey._( + 'meshtastic.deviceId', + ); + static const PrefKey meshDeviceName = PrefKey._( + 'meshtastic.deviceName', + ); + + /// Local (never pushed) mesh notifications. Messages default on; new-node + /// alerts default **off** — a busy mesh introduces neighbours all day. See + /// `MeshAlerts`. + static const PrefKey meshNotifyMessages = PrefKey._( + 'meshtastic.notifyMessages', + ); + static const PrefKey meshNotifyNodes = PrefKey._( + 'meshtastic.notifyNodes', + ); + + /// Whether the mesh map layer hides MQTT-only nodes. Defaults to **true** — + /// see `MeshNodeStore.excludeMqtt`. + static const PrefKey meshExcludeMqtt = PrefKey._( + 'map.meshExcludeMqtt', + ); + + /// The last known mesh node table (JSON strings, most-recently-heard first). + /// Survives reconnects and restarts so the map and the node list have + /// something to show with no radio attached. See `MeshNodeStore`. + static const PrefKey> meshNodes = PrefKey>._( + 'meshtastic.nodes', + ); + + /// The mesh message log — the most recent messages as JSON strings, newest + /// first. See `MeshChatController`. The radio's own replay queue is small, + /// shared with telemetry, and lost on reboot, so the log is kept here. + static const PrefKey> meshMessages = PrefKey>._( + 'meshtastic.messages', + ); + /// Selected LB / Core API region. See `RegionSelection`. /// /// Colon-form kept as-is (pre-existing storage address). @@ -118,4 +158,16 @@ abstract final class PreferenceKeys { static const PrefKey regionCore = PrefKey._( 'network:region:core', ); + + /// The most recent satellite element set, and when it was fetched. + /// + /// Cached as the raw TLE text: it is a few hundred bytes, it is the format + /// every source speaks, and keeping it verbatim means the parser is the only + /// thing that has to understand it. + static const PrefKey satelliteElements = PrefKey._( + 'astro:satellite:tle', + ); + static const PrefKey satelliteElementsFetchedAt = PrefKey._( + 'astro:satellite:fetchedAt', + ); } diff --git a/lib/core/storage/app_storage_scan.dart b/lib/core/storage/app_storage_scan.dart new file mode 100644 index 000000000..76f269341 --- /dev/null +++ b/lib/core/storage/app_storage_scan.dart @@ -0,0 +1,240 @@ +/// Sandbox disk-usage scan: what is actually on disk, where, and why. +/// +/// iOS Settings reports the whole sandbox, which is routinely far larger than +/// the ETag-cache budget (350 MB of *body* blobs) — the SQLite file carries +/// page/free-space overhead, and debug runs leave JIT kernel snapshots in tmp. +/// The system URL cache used to keep its own copy of HTTP responses too, but +/// it is now disabled at startup ([StorageScanner.configure]) so every cached +/// byte lives in the app's own SQLite; the Debug page needs real numbers, so +/// this scans the platform's cache/support/document/tmp trees once and splits +/// the result into the app's own top-level directories plus the biggest +/// individual files. +library; + +import 'package:dpip/core/logging/log.dart'; +import 'package:flutter/services.dart'; + +/// One scanned top-level directory (or large file). +class StorageEntry { + const StorageEntry({required this.path, required this.bytes}); + + final String path; + final int bytes; + + /// File name (or last path component) — what the Debug page shows. + String get name => path.split('/').last; + + /// The last two path components (`tmp/main.dart.dill`) — enough to tell + /// which sandbox tree a file lives in when several share a name. + String get shortPath { + final parts = path.split('/'); + return parts.length >= 2 + ? '${parts[parts.length - 2]}/${parts.last}' + : name; + } +} + +/// Result of a sandbox scan. +class StorageScan { + const StorageScan({ + required this.totalBytes, + required this.dirs, + required this.files, + }); + + /// Everything under the sandbox's cache/support/document/tmp trees. + final int totalBytes; + + /// Top-level directories, each with its total size. + final List dirs; + + /// The largest individual files (above the platform's reporting floor), + /// descending. + final List files; +} + +/// Formats a byte count as a compact human string (B / KB / MB / GB). +String formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + const units = ['KB', 'MB', 'GB', 'TB']; + var value = bytes / 1024; + var unit = 0; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit++; + } + return '${value.toStringAsFixed(value < 10 ? 1 : 0)} ${units[unit]}'; +} + +/// One slice of the storage breakdown — a label plus its bytes. +class StorageSlice { + const StorageSlice({required this.label, required this.bytes}); + + final String label; + final int bytes; +} + +/// Splits a [StorageScan] into the app's own categories. +/// +/// Known file names are matched first (SQLite DB + its `-wal`/`-shm` +/// companions, MapLibre's DB, the engine's caches, the system HTTP cache), +/// subtracted from the directory that contains them, and the remainder is +/// attributed to the directory itself. Files below the platform's reporting +/// floor stay inside their directory bucket, so slices may not sum exactly to +/// [StorageScan.totalBytes] — they are close enough for a pie chart. +List storageBreakdown(StorageScan scan) { + final slices = {}; + final dirBytes = {for (final d in scan.dirs) d.path: d.bytes}; + + // /var is a symlink to /private/var on iOS; normalize either spelling so a + // file always matches the directory that contains it. + String varPath(String path) => path.replaceFirst('/private/var', '/var'); + + String? dirOf(String path) { + for (final dir in scan.dirs) { + if (path.startsWith(dir.path)) return dir.path; + // iOS can hand one side /private/var and the other /var — treat them + // as the same tree so the subtraction still lands. + if (varPath(path).startsWith(varPath(dir.path))) return dir.path; + } + return null; + } + + void known(String label, bool Function(StorageEntry) match) { + var sum = 0; + for (final file in scan.files) { + if (!match(file)) continue; + sum += file.bytes; + final dir = dirOf(file.path); + if (dir != null) dirBytes[dir] = dirBytes[dir]! - file.bytes; + } + if (sum > 0) slices[label] = (slices[label] ?? 0) + sum; + } + + known('ETag cache (SQLite)', (f) => f.name.startsWith('http_etag_cache.db')); + known( + 'MapLibre', + (f) => f.path.contains('MapLibre') || f.path.contains('mapbox'), + ); + known( + 'Flutter engine', + // `io.flutter` covers the engine's own caches; `*.dill` covers the + // debug-mode kernel snapshots flutter run leaves in tmp on every launch + // (tens of MB each, and they pile up — they are not app data). + (f) => f.path.contains('io.flutter') || f.name.endsWith('.dill'), + ); + known( + 'System HTTP cache', + // Residue only: [configure] disables the disk cache at startup, so this + // slice exists to explain bytes left by older builds until a clear. + (f) => f.path.contains('HTTPCache') || f.path.contains('URLCache'), + ); + + for (final dir in scan.dirs) { + final bytes = dirBytes[dir.path] ?? 0; + if (bytes <= 0) { + continue; + } + // A directory that gave up a known file keeps the rest; say so in the + // label so the pie reads as ETag being *part of* Caches, not a sibling. + final label = bytes < dir.bytes ? '${dir.name} (other)' : dir.name; + slices[label] = (slices[label] ?? 0) + bytes; + } + + final accounted = slices.values.fold(0, (a, b) => a + b); + final out = [ + for (final entry in slices.entries) + StorageSlice(label: entry.key, bytes: entry.value), + ]..sort((a, b) => b.bytes.compareTo(a.bytes)); + // Files below the platform's reporting floor are invisible to the + // per-file pass, so fold the difference into "Other" — the pie then sums + // exactly to what Settings reports. + if (accounted < scan.totalBytes) { + out.add(StorageSlice(label: 'Other', bytes: scan.totalBytes - accounted)); + } + return out; +} + +/// App-owned native bridge (iOS `StorageScanPlugin`, Android +/// `StorageScanChannel`). +class StorageScanner { + const StorageScanner(); + + static const MethodChannel _channel = MethodChannel( + 'com.exptech.dpip/storage_scan', + ); + + /// Measures the sandbox. Best-effort — an error yields an empty scan rather + /// than a broken Debug page. + Future scan() async { + try { + final raw = await _channel.invokeMapMethod('scan'); + if (raw == null) { + return const StorageScan(totalBytes: 0, dirs: [], files: []); + } + List entries(String key) => [ + for (final row in (raw[key] as List? ?? const [])) + StorageEntry( + path: (row as Map)['path'] as String, + bytes: (row['bytes'] as num).toInt(), + ), + ]; + return StorageScan( + totalBytes: (raw['totalBytes'] as num?)?.toInt() ?? 0, + dirs: entries('dirs'), + files: entries('files'), + ); + } on MissingPluginException { + return const StorageScan(totalBytes: 0, dirs: [], files: []); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'storage scan'); + return const StorageScan(totalBytes: 0, dirs: [], files: []); + } + } + + /// One-shot startup tuning: the system disk HTTP cache (iOS NSURLCache) is + /// **disabled** — MapLibre's downloads are persisted in the app's own SQLite + /// through the tile bridge, so a second disk copy is pure overhead — and any + /// residue from before this ran is dropped. Android has no equivalent to + /// configure. + Future configure() async { + try { + await _channel.invokeMethod('configure'); + } on MissingPluginException { + // Platform without the handler. + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'storage configure'); + } + } + + /// Empties the OS-level HTTP cache (iOS NSURLCache). Android is a no-op — + /// the plugins' caches live in the app's own SQLite, already covered by the + /// ETag-cache clear. + Future clearSystemHttpCache() async { + try { + await _channel.invokeMethod('clearSystemHttpCache'); + } on MissingPluginException { + // Platform without the handler. + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'clear system HTTP cache'); + } + } + + /// Empties the sandbox's temporary directory. iOS-only: Android has no + /// separate tmp tree — `cacheDir` is the whole story and is covered by the + /// cache clear already. + /// + /// Nothing the app owns lives in tmp permanently, so this is always safe; + /// it exists because some native path (historically MapLibre's transient + /// tile work, aborted snapshot writes) can pile up hundreds of MB there + /// that no other clear reaches. + Future clearTmp() async { + try { + await _channel.invokeMethod('clearTmp'); + } on MissingPluginException { + // Platform without the handler (Android — no-op by design). + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'clear tmp'); + } + } +} diff --git a/lib/features/changelog/domain/release_note.freezed.dart b/lib/features/changelog/domain/release_note.freezed.dart index d1cb4e5f5..69fea2687 100644 --- a/lib/features/changelog/domain/release_note.freezed.dart +++ b/lib/features/changelog/domain/release_note.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'release_note.dart'; @@ -9,6 +9,7 @@ part of 'release_note.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -71,7 +72,7 @@ class _$ReleaseNoteCopyWithImpl<$Res> /// Create a copy of ReleaseNote /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? tagName = null,Object? name = null,Object? body = null,Object? prerelease = null,Object? publishedAt = null,}) { - return _then(_self.copyWith( + return _then(ReleaseNote( tagName: null == tagName ? _self.tagName : tagName // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,body: null == body ? _self.body : body // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/changelog/presentation/pages/changelog_page.dart b/lib/features/changelog/presentation/pages/changelog_page.dart index 3eeb6c54e..c28db9fac 100644 --- a/lib/features/changelog/presentation/pages/changelog_page.dart +++ b/lib/features/changelog/presentation/pages/changelog_page.dart @@ -154,9 +154,8 @@ class _ReleaseTile extends StatelessWidget { ? Icons.science_outlined : Icons.verified_outlined; final title = note.name.isEmpty ? note.tagName : note.name; - final date = DateFormat.yMMMd( - Localizations.localeOf(context).toString(), - ).format(note.publishedAt.toLocal()); + final date = DateFormat.yMMMd(Localizations.localeOf(context).toString()) + .format(note.publishedAt.toLocal()); final emphasized = isCurrent || expanded; return CustomPaint( diff --git a/lib/features/data/presentation/observer_place.dart b/lib/features/data/presentation/observer_place.dart new file mode 100644 index 000000000..35c2fd9fb --- /dev/null +++ b/lib/features/data/presentation/observer_place.dart @@ -0,0 +1,36 @@ +/// Which place the astronomy pages compute for. +/// +/// Rise, set, twilight and pointing are all statements about a location, so +/// every astronomy page has to answer "where?" the same way and then *say* the +/// answer. A time with no place attached is just a number. +/// +/// The order is: the township the user has selected, then the one GPS +/// reported, then the nearest township to a documented fallback. The fallback +/// is resolved through the directory rather than used as bare coordinates, so +/// the page always has a real township name to show — never a silent +/// assumption. +library; + +import 'package:dpip/core/geo/town.dart'; +import 'package:dpip/core/geo/town_directory.dart'; +import 'package:dpip/core/settings/home_area.dart'; +import 'package:dpip/core/settings/region_store.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +/// Used only when no township is known: Taipei City Hall. +const ({double lat, double lng}) fallbackPlace = (lat: 25.0330, lng: 121.5654); + +/// The township this page's times belong to, or null if the directory is +/// empty (which only happens if the bundled asset failed to load). +Town? observerTown(BuildContext context) { + final directory = context.read(); + final regions = context.watch(); + final code = switch (regions.selected) { + SavedArea(:final code) => code, + CurrentArea(:final code) => code, + NationwideArea() => null, + }; + return directory.byCode(code ?? regions.currentCode) ?? + directory.nearest(fallbackPlace.lat, fallbackPlace.lng); +} diff --git a/lib/features/data/presentation/pages/almanac_page.dart b/lib/features/data/presentation/pages/almanac_page.dart new file mode 100644 index 000000000..d84200d28 --- /dev/null +++ b/lib/features/data/presentation/pages/almanac_page.dart @@ -0,0 +1,172 @@ +/// 曆法 — the lunisolar date, and the eclipses ahead. +/// +/// Both are derived, not tabulated: the calendar from the new moons and the +/// solar terms this package computes, the eclipses from the same positions +/// asked a different question. So the page works for any year, not for the +/// span someone once pasted in. +library; + +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/eclipse.dart'; +import 'package:dpip/core/astro/lunisolar_calendar.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/features/data/presentation/observer_place.dart'; +import 'package:dpip/features/data/presentation/widgets/astro_card.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class AlmanacPage extends StatelessWidget { + const AlmanacPage({super.key}); + + static final DateFormat _date = DateFormat('yyyy/MM/dd'); + static final DateFormat _stamp = DateFormat('yyyy/MM/dd HH:mm'); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final town = observerTown(context); + final now = AppTime.utc; + final lunar = LunisolarCalendar.of(now); + + final lunarEclipses = []; + var cursor = now; + for (var i = 0; i < 3; i++) { + final eclipse = Eclipses.nextLunar(cursor, withinDays: 800); + if (eclipse == null) break; + lunarEclipses.add(eclipse); + cursor = eclipse.peak.add(const Duration(days: 20)); + } + + final solarEclipses = []; + if (town != null) { + cursor = now; + for (var i = 0; i < 2; i++) { + final eclipse = Eclipses.nextSolar( + cursor, + latitude: town.lat, + longitude: town.lng, + withinDays: 4000, + ); + if (eclipse == null) break; + solarEclipses.add(eclipse); + cursor = eclipse.peak.add(const Duration(days: 20)); + } + } + + return Scaffold( + appBar: AppBar(title: Text(l10n.almanacTitle)), + body: ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + SectionHeader(l10n.almanacSectionToday), + AstroReadings( + rows: [ + ( + Icons.today_outlined, + l10n.almanacGregorian, + _date.format(AppTime.utc8), + ), + ( + Icons.brightness_3_outlined, + l10n.almanacLunar, + _lunarLabel(l10n, lunar), + ), + ( + Icons.filter_vintage_outlined, + l10n.almanacYear, + // l10n-ignore: the sexagenary pair is a proper name in Chinese + '${lunar.sexagenaryYear} · ${_zodiac(l10n, lunar.zodiacIndex)}', + ), + ( + Icons.event_outlined, + l10n.almanacMonthLength, + lunar.monthLength == 30 + ? l10n.almanacLongMonth + : l10n.almanacShortMonth, + ), + ], + ), + SectionHeader(l10n.almanacSectionLunarEclipses), + AstroReadings( + rows: [ + for (final eclipse in lunarEclipses) + ( + Icons.brightness_1_outlined, + '${_eclipseKind(l10n, eclipse.kind)} · ' + '${_stamp.format(AppTime.taipei(eclipse.peak))}', + // l10n-ignore: a magnitude + eclipse.magnitude.toStringAsFixed(2), + ), + ], + ), + SectionHeader( + l10n.almanacSectionSolarEclipses, + trailing: town == null + ? null + : Text( + town.fullName, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + AstroReadings( + rows: solarEclipses.isEmpty + ? [ + ( + Icons.wb_sunny_outlined, + l10n.almanacNoSolarEclipse, + l10n.moonNoEvent, + ), + ] + : [ + for (final eclipse in solarEclipses) + ( + Icons.wb_sunny_outlined, + '${_eclipseKind(l10n, eclipse.kind)} · ' + '${_stamp.format(AppTime.taipei(eclipse.peak))}', + // l10n-ignore: a magnitude + eclipse.magnitude.toStringAsFixed(2), + ), + ], + ), + ], + ), + ); + } + + static String _lunarLabel(AppLocalizations l10n, LunisolarDate date) => + l10n.almanacLunarDate( + date.isLeapMonth ? l10n.almanacLeapPrefix : '', + date.month, + date.day, + ); + + static String _zodiac(AppLocalizations l10n, int index) => switch (index) { + 0 => l10n.zodiacRat, + 1 => l10n.zodiacOx, + 2 => l10n.zodiacTiger, + 3 => l10n.zodiacRabbit, + 4 => l10n.zodiacDragon, + 5 => l10n.zodiacSnake, + 6 => l10n.zodiacHorse, + 7 => l10n.zodiacGoat, + 8 => l10n.zodiacMonkey, + 9 => l10n.zodiacRooster, + 10 => l10n.zodiacDog, + _ => l10n.zodiacPig, + }; + + static String _eclipseKind(AppLocalizations l10n, EclipseKind kind) => + switch (kind) { + EclipseKind.total => l10n.eclipseTotal, + EclipseKind.partial => l10n.eclipsePartial, + EclipseKind.annular => l10n.eclipseAnnular, + EclipseKind.penumbral => l10n.eclipsePenumbral, + EclipseKind.none => l10n.moonNoEvent, + }; +} diff --git a/lib/features/data/presentation/pages/data_page.dart b/lib/features/data/presentation/pages/data_page.dart index c5de7e17b..64b4f125b 100644 --- a/lib/features/data/presentation/pages/data_page.dart +++ b/lib/features/data/presentation/pages/data_page.dart @@ -103,6 +103,74 @@ class DataPage extends StatelessWidget { ), ], ), + SectionHeader(l10n.dataSectionAstronomy), + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + 0, + AppSpacing.lg, + AppSpacing.sm, + ), + crossAxisSpacing: AppSpacing.sm, + mainAxisSpacing: AppSpacing.sm, + childAspectRatio: 1.45, + children: [ + for (final (route, icon, label, accent) + in <(String, IconData, String, Color)>[ + ( + AppRoutes.moon, + Icons.nightlight_outlined, + l10n.moonTitle, + colors.tertiary, + ), + ( + AppRoutes.sun, + Icons.wb_sunny_outlined, + l10n.sunTitle, + colors.primary, + ), + ( + AppRoutes.planets, + Icons.blur_circular_outlined, + l10n.planetsTitle, + colors.secondary, + ), + ( + AppRoutes.tonight, + Icons.dark_mode_outlined, + l10n.tonightTitle, + colors.primary, + ), + ( + AppRoutes.skyChart, + Icons.auto_awesome_outlined, + l10n.skyChartTitle, + colors.tertiary, + ), + ( + AppRoutes.almanac, + Icons.calendar_month_outlined, + l10n.almanacTitle, + colors.secondary, + ), + ( + AppRoutes.tide, + Icons.waves_outlined, + l10n.tideTitle, + colors.primary, + ), + ]) + _RankingGridTile( + icon: icon, + title: label, + accent: accent, + onTap: () => context.pushNamed(route), + ), + ], + ), ], ), ); diff --git a/lib/features/data/presentation/pages/moon_page.dart b/lib/features/data/presentation/pages/moon_page.dart new file mode 100644 index 000000000..f8a701db0 --- /dev/null +++ b/lib/features/data/presentation/pages/moon_page.dart @@ -0,0 +1,699 @@ +/// The Moon, for any moment you scrub to. +/// +/// Everything is computed on the device. The position comes from +/// `core/astro/moon_ephemeris.dart` — one Meeus series, pinned against JPL +/// Horizons and the USNO — and the surface is the real Moon: NASA/GSFC's CGI +/// Moon Kit colour and elevation maps in `assets/astro/`, projected onto a +/// sphere and lit for the chosen instant by +/// `shaders/weather/moon_display.frag`. +/// +/// The maps are bundled rather than fetched on purpose: this is a +/// disaster-preparedness app, and a page that needs the network to draw the +/// Moon is a page that stops working on the night it would most be looked at. +/// +/// Two controls, because the Moon has two timescales. The **timeline** steps +/// two hours — the resolution at which the terminator visibly moves and the +/// rise/set times shift — and the **calendar** steps a month, showing the +/// whole lunation as a shape. They drive the same selection, so neither can +/// point somewhere the other cannot reach. +/// +/// Rise and set are for a place, so the page says which: the current township +/// when location is available, otherwise the nearest township to a documented +/// fallback, named either way rather than silently assumed. +library; + +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/moon_orientation.dart'; +import 'package:dpip/core/astro/moon_phase.dart'; +import 'package:dpip/core/astro/moon_rise_set.dart'; +import 'package:dpip/core/geo/town.dart'; +import 'package:dpip/core/geo/town_directory.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/core/settings/home_area.dart'; +import 'package:dpip/core/settings/region_store.dart'; +import 'package:dpip/features/data/presentation/widgets/moon_calendar.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/map/map_layer.dart'; +import 'package:dpip/shared/map/map_timeline.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +/// Taiwan's fixed offset — the wall clock every date on this page is read in. +const Duration _taiwanOffset = Duration(hours: 8); + +/// Where the timeline starts and stops, either side of today. A full lunation +/// each way, so any phase is always reachable by scrubbing alone. +const int _daysEitherSide = 31; + +/// Timeline resolution. The Moon's elongation gains about half a degree an +/// hour, so two hours is roughly one degree — the coarsest step at which +/// consecutive frames still look different. +const int _stepHours = 2; + +/// Used only when no township is known. Taipei City Hall; resolved through the +/// directory so the page names a real township rather than a coordinate. +const ({double lat, double lng}) _fallbackPlace = (lat: 25.0330, lng: 121.5654); + +class MoonPage extends StatefulWidget { + const MoonPage({super.key}); + + @override + State createState() => _MoonPageState(); +} + +class _MoonPageState extends State { + /// Two-hourly frames spanning [_daysEitherSide] either side of today, built + /// once. The page's only real state is which one is selected. + late final List _frames; + late final int _nowIndex; + + late int _selectedIndex; + + /// The month the calendar is showing — follows the selection, but can also + /// be paged on its own without moving it. + late DateTime _visibleMonth; + + ui.FragmentProgram? _program; + ui.Image? _color; + ui.Image? _height; + Object? _loadError; + + // Numeric formats only, so no `intl` locale symbol data is needed. + static final DateFormat _dayMonth = DateFormat('M/d'); + static final DateFormat _clock = DateFormat('HH:mm'); + static final NumberFormat _grouped = NumberFormat.decimalPattern(); + + @override + void initState() { + super.initState(); + final now = AppTime.utc; + final today = AppTime.taipei(now); + // Midnight Taiwan time, [_daysEitherSide] back, as a UTC instant. + final start = DateTime.utc( + today.year, + today.month, + today.day, + ).subtract(_taiwanOffset).subtract(const Duration(days: _daysEitherSide)); + const count = (2 * _daysEitherSide * 24) ~/ _stepHours + 1; + _frames = [ + for (var i = 0; i < count; i++) + MapFrame( + id: 'moon$i', + time: start.add(Duration(hours: i * _stepHours)), + ), + ]; + _nowIndex = _indexAt(now); + _selectedIndex = _nowIndex; + _visibleMonth = today; + unawaited(_load()); + } + + /// The frame at or just before [utc], clamped to the range. + int _indexAt(DateTime utc) { + final steps = + utc.difference(_frames.first.time).inMinutes ~/ (_stepHours * 60); + return steps.clamp(0, _frames.length - 1); + } + + DateTime get _selected => _frames[_selectedIndex].time; + + /// The selected instant as Taipei wall time. + DateTime get _selectedLocal => AppTime.taipei(_selected); + + Future _load() async { + try { + final program = await ui.FragmentProgram.fromAsset( + 'shaders/weather/moon_display.frag', + ); + final color = await _decode('assets/astro/moon_color_2k.jpg'); + final height = await _decode('assets/astro/moon_height_1k.png'); + if (!mounted) { + color.dispose(); + height.dispose(); + return; + } + setState(() { + _program = program; + _color = color; + _height = height; + }); + } catch (error) { + if (!mounted) return; + setState(() => _loadError = error); + } + } + + Future _decode(String key) async { + final data = await rootBundle.load(key); + final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final frame = await codec.getNextFrame(); + codec.dispose(); + return frame.image; + } + + @override + void dispose() { + _color?.dispose(); + _height?.dispose(); + super.dispose(); + } + + void _select(int index) => setState(() { + _selectedIndex = index; + _visibleMonth = AppTime.taipei(_frames[index].time); + }); + + /// Jumps to [day] (Taipei wall time) keeping the time of day, so stepping + /// through the calendar compares like with like. + void _selectDay(DateTime day) { + final local = _selectedLocal; + final target = DateTime.utc( + day.year, + day.month, + day.day, + local.hour, + ).subtract(_taiwanOffset); + _select(_indexAt(target)); + } + + /// The township rise and set are computed for: the current GPS township, or + /// the selected saved one, or the nearest to [_fallbackPlace]. + Town? _observer(BuildContext context) { + final directory = context.read(); + final regions = context.watch(); + final selected = regions.selected; + final code = switch (selected) { + SavedArea(:final code) => code, + CurrentArea(:final code) => code, + NationwideArea() => null, + }; + return directory.byCode(code ?? regions.currentCode) ?? + directory.nearest(_fallbackPlace.lat, _fallbackPlace.lng); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final phase = MoonPhase.at(_selected); + final libration = MoonPhase.librationAt(_selected); + final town = _observer(context); + final local = _selectedLocal; + final riseSet = town == null + ? null + : MoonRiseSet.of( + DateTime.utc( + local.year, + local.month, + local.day, + ).subtract(_taiwanOffset), + latitude: town.lat, + longitude: town.lng, + ); + final orientation = town == null + ? null + : MoonOrientation.at( + _selected, + latitude: town.lat, + longitude: town.lng, + ); + final aboveHorizon = + town != null && + riseSet != null && + riseSet.isCircumpolar && + MoonRiseSet.aboveHorizon( + _selected, + latitude: town.lat, + longitude: town.lng, + ); + + return Scaffold( + appBar: AppBar( + title: Text(l10n.moonTitle), + actions: [ + // Only once the selection has left the present. Deliberately an icon: + // the timeline already writes "now" as the label for the current + // frame, and two different "now"s on one screen read as one thing. + if (_selectedIndex != _nowIndex) + IconButton( + onPressed: () => _select(_nowIndex), + icon: const Icon(Icons.today_outlined), + tooltip: l10n.moonNow, + ), + ], + ), + body: ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + _MoonStage( + color: _color, + height: _height, + program: _program, + error: _loadError, + phase: phase, + libration: Offset(libration.longitude, libration.latitude), + orientation: orientation, + title: _phaseName(l10n, phase.name), + // l10n-ignore: percentage readout + subtitle: '${(phase.brightness * 100).round()}%', + ), + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.lg, + AppSpacing.lg, + 0, + ), + child: MapTimeline( + frames: _frames, + selectedIndex: _selectedIndex, + onSelected: _select, + caption: l10n.moonTimelineCaption, + itemExtent: 22, + ), + ), + SectionHeader(l10n.moonSectionAppearance), + _StatCard( + rows: [ + ( + Icons.hourglass_bottom_outlined, + l10n.moonAge, + '${phase.ageInDays.toStringAsFixed(1)} ${l10n.moonDays}', + ), + ( + Icons.straighten_outlined, + l10n.moonDistance, + '${_grouped.format(phase.distanceKm.round())}' + ' ${l10n.moonKilometres}', + ), + ( + Icons.circle_outlined, + l10n.moonApparentSize, + // l10n-ignore: degree symbol + '${phase.apparentDiameterDegrees.toStringAsFixed(3)}°', + ), + ], + ), + SectionHeader( + l10n.moonSectionRiseSet, + trailing: town == null + ? null + : Text( + town.fullName, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + _StatCard( + rows: [ + ( + Icons.arrow_upward, + l10n.moonRise, + _eventLabel(l10n, riseSet?.rise, aboveHorizon: aboveHorizon), + ), + ( + Icons.arrow_downward, + l10n.moonSet, + _eventLabel(l10n, riseSet?.set, aboveHorizon: aboveHorizon), + ), + ], + ), + SectionHeader(l10n.moonSectionUpcoming), + _StatCard( + rows: [ + ( + Icons.brightness_1_outlined, + l10n.moonNextFullMoon, + _stamp(MoonPhase.nextFullMoon(_selected)), + ), + ( + Icons.brightness_3_outlined, + l10n.moonNextNewMoon, + _stamp(MoonPhase.nextNewMoon(_selected)), + ), + ], + ), + SectionHeader(l10n.moonSectionCalendar), + _Card( + padding: const EdgeInsets.fromLTRB( + AppSpacing.sm, + AppSpacing.xs, + AppSpacing.sm, + AppSpacing.md, + ), + child: MoonCalendar( + month: _visibleMonth, + selected: local, + today: AppTime.utc8, + firstDay: AppTime.taipei(_frames.first.time), + lastDay: AppTime.taipei(_frames.last.time), + onMonthChanged: (month) => setState(() => _visibleMonth = month), + onDaySelected: _selectDay, + phaseAt: (day) => MoonPhase.angleAt( + DateTime.utc( + day.year, + day.month, + day.day, + 12, + ).subtract(_taiwanOffset), + ), + ), + ), + ], + ), + ); + } + + /// `M/d HH:mm` in Taipei wall time — the form the rest of the page uses. + String _stamp(DateTime utc) { + final local = AppTime.taipei(utc); + return '${_dayMonth.format(local)} ${_clock.format(local)}'; + } + + /// A rise or set time, or why there isn't one. A day with no moonrise is + /// ordinary (the Moon slips ~50 minutes later daily), so it gets a real + /// answer rather than a dash. + String _eventLabel( + AppLocalizations l10n, + DateTime? event, { + required bool aboveHorizon, + }) { + if (event != null) return _clock.format(AppTime.taipei(event)); + return aboveHorizon ? l10n.moonAlwaysUp : l10n.moonNoEvent; + } + + String _phaseName(AppLocalizations l10n, MoonPhaseName name) => + switch (name) { + MoonPhaseName.newMoon => l10n.moonPhaseNew, + MoonPhaseName.waxingCrescent => l10n.moonPhaseWaxingCrescent, + MoonPhaseName.firstQuarter => l10n.moonPhaseFirstQuarter, + MoonPhaseName.waxingGibbous => l10n.moonPhaseWaxingGibbous, + MoonPhaseName.fullMoon => l10n.moonPhaseFull, + MoonPhaseName.waningGibbous => l10n.moonPhaseWaningGibbous, + MoonPhaseName.lastQuarter => l10n.moonPhaseLastQuarter, + MoonPhaseName.waningCrescent => l10n.moonPhaseWaningCrescent, + }; +} + +/// The hero: the lit globe over deep space, with the phase named beneath it. +class _MoonStage extends StatelessWidget { + const _MoonStage({ + required this.color, + required this.height, + required this.program, + required this.error, + required this.phase, + required this.libration, + required this.orientation, + required this.title, + required this.subtitle, + }); + + final ui.Image? color; + final ui.Image? height; + final ui.FragmentProgram? program; + final Object? error; + final MoonPhase phase; + final Offset libration; + + /// How the globe is tilted for this observer — see `moon_orientation.dart`. + /// Null before a place is known, in which case the canonical north-up, + /// terminator-vertical view is drawn rather than a guessed one. + final MoonOrientation? orientation; + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + // Big enough to be the point of the page, capped so the readings under it + // are still on screen on a short phone. + final screen = MediaQuery.sizeOf(context); + final stage = (screen.height * 0.42).clamp(300.0, 380.0); + final disc = (stage * 0.62).clamp(180.0, 260.0); + + return SizedBox( + height: stage, + child: Stack( + fit: StackFit.expand, + children: [ + const DecoratedBox( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment(0, -0.35), + radius: 1.2, + colors: [Color(0xFF16203D), Color(0xFF05070F)], + ), + ), + ), + CustomPaint(painter: const _StarfieldPainter()), + // A halo that grows with the lit fraction — the sky around a full + // moon really is washed out, and a new moon really does sit in the + // dark. It also stops the disc from looking pasted on. + Align( + alignment: const Alignment(0, -0.26), + child: IgnorePointer( + child: Container( + width: disc * 1.9, + height: disc * 1.9, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + Colors.white.withValues(alpha: 0.16 * phase.brightness), + Colors.transparent, + ], + ), + ), + ), + ), + ), + Align( + alignment: const Alignment(0, -0.26), + child: switch ((color, height, program, error)) { + ( + final ui.Image c, + final ui.Image h, + final ui.FragmentProgram p, + _, + ) => + CustomPaint( + size: Size.square(disc), + painter: _MoonPainter( + shader: p.fragmentShader(), + color: c, + height: h, + phase: phase.angle, + libration: libration, + // Canonical view until a place is known: pole up, lit + // limb to the right. A guessed tilt would look confident + // and be wrong. + northBearing: orientation?.northBearing ?? 0, + brightLimbBearing: + orientation?.brightLimbBearing ?? math.pi / 2, + ), + ), + (_, _, _, final Object e) => Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Text( + // l10n-ignore: shader failure is a developer error + 'moon shader: $e', + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + ), + _ => const SizedBox.square( + dimension: 40, + child: CircularProgressIndicator(strokeWidth: 3), + ), + }, + ), + Positioned( + left: 0, + right: 0, + bottom: AppSpacing.xl, + child: Column( + children: [ + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + subtitle, + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.white70, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// A fixed field of faint stars. Deterministic — a starfield that reshuffles +/// on every scrub frame reads as noise rather than as sky. +class _StarfieldPainter extends CustomPainter { + const _StarfieldPainter(); + + /// A cheap integer hash, so the field needs no stored table and no `Random`. + static double _unit(int seed) => ((seed * 2654435761) % 65536) / 65536; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white; + for (var i = 0; i < 90; i++) { + final x = _unit(i * 3 + 1) * size.width; + final y = _unit(i * 7 + 2) * size.height; + final brightness = _unit(i * 11 + 3); + canvas.drawCircle( + Offset(x, y), + 0.4 + brightness * 0.9, + paint..color = Colors.white.withValues(alpha: 0.15 + brightness * 0.45), + ); + } + } + + @override + bool shouldRepaint(covariant _StarfieldPainter oldDelegate) => false; +} + +class _MoonPainter extends CustomPainter { + const _MoonPainter({ + required this.shader, + required this.color, + required this.height, + required this.phase, + required this.libration, + required this.northBearing, + required this.brightLimbBearing, + }); + + final ui.FragmentShader shader; + final ui.Image color; + final ui.Image height; + + /// Phase angle in radians — the only thing that changes between frames. + final double phase; + + /// The selenographic point facing Earth — the Moon's monthly rocking. + final Offset libration; + + /// Screen bearing of the Moon's north pole, and of the middle of its lit + /// limb — what tilts the globe the way the observer actually sees it. + final double northBearing; + final double brightLimbBearing; + + @override + void paint(ui.Canvas canvas, ui.Size size) { + shader.setFloat(0, size.width); + shader.setFloat(1, size.height); + shader.setFloat(2, phase); + shader.setFloat(3, libration.dx); + shader.setFloat(4, libration.dy); + shader.setFloat(5, northBearing); + shader.setFloat(6, brightLimbBearing); + shader.setImageSampler(0, color); + shader.setImageSampler(1, height); + canvas.drawRect(Offset.zero & size, Paint()..shader = shader); + } + + @override + bool shouldRepaint(covariant _MoonPainter oldDelegate) => + phase != oldDelegate.phase || + libration != oldDelegate.libration || + northBearing != oldDelegate.northBearing || + brightLimbBearing != oldDelegate.brightLimbBearing || + color != oldDelegate.color || + height != oldDelegate.height || + !identical(shader, oldDelegate.shader); +} + +/// The page's one surface: an inset, rounded container. Every group on the +/// page sits in one, so the calendar reads as a peer of the readings rather +/// than as loose widgets after them. +class _Card extends StatelessWidget { + const _Card({required this.child, this.padding = EdgeInsets.zero}); + + final Widget child; + final EdgeInsetsGeometry padding; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: AppRadius.medium, + ), + child: Padding(padding: padding, child: child), + ), + ); +} + +/// A grouped card of icon / label / value rows. +class _StatCard extends StatelessWidget { + const _StatCard({required this.rows}); + + final List<(IconData, String, String)> rows; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return _Card( + child: Column( + children: [ + for (final (index, (icon, label, value)) in rows.indexed) ...[ + if (index > 0) + Divider( + height: 1, + indent: AppSpacing.xxl + AppSpacing.md, + color: colors.outlineVariant, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + child: Row( + children: [ + Icon(icon, size: 20, color: colors.onSurfaceVariant), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Text(label, style: theme.textTheme.bodyMedium), + ), + Text( + value, + style: theme.textTheme.titleSmall?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/features/data/presentation/pages/planets_page.dart b/lib/features/data/presentation/pages/planets_page.dart new file mode 100644 index 000000000..be5138807 --- /dev/null +++ b/lib/features/data/presentation/pages/planets_page.dart @@ -0,0 +1,285 @@ +/// The planets, tonight. +/// +/// Ordered by what actually decides whether you can see one: how high it gets, +/// not how far out it orbits. A planet below the horizon or inside the Sun's +/// glare is listed as such rather than given a magnitude that implies it is +/// there to be seen — magnitude answers "how bright", never "is it visible". +/// +/// Computed on the device from JPL's Keplerian elements (`core/astro/ +/// planet_ephemeris.dart`), pinned against JPL Horizons. +library; + +import 'dart:math' as math; + +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/planet_ephemeris.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/features/data/presentation/observer_place.dart'; +import 'package:dpip/features/data/presentation/widgets/astro_card.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +const Duration _taiwanOffset = Duration(hours: 8); + +/// Below this elongation a planet is lost in twilight whatever its magnitude. +const double _glareElongation = 15; + +class PlanetsPage extends StatelessWidget { + const PlanetsPage({super.key}); + + static final DateFormat _clock = DateFormat('HH:mm'); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final town = observerTown(context); + final now = AppTime.utc; + final today = AppTime.taipei(now); + final dayStart = DateTime.utc( + today.year, + today.month, + today.day, + ).subtract(_taiwanOffset); + + final observer = town == null + ? null + : Observer(latitude: town.lat, longitude: town.lng); + + final entries = [ + for (final planet in Planet.values) + _Entry( + planet: planet, + body: PlanetEphemeris.at(planet, now), + now: observer?.lookAt( + PlanetEphemeris.at(planet, now).equatorial, + now, + ), + events: observer == null + ? null + : RiseSet.solve( + from: dayStart, + observer: observer, + track: PlanetEphemeris.trackOf(planet), + horizon: (_) => pointHorizon, + ), + ), + ]..sort((a, b) => b.rank.compareTo(a.rank)); + + return Scaffold( + appBar: AppBar(title: Text(l10n.planetsTitle)), + body: ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + SectionHeader( + l10n.planetsSectionTonight, + trailing: town == null + ? null + : Text( + town.fullName, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + for (final entry in entries) + Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.sm), + child: AstroCard( + padding: const EdgeInsets.all(AppSpacing.lg), + child: _PlanetTile(entry: entry, clock: _clock), + ), + ), + ], + ), + ); + } +} + +/// A planet with everything the tile needs, resolved once. +class _Entry { + _Entry({ + required this.planet, + required this.body, + required this.now, + required this.events, + }); + + final Planet planet; + final PlanetEphemeris body; + final Horizontal? now; + final RiseSet? events; + + bool get isUp => (now?.altitude ?? -1) > 0; + bool get isInGlare => body.elongation * 180 / math.pi < _glareElongation; + + /// Sort key: up and clear of the Sun first, then by brightness. A dim planet + /// you can actually see beats a bright one that has set. + double get rank { + if (isInGlare) return -100; + if (!isUp) return -50 - body.magnitude; + return 10 - body.magnitude; + } +} + +class _PlanetTile extends StatelessWidget { + const _PlanetTile({required this.entry, required this.clock}); + + final _Entry entry; + final DateFormat clock; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final colors = theme.colorScheme; + final body = entry.body; + + final status = entry.isInGlare + ? l10n.planetInGlare + : entry.isUp + ? l10n.planetUp + : l10n.planetDown; + final statusColor = entry.isInGlare + ? colors.onSurfaceVariant + : entry.isUp + ? colors.primary + : colors.onSurfaceVariant; + + String time(DateTime? at) => + at == null ? l10n.moonNoEvent : clock.format(AppTime.taipei(at)); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + planetName(l10n, entry.planet), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: 2, + ), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.14), + borderRadius: AppRadius.small, + ), + child: Text( + status, + style: theme.textTheme.labelSmall?.copyWith(color: statusColor), + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.lg, + runSpacing: AppSpacing.xs, + children: [ + _Fact( + label: l10n.planetMagnitude, + // l10n-ignore: a signed number + value: body.magnitude.toStringAsFixed(1), + ), + _Fact( + label: l10n.planetElongation, + // l10n-ignore: degree symbol + value: '${(body.elongation * 180 / math.pi).round()}°', + ), + _Fact( + label: l10n.planetSky, + value: body.isEvening ? l10n.planetEvening : l10n.planetMorning, + ), + _Fact( + label: l10n.planetDistance, + // l10n-ignore: the unit word is localised beside it + value: '${body.distanceAu.toStringAsFixed(2)} ${l10n.planetAu}', + ), + if (entry.now != null && entry.isUp) + _Fact( + label: l10n.planetAltitude, + // l10n-ignore: degree symbol + value: '${(entry.now!.altitude * 180 / math.pi).round()}°', + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + Row( + children: [ + Icon(Icons.arrow_upward, size: 14, color: colors.onSurfaceVariant), + const SizedBox(width: AppSpacing.xs), + Text(time(entry.events?.rise), style: theme.textTheme.bodySmall), + const SizedBox(width: AppSpacing.md), + Icon( + Icons.vertical_align_top, + size: 14, + color: colors.onSurfaceVariant, + ), + const SizedBox(width: AppSpacing.xs), + Text(time(entry.events?.transit), style: theme.textTheme.bodySmall), + const SizedBox(width: AppSpacing.md), + Icon( + Icons.arrow_downward, + size: 14, + color: colors.onSurfaceVariant, + ), + const SizedBox(width: AppSpacing.xs), + Text(time(entry.events?.set), style: theme.textTheme.bodySmall), + ], + ), + ], + ); + } +} + +class _Fact extends StatelessWidget { + const _Fact({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + Text( + value, + style: theme.textTheme.titleSmall?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} + +/// The localised name of a planet. +String planetName(AppLocalizations l10n, Planet planet) => switch (planet) { + Planet.mercury => l10n.planetMercury, + Planet.venus => l10n.planetVenus, + Planet.mars => l10n.planetMars, + Planet.jupiter => l10n.planetJupiter, + Planet.saturn => l10n.planetSaturn, + Planet.uranus => l10n.planetUranus, + Planet.neptune => l10n.planetNeptune, +}; diff --git a/lib/features/data/presentation/pages/sky_chart_page.dart b/lib/features/data/presentation/pages/sky_chart_page.dart new file mode 100644 index 000000000..8a1d43cc7 --- /dev/null +++ b/lib/features/data/presentation/pages/sky_chart_page.dart @@ -0,0 +1,312 @@ +/// A chart of the sky above you, right now. +/// +/// Stereographic from the zenith: the projection every planisphere uses, +/// because it keeps shapes locally true — a constellation near the horizon is +/// stretched but still recognisable, which a plain equal-area or orthographic +/// projection cannot manage. +/// +/// The whole chart is one repaint of one painter. 5,000 stars, 150 +/// constellation strokes, the planets and the Moon are all drawn from data +/// already in memory, so the only cost is the projection arithmetic. +/// +/// North is at the top and **east is on the left**, which looks backwards on +/// paper and is correct on the sky: you are looking up at it, not down at a +/// map. +library; + +import 'dart:math' as math; + +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/deep_sky.dart'; +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:dpip/core/astro/planet_ephemeris.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:dpip/core/astro/star_catalog.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/features/data/presentation/observer_place.dart'; +import 'package:dpip/features/data/presentation/pages/planets_page.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:flutter/material.dart'; + +class SkyChartPage extends StatefulWidget { + const SkyChartPage({super.key}); + + @override + State createState() => _SkyChartPageState(); +} + +class _SkyChartPageState extends State { + StarCatalog? _catalog; + Object? _error; + + @override + void initState() { + super.initState(); + StarCatalog.load() + .then((catalog) { + if (mounted) setState(() => _catalog = catalog); + }) + // A missing asset must not leave a spinner turning forever — that is + // indistinguishable from slow, and the usual cause is adding an asset + // without a full rebuild. + .catchError((Object error) { + if (mounted) setState(() => _error = error); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final town = observerTown(context); + final now = AppTime.utc; + final catalog = _catalog; + + return Scaffold( + appBar: AppBar(title: Text(l10n.skyChartTitle)), + body: ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: AspectRatio( + aspectRatio: 1, + child: DecoratedBox( + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [Color(0xFF10182E), Color(0xFF04060D)], + ), + ), + child: _error != null + ? Center( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.xl), + child: Text( + l10n.skyChartUnavailable, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: Colors.white70), + ), + ), + ) + : catalog == null || town == null + ? const Center( + child: SizedBox.square( + dimension: 32, + child: CircularProgressIndicator(strokeWidth: 3), + ), + ) + : CustomPaint( + painter: _SkyPainter( + catalog: catalog, + observer: Observer( + latitude: town.lat, + longitude: town.lng, + ), + at: now, + labels: ( + north: l10n.skyChartNorth, + east: l10n.skyChartEast, + south: l10n.skyChartSouth, + west: l10n.skyChartWest, + ), + planetLabel: (planet) => planetName(l10n, planet), + ), + ), + ), + ), + ), + if (town != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: AppRadius.medium, + ), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Text( + town.fullName, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + ), + ], + ), + ); + } +} + +/// Everything on the chart, drawn from the zenith outward. +class _SkyPainter extends CustomPainter { + const _SkyPainter({ + required this.catalog, + required this.observer, + required this.at, + required this.labels, + required this.planetLabel, + }); + + final StarCatalog catalog; + final Observer observer; + final DateTime at; + final ({String north, String east, String south, String west}) labels; + final String Function(Planet) planetLabel; + + /// Stereographic projection of an apparent place onto the disc, or null if + /// it is below the horizon. + Offset? _project(Horizontal position, Size size) { + if (position.altitude <= 0) return null; + final radius = size.width / 2; + // Zenith at the centre, horizon at the rim. + final r = radius * math.tan((math.pi / 2 - position.altitude) / 2); + // East to the left, because the chart is held up to the sky. + return Offset( + radius - r * math.sin(position.azimuth), + radius - r * math.cos(position.azimuth), + ); + } + + Offset? _projectEquatorial(Equatorial position, Size size) => + _project(observer.lookAt(position, at), size); + + @override + void paint(Canvas canvas, Size size) { + final radius = size.width / 2; + final centre = Offset(radius, radius); + + // Constellation figures first, so the stars sit on top of their own lines. + final linePaint = Paint() + ..color = const Color(0x553D6EA8) + ..strokeWidth = 1 + ..style = PaintingStyle.stroke; + for (final figure in catalog.figures) { + Offset? previous; + for (final (ra, dec) in figure.points) { + final point = _projectEquatorial( + StarCatalog.precess(ra, dec, at), + size, + ); + if (previous != null && point != null) { + canvas.drawLine(previous, point, linePaint); + } + previous = point; + } + } + + // Stars, brightest last so they are never overdrawn. + final starPaint = Paint()..color = Colors.white; + for (final star in catalog.stars.reversed) { + final point = _projectEquatorial( + StarCatalog.precess(star.rightAscension, star.declination, at), + size, + ); + if (point == null) continue; + // Magnitude to radius: each magnitude is 2.5x the flux, and the eye is + // logarithmic, so a linear map on magnitude reads correctly. + final scale = ((6.5 - star.magnitude) / 6.5).clamp(0.0, 1.0); + canvas.drawCircle( + point, + 0.4 + scale * scale * 2.4, + starPaint..color = Colors.white.withValues(alpha: 0.35 + scale * 0.65), + ); + } + + // Messier objects that are up — small rings, so they read as "not a star". + final objectPaint = Paint() + ..color = const Color(0x99A5D6A7) + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + for (final object in messierCatalogue) { + if (object.magnitude > 7) continue; + final point = _projectEquatorial(object.positionAt(at), size); + if (point != null) canvas.drawCircle(point, 3, objectPaint); + } + + // The Moon and the planets, labelled — the things a reader will look for. + final moon = MoonEphemeris.at(at); + final moonPoint = _projectEquatorial(moon.equatorial, size); + if (moonPoint != null) { + canvas.drawCircle(moonPoint, 6, Paint()..color = const Color(0xFFE8E3DA)); + } + for (final planet in Planet.values) { + final body = PlanetEphemeris.at(planet, at); + if (body.magnitude > 5.5) continue; + final point = _projectEquatorial(body.equatorial, size); + if (point == null) continue; + canvas.drawCircle(point, 3.5, Paint()..color = const Color(0xFFFFD98A)); + _text( + canvas, + planetLabel(planet), + point + const Offset(6, -6), + 10, + const Color(0xFFFFD98A), + ); + } + + // The horizon and the cardinal points. + canvas.drawCircle( + centre, + radius - 1, + Paint() + ..color = const Color(0x66FFFFFF) + ..style = PaintingStyle.stroke + ..strokeWidth = 1, + ); + const inset = 14.0; + _text( + canvas, + labels.north, + Offset(radius, inset), + 12, + Colors.white70, + centred: true, + ); + _text( + canvas, + labels.south, + Offset(radius, size.height - inset - 12), + 12, + Colors.white70, + centred: true, + ); + _text(canvas, labels.east, Offset(inset, radius - 6), 12, Colors.white70); + _text( + canvas, + labels.west, + Offset(size.width - inset - 10, radius - 6), + 12, + Colors.white70, + ); + } + + void _text( + Canvas canvas, + String value, + Offset at, + double size, + Color color, { + bool centred = false, + }) { + final painter = TextPainter( + text: TextSpan( + text: value, + style: TextStyle(color: color, fontSize: size), + ), + textDirection: TextDirection.ltr, + )..layout(); + painter.paint(canvas, centred ? at - Offset(painter.width / 2, 0) : at); + } + + @override + bool shouldRepaint(covariant _SkyPainter oldDelegate) => + at != oldDelegate.at || + !identical(catalog, oldDelegate.catalog) || + observer.latitude != oldDelegate.observer.latitude || + observer.longitude != oldDelegate.observer.longitude; +} diff --git a/lib/features/data/presentation/pages/sun_page.dart b/lib/features/data/presentation/pages/sun_page.dart new file mode 100644 index 000000000..f5ad3ab81 --- /dev/null +++ b/lib/features/data/presentation/pages/sun_page.dart @@ -0,0 +1,288 @@ +/// The Sun: one day's light, and the year's solar terms. +/// +/// Computed on the device from `core/astro/` — the same Meeus solar series the +/// moon page reads for the phase, so the two pages can never disagree about +/// where the Sun is. +/// +/// The twilights are here for more than stargazing. Civil twilight is the +/// boundary of working outdoors without light, which is what an evacuation +/// window or a night search is planned against — so it is given as a span, not +/// a single time, and sits above the photographers' golden hour rather than +/// among it. +library; + +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/solar_terms.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; +import 'package:dpip/core/astro/sun_events.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/features/data/presentation/observer_place.dart'; +import 'package:dpip/features/data/presentation/widgets/astro_card.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// Taiwan's fixed offset — the wall clock every time on this page is read in. +const Duration _taiwanOffset = Duration(hours: 8); + +class SunPage extends StatelessWidget { + const SunPage({super.key}); + + // Numeric formats only, so no `intl` locale symbol data is needed. + static final DateFormat _clock = DateFormat('HH:mm'); + static final DateFormat _dayMonth = DateFormat('M/d'); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final town = observerTown(context); + final now = AppTime.utc; + final today = AppTime.taipei(now); + final dayStart = DateTime.utc( + today.year, + today.month, + today.day, + ).subtract(_taiwanOffset); + + final events = town == null + ? null + : SunEvents.of(dayStart, latitude: town.lat, longitude: town.lng); + final equationOfTime = SunEphemeris.at(now).equationOfTime; + final (nextTerm, nextTermAt) = SolarTerms.upcoming(now); + final terms = SolarTerms.ofYear(today.year, offset: _taiwanOffset); + + String at(DateTime? utc) => + utc == null ? l10n.moonNoEvent : _clock.format(AppTime.taipei(utc)); + + return Scaffold( + appBar: AppBar(title: Text(l10n.sunTitle)), + body: ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + SectionHeader( + l10n.sunSectionDaylight, + trailing: town == null + ? null + : Text( + town.fullName, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + AstroReadings( + rows: [ + (Icons.wb_twilight_outlined, l10n.sunRise, at(events?.rise)), + (Icons.wb_sunny_outlined, l10n.sunNoon, at(events?.noon)), + (Icons.nights_stay_outlined, l10n.sunSet, at(events?.set)), + ( + Icons.hourglass_bottom_outlined, + l10n.sunDayLength, + events == null ? l10n.moonNoEvent : _span(events.dayLength), + ), + ], + ), + SectionHeader(l10n.sunSectionTwilight), + AstroCard( + child: Column( + children: [ + AstroSpan( + icon: Icons.brightness_5_outlined, + label: l10n.sunTwilightCivil, + from: at(events?.civilDawn), + to: at(events?.civilDusk), + ), + const Divider( + height: 1, + indent: AppSpacing.xxl + AppSpacing.md, + ), + AstroSpan( + icon: Icons.brightness_4_outlined, + label: l10n.sunTwilightNautical, + from: at(events?.nauticalDawn), + to: at(events?.nauticalDusk), + ), + const Divider( + height: 1, + indent: AppSpacing.xxl + AppSpacing.md, + ), + AstroSpan( + icon: Icons.brightness_2_outlined, + label: l10n.sunTwilightAstronomical, + from: at(events?.astronomicalDawn), + to: at(events?.astronomicalDusk), + ), + ], + ), + ), + SectionHeader(l10n.sunSectionLight), + AstroCard( + child: Column( + children: [ + AstroSpan( + icon: Icons.wb_iridescent_outlined, + label: l10n.sunGoldenHourMorning, + from: at(events?.blueMorningStart), + to: at(events?.goldenMorningEnd), + ), + const Divider( + height: 1, + indent: AppSpacing.xxl + AppSpacing.md, + ), + AstroSpan( + icon: Icons.wb_incandescent_outlined, + label: l10n.sunGoldenHourEvening, + from: at(events?.goldenEveningStart), + to: at(events?.blueEveningEnd), + ), + const Divider( + height: 1, + indent: AppSpacing.xxl + AppSpacing.md, + ), + AstroSpan( + icon: Icons.water_outlined, + label: l10n.sunBlueHour, + from: at(events?.blueEveningEnd), + to: at(events?.nauticalDusk), + ), + ], + ), + ), + SectionHeader(l10n.sunSectionSundial), + AstroReadings( + rows: [ + ( + Icons.schedule_outlined, + l10n.sunEquationOfTime, + _signedMinutes(equationOfTime, l10n), + ), + ( + Icons.eco_outlined, + l10n.solarTermNext, + '${solarTermName(l10n, nextTerm)} ' + '${_dayMonth.format(AppTime.taipei(nextTermAt))}', + ), + ], + ), + SectionHeader(l10n.sunSectionTerms), + AstroCard( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + child: Column( + children: [ + for (final (term, instant) in terms) + _TermRow( + name: solarTermName(l10n, term), + stamp: + '${_dayMonth.format(AppTime.taipei(instant))} ' + '${_clock.format(AppTime.taipei(instant))}', + isMajor: term.isMajor, + isNext: term == nextTerm, + ), + ], + ), + ), + ], + ), + ); + } + + /// `H hr M min`, using the localised unit words. + static String _span(Duration duration) => + // l10n-ignore: digits and a colon + '${duration.inHours}:' + '${(duration.inMinutes % 60).toString().padLeft(2, '0')}'; + + static String _signedMinutes(Duration value, AppLocalizations l10n) { + final minutes = value.inSeconds / 60; + // l10n-ignore: a signed number; the unit word is localised beside it + final text = '${minutes >= 0 ? '+' : ''}${minutes.toStringAsFixed(1)}'; + return '$text ${l10n.sunMinutes}'; + } +} + +class _TermRow extends StatelessWidget { + const _TermRow({ + required this.name, + required this.stamp, + required this.isMajor, + required this.isNext, + }); + + final String name; + final String stamp; + + /// 中氣 — the twelve that anchor the lunisolar calendar. Emphasised because + /// the distinction is real and invisible otherwise. + final bool isMajor; + final bool isNext; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Container( + color: isNext ? colors.primaryContainer.withValues(alpha: 0.5) : null, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + child: Row( + children: [ + SizedBox( + width: 8, + child: isMajor + ? Icon(Icons.circle, size: 6, color: colors.primary) + : null, + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + name, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: isMajor ? FontWeight.w600 : FontWeight.w400, + ), + ), + ), + Text( + stamp, + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ); + } +} + +/// The localised name of a solar term. +String solarTermName(AppLocalizations l10n, SolarTerm term) => switch (term) { + SolarTerm.vernalEquinox => l10n.solarTermVernalEquinox, + SolarTerm.pureBrightness => l10n.solarTermPureBrightness, + SolarTerm.grainRain => l10n.solarTermGrainRain, + SolarTerm.startOfSummer => l10n.solarTermStartOfSummer, + SolarTerm.grainFull => l10n.solarTermGrainFull, + SolarTerm.grainInEar => l10n.solarTermGrainInEar, + SolarTerm.summerSolstice => l10n.solarTermSummerSolstice, + SolarTerm.minorHeat => l10n.solarTermMinorHeat, + SolarTerm.majorHeat => l10n.solarTermMajorHeat, + SolarTerm.startOfAutumn => l10n.solarTermStartOfAutumn, + SolarTerm.endOfHeat => l10n.solarTermEndOfHeat, + SolarTerm.whiteDew => l10n.solarTermWhiteDew, + SolarTerm.autumnalEquinox => l10n.solarTermAutumnalEquinox, + SolarTerm.coldDew => l10n.solarTermColdDew, + SolarTerm.frostDescent => l10n.solarTermFrostDescent, + SolarTerm.startOfWinter => l10n.solarTermStartOfWinter, + SolarTerm.minorSnow => l10n.solarTermMinorSnow, + SolarTerm.majorSnow => l10n.solarTermMajorSnow, + SolarTerm.winterSolstice => l10n.solarTermWinterSolstice, + SolarTerm.minorCold => l10n.solarTermMinorCold, + SolarTerm.majorCold => l10n.solarTermMajorCold, + SolarTerm.startOfSpring => l10n.solarTermStartOfSpring, + SolarTerm.rainWater => l10n.solarTermRainWater, + SolarTerm.awakeningOfInsects => l10n.solarTermAwakeningOfInsects, +}; diff --git a/lib/features/data/presentation/pages/tide_page.dart b/lib/features/data/presentation/pages/tide_page.dart new file mode 100644 index 000000000..80d54eef0 --- /dev/null +++ b/lib/features/data/presentation/pages/tide_page.dart @@ -0,0 +1,138 @@ +/// 潮汐 — the astronomical part of the tide. +/// +/// Deliberately not a tide table. A real prediction needs a harbour's harmonic +/// constants, which are the ocean's response and cannot be derived from the +/// sky; the CWA publishes those and this page says so. What it *does* give is +/// the forcing, which is purely astronomical and therefore works offline: when +/// the pull peaks, whether this is a spring or a neap, and whether a perigean +/// spring — the highest water of the season, and the one to check a storm +/// surge against — is coming. +library; + +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/tidal_forcing.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/features/data/presentation/observer_place.dart'; +import 'package:dpip/features/data/presentation/widgets/astro_card.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +const Duration _taiwanOffset = Duration(hours: 8); + +class TidePage extends StatelessWidget { + const TidePage({super.key}); + + static final DateFormat _clock = DateFormat('HH:mm'); + static final DateFormat _date = DateFormat('yyyy/MM/dd'); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final town = observerTown(context); + final now = AppTime.utc; + final today = AppTime.taipei(now); + final dayStart = DateTime.utc( + today.year, + today.month, + today.day, + ).subtract(_taiwanOffset); + + if (town == null) { + return Scaffold( + appBar: AppBar(title: Text(l10n.tideTitle)), + body: const SizedBox.shrink(), + ); + } + + final forcing = TidalForcing.at( + now, + latitude: town.lat, + longitude: town.lng, + ); + final extremes = TidalForcing.extremes( + dayStart, + latitude: town.lat, + longitude: town.lng, + ); + final perigean = TidalForcing.nextPerigeanSpring(now); + + return Scaffold( + appBar: AppBar(title: Text(l10n.tideTitle)), + body: ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.md, + AppSpacing.lg, + 0, + ), + child: Text( + l10n.tideDisclaimer, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + SectionHeader( + l10n.tideSectionNow, + trailing: Text( + town.fullName, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + AstroReadings( + rows: [ + ( + Icons.waves_outlined, + l10n.tidePhase, + switch (forcing.phase) { + TidePhase.spring => l10n.tideSpring, + TidePhase.neap => l10n.tideNeap, + TidePhase.middling => l10n.tideMiddling, + }, + ), + ( + Icons.straighten_outlined, + l10n.tideLunarDistanceFactor, + // l10n-ignore: a multiplier + '${forcing.distanceFactor.toStringAsFixed(2)}×', + ), + ( + Icons.trending_up_outlined, + l10n.tideEquilibrium, + // l10n-ignore: metres, with the unit localised beside it + '${forcing.equilibriumMetres.toStringAsFixed(2)} ${l10n.tideMetres}', + ), + if (perigean != null) + ( + Icons.warning_amber_outlined, + l10n.tidePerigeanSpring, + _date.format(AppTime.taipei(perigean)), + ), + ], + ), + SectionHeader(l10n.tideSectionTurningPoints), + AstroReadings( + rows: [ + for (final extreme in extremes) + ( + extreme.isHigh ? Icons.arrow_upward : Icons.arrow_downward, + extreme.isHigh ? l10n.tideHigh : l10n.tideLow, + _clock.format(AppTime.taipei(extreme.at)), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/data/presentation/pages/tonight_page.dart b/lib/features/data/presentation/pages/tonight_page.dart new file mode 100644 index 000000000..34ad48522 --- /dev/null +++ b/lib/features/data/presentation/pages/tonight_page.dart @@ -0,0 +1,285 @@ +/// Tonight — what is observable, and when. +/// +/// The dark window comes first because it gates everything else: a list of +/// targets is useless if the Moon is up all night. Then the showers that are +/// running, then the satellites, then the deep-sky objects high enough to be +/// worth pointing at. +/// +/// Every section states its empty case rather than disappearing. "No visible +/// passes for two days" is a real and common answer — the station spends whole +/// stretches passing only inside the Earth's shadow — and a section that +/// simply vanishes cannot be told apart from one that is broken. +/// +/// The work happens in `TonightReport`, off the UI thread: the satellite +/// search alone is tens of thousands of SGP4 evaluations and would freeze a +/// build. +library; + +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/deep_sky.dart'; +import 'package:dpip/core/astro/meteor_showers.dart'; +import 'package:dpip/core/astro/tle_source.dart'; +import 'package:dpip/core/astro/tonight_report.dart'; +import 'package:dpip/core/geo/town.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:dpip/features/data/presentation/observer_place.dart'; +import 'package:dpip/features/data/presentation/widgets/astro_card.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/loading_view.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +const Duration _taiwanOffset = Duration(hours: 8); + +class TonightPage extends StatefulWidget { + const TonightPage({super.key}); + + @override + State createState() => _TonightPageState(); +} + +class _TonightPageState extends State { + static final DateFormat _clock = DateFormat('HH:mm'); + static final DateFormat _dayMonth = DateFormat('M/d'); + + Future? _report; + String? _resolvedFor; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final town = observerTown(context); + // Recompute only when the place changes — the report is expensive and the + // page rebuilds for unrelated reasons. + if (town == null || town.code == _resolvedFor) return; + _resolvedFor = town.code; + _report = _buildReport(town); + } + + Future _buildReport(Town town) { + final now = AppTime.utc; + final today = AppTime.taipei(now); + return TonightReport.build( + DateTime.utc(today.year, today.month, today.day).subtract(_taiwanOffset), + now: now, + latitude: town.lat, + longitude: town.lng, + // Cache-backed, so wiring a feed later only has to fill in `fetch`. + // With none wired it resolves to the bundled snapshot, which is what + // ships today. + source: CachedTleSource(prefs: context.read(), now: () => now), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final town = observerTown(context); + final report = _report; + + return Scaffold( + appBar: AppBar(title: Text(l10n.tonightTitle)), + body: town == null || report == null + ? const LoadingView() + : FutureBuilder( + future: report, + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) return const LoadingView(); + return _Report(report: data, place: town.fullName); + }, + ), + ); + } + + static String _at(AppLocalizations l10n, DateTime? utc) => + utc == null ? l10n.moonNoEvent : _clock.format(AppTime.taipei(utc)); + + static String _day(DateTime utc) => _dayMonth.format(AppTime.taipei(utc)); +} + +class _Report extends StatelessWidget { + const _Report({required this.report, required this.place}); + + final TonightReport report; + final String place; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final night = report.night; + + Widget trailing(String text) => Text( + text, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ); + + return ListView( + padding: EdgeInsets.only( + bottom: AppSpacing.xl + MediaQuery.paddingOf(context).bottom, + ), + children: [ + SectionHeader(l10n.tonightSectionDark, trailing: trailing(place)), + AstroReadings( + rows: [ + ( + Icons.dark_mode_outlined, + l10n.tonightAstronomicalNight, + night.astronomicalNight == null + ? l10n.tonightNeverDark + : '${_TonightPageState._at(l10n, night.astronomicalNight!.$1)}' + ' – ' + '${_TonightPageState._at(l10n, night.astronomicalNight!.$2)}', + ), + ( + Icons.visibility_outlined, + l10n.tonightDarkWindow, + night.best == null + ? l10n.tonightMoonAllNight + : '${_TonightPageState._at(l10n, night.best!.from)} – ' + '${_TonightPageState._at(l10n, night.best!.to)}', + ), + ( + Icons.hourglass_bottom_outlined, + l10n.tonightDarkTotal, + // l10n-ignore: digits and a colon + '${night.totalDark.inHours}:' + '${(night.totalDark.inMinutes % 60).toString().padLeft(2, '0')}', + ), + ( + Icons.brightness_2_outlined, + l10n.tonightMoonlight, + // l10n-ignore: percentage readout + '${(night.moonIllumination * 100).round()}%', + ), + ], + ), + + SectionHeader(l10n.tonightSectionShowers), + AstroReadings( + rows: report.showers.isEmpty + ? [ + ( + Icons.auto_awesome_outlined, + l10n.tonightNoShowers, + l10n.moonNoEvent, + ), + ] + : [ + for (final conditions in report.showers) + ( + conditions.isFavourable + ? Icons.auto_awesome + : Icons.auto_awesome_outlined, + '${meteorShowerName(l10n, conditions.shower)} · ' + '${_TonightPageState._day(conditions.peak)}', + conditions.bestAltitude <= 0 + ? l10n.tonightRadiantDown + // l10n-ignore: a rate; the unit word is localised + : '~${conditions.visibleRate.round()}' + ' ${l10n.tonightPerHour}', + ), + ], + ), + + SectionHeader( + l10n.tonightSectionSatellites, + trailing: report.elementAge == null + ? null + : trailing(l10n.tonightElementAge(report.elementAge!.inDays)), + ), + AstroReadings( + rows: switch (report) { + TonightReport(satellitesFailed: true) => [ + ( + Icons.error_outline, + l10n.tonightSatellitesUnavailable, + l10n.moonNoEvent, + ), + ], + TonightReport(passes: final passes) when passes.isEmpty => [ + ( + Icons.satellite_alt_outlined, + l10n.tonightNoPasses, + l10n.moonNoEvent, + ), + ], + TonightReport(passes: final passes) => [ + for (final entry in passes.take(6)) + ( + Icons.satellite_alt_outlined, + '${entry.name} · ${_TonightPageState._day(entry.pass.rises)}', + // l10n-ignore: a time and a degree readout + '${_TonightPageState._at(l10n, entry.pass.peaks)} ' + '${(entry.pass.peakAltitude / degrees).round()}°', + ), + ], + }, + ), + + SectionHeader(l10n.tonightSectionTargets), + AstroReadings( + rows: report.targets.isEmpty + ? [ + ( + Icons.blur_on_outlined, + l10n.tonightNoTargets, + l10n.moonNoEvent, + ), + ] + : [ + for (final sighting in report.targets.take(8)) + ( + Icons.blur_on_outlined, + sighting.object.commonName.isEmpty + ? '${sighting.object.label} · ' + '${deepSkyTypeName(l10n, sighting.object.type)}' + : '${sighting.object.label} · ' + '${sighting.object.commonName}', + // l10n-ignore: magnitude and a degree readout + '${sighting.object.magnitude.toStringAsFixed(1)} ' + '${(sighting.altitude / degrees).round()}°', + ), + ], + ), + ], + ); + } +} + +/// The localised name of a shower. +String meteorShowerName(AppLocalizations l10n, MeteorShower shower) => + switch (shower.id) { + 'quadrantids' => l10n.showerQuadrantids, + 'lyrids' => l10n.showerLyrids, + 'etaAquariids' => l10n.showerEtaAquariids, + 'deltaAquariids' => l10n.showerDeltaAquariids, + 'perseids' => l10n.showerPerseids, + 'orionids' => l10n.showerOrionids, + 'southernTaurids' => l10n.showerSouthernTaurids, + 'leonids' => l10n.showerLeonids, + 'geminids' => l10n.showerGeminids, + _ => l10n.showerUrsids, + }; + +/// The localised name of a deep-sky object type. +String deepSkyTypeName(AppLocalizations l10n, DeepSkyType type) => + switch (type) { + DeepSkyType.oc => l10n.deepSkyOpenCluster, + DeepSkyType.gc => l10n.deepSkyGlobularCluster, + DeepSkyType.s => l10n.deepSkySpiralGalaxy, + DeepSkyType.e => l10n.deepSkyEllipticalGalaxy, + DeepSkyType.i => l10n.deepSkyIrregularGalaxy, + DeepSkyType.pn => l10n.deepSkyPlanetaryNebula, + DeepSkyType.snr => l10n.deepSkySupernovaRemnant, + DeepSkyType.sfr => l10n.deepSkyEmissionNebula, + DeepSkyType.rn => l10n.deepSkyReflectionNebula, + DeepSkyType.pos => l10n.deepSkyAsterism, + }; diff --git a/lib/features/data/presentation/widgets/astro_card.dart b/lib/features/data/presentation/widgets/astro_card.dart new file mode 100644 index 000000000..8735f2834 --- /dev/null +++ b/lib/features/data/presentation/widgets/astro_card.dart @@ -0,0 +1,126 @@ +/// The one surface the astronomy pages are built from. +/// +/// Every group on every astronomy page sits in the same inset rounded card, so +/// a table of planets reads as a peer of a list of twilight times rather than +/// as a different kind of thing. Shared rather than copied because the moon, +/// sun and planet pages are read one after another and any drift between them +/// shows immediately. +library; + +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:flutter/material.dart'; + +/// One icon / label / value line. +typedef AstroReading = (IconData icon, String label, String value); + +/// An inset rounded surface. +class AstroCard extends StatelessWidget { + const AstroCard({super.key, required this.child, this.padding}); + + final Widget child; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: AppRadius.medium, + ), + child: Padding(padding: padding ?? EdgeInsets.zero, child: child), + ), + ); +} + +/// A card of readings, divided. +class AstroReadings extends StatelessWidget { + const AstroReadings({super.key, required this.rows}); + + final List rows; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return AstroCard( + child: Column( + children: [ + for (final (index, (icon, label, value)) in rows.indexed) ...[ + if (index > 0) + Divider( + height: 1, + indent: AppSpacing.xxl + AppSpacing.md, + color: colors.outlineVariant, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + child: Row( + children: [ + Icon(icon, size: 20, color: colors.onSurfaceVariant), + const SizedBox(width: AppSpacing.md), + Expanded( + child: Text(label, style: theme.textTheme.bodyMedium), + ), + Text( + value, + style: theme.textTheme.titleSmall?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ], + ], + ), + ); + } +} + +/// A pair of times that bracket an event — dawn either side of sunrise, dusk +/// either side of sunset. Rendered as one row so the pairing is visible. +class AstroSpan extends StatelessWidget { + const AstroSpan({ + super.key, + required this.icon, + required this.label, + required this.from, + required this.to, + }); + + final IconData icon; + final String label; + final String from; + final String to; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + child: Row( + children: [ + Icon(icon, size: 20, color: colors.onSurfaceVariant), + const SizedBox(width: AppSpacing.md), + Expanded(child: Text(label, style: theme.textTheme.bodyMedium)), + Text( + // l10n-ignore: an en dash between two already-localised times + '$from – $to', + style: theme.textTheme.titleSmall?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/data/presentation/widgets/moon_calendar.dart b/lib/features/data/presentation/widgets/moon_calendar.dart new file mode 100644 index 000000000..5fa12b608 --- /dev/null +++ b/lib/features/data/presentation/widgets/moon_calendar.dart @@ -0,0 +1,237 @@ +/// A month of moons — the coarse control for the lunar page. +/// +/// The timeline beside it steps two hours at a time, which is the right +/// resolution for watching a terminator move and the wrong one for "show me +/// next month". This is the other half: a month at a glance, every day drawn +/// with its own phase, so the lunation reads as a shape rather than as a list +/// of dates. +/// +/// Weekday order and month titles come from [MaterialLocalizations] rather +/// than `intl` — the app never initialises `intl`'s locale symbol data (the +/// timelines all format numerically for that reason), and Flutter already +/// carries a correctly localised calendar for every supported locale. +library; + +import 'package:dpip/app/theme/app_motion.dart'; +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/features/data/presentation/widgets/moon_glyph.dart'; +import 'package:flutter/material.dart'; + +class MoonCalendar extends StatelessWidget { + const MoonCalendar({ + super.key, + required this.month, + required this.selected, + required this.today, + required this.firstDay, + required this.lastDay, + required this.onMonthChanged, + required this.onDaySelected, + required this.phaseAt, + }); + + /// Any instant inside the month being shown (Taipei wall time). + final DateTime month; + + /// The selected day (Taipei wall time). + final DateTime selected; + + /// Today (Taipei wall time), outlined rather than filled. + final DateTime today; + + /// The selectable range — the same span the timeline covers, so the two + /// controls can never point somewhere the other cannot reach. + final DateTime firstDay; + final DateTime lastDay; + + final ValueChanged onMonthChanged; + final ValueChanged onDaySelected; + + /// Phase angle for a day, in radians. Passed in so the calendar stays a + /// layout — the page owns which instant of the day it means. + final double Function(DateTime day) phaseAt; + + static bool _sameDay(DateTime a, DateTime b) => + a.year == b.year && a.month == b.month && a.day == b.day; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + final materialL10n = MaterialLocalizations.of(context); + + final firstOfMonth = DateTime.utc(month.year, month.month); + final daysInMonth = DateTime.utc( + month.year, + month.month + 1, + ).difference(firstOfMonth).inDays; + // Monday is 1 in Dart; narrowWeekdays is indexed from Sunday. + final leading = + (firstOfMonth.weekday % 7 - materialL10n.firstDayOfWeekIndex + 7) % 7; + + final canGoBack = !firstOfMonth + .subtract(const Duration(days: 1)) + .isBefore(DateTime.utc(firstDay.year, firstDay.month)); + final canGoForward = !DateTime.utc( + month.year, + month.month + 1, + ).isAfter(DateTime.utc(lastDay.year, lastDay.month)); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + IconButton( + onPressed: canGoBack + ? () => onMonthChanged( + DateTime.utc(month.year, month.month - 1), + ) + : null, + icon: const Icon(Icons.chevron_left), + tooltip: materialL10n.previousMonthTooltip, + ), + Expanded( + child: Text( + materialL10n.formatMonthYear(firstOfMonth), + textAlign: TextAlign.center, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + onPressed: canGoForward + ? () => onMonthChanged( + DateTime.utc(month.year, month.month + 1), + ) + : null, + icon: const Icon(Icons.chevron_right), + tooltip: materialL10n.nextMonthTooltip, + ), + ], + ), + const SizedBox(height: AppSpacing.xs), + Row( + children: [ + for (var i = 0; i < 7; i++) + Expanded( + child: Text( + materialL10n + .narrowWeekdays[(materialL10n.firstDayOfWeekIndex + i) % + 7], + textAlign: TextAlign.center, + style: theme.textTheme.labelSmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ), + ], + ), + const SizedBox(height: AppSpacing.xs), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + childAspectRatio: 0.78, + ), + itemCount: leading + daysInMonth, + itemBuilder: (context, index) { + if (index < leading) return const SizedBox.shrink(); + final day = DateTime.utc( + month.year, + month.month, + index - leading + 1, + ); + final inRange = + !day.isBefore( + DateTime.utc(firstDay.year, firstDay.month, firstDay.day), + ) && + !day.isAfter( + DateTime.utc(lastDay.year, lastDay.month, lastDay.day), + ); + return _DayCell( + day: day, + angle: phaseAt(day), + isSelected: _sameDay(day, selected), + isToday: _sameDay(day, today), + onTap: inRange ? () => onDaySelected(day) : null, + ); + }, + ), + ], + ); + } +} + +class _DayCell extends StatelessWidget { + const _DayCell({ + required this.day, + required this.angle, + required this.isSelected, + required this.isToday, + required this.onTap, + }); + + final DateTime day; + final double angle; + final bool isSelected; + final bool isToday; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + final enabled = onTap != null; + final label = isSelected ? colors.onPrimary : colors.onSurface; + + return Padding( + padding: const EdgeInsets.all(2), + child: AnimatedContainer( + duration: AppMotion.fast, + decoration: BoxDecoration( + color: isSelected ? colors.primary : null, + borderRadius: AppRadius.small, + border: isToday && !isSelected + ? Border.all(color: colors.primary, width: 1.5) + : null, + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Opacity( + opacity: enabled ? 1 : 0.3, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MoonGlyph( + angle: angle, + size: 20, + lit: isSelected ? colors.onPrimary : const Color(0xFFE8E3DA), + dark: isSelected + ? colors.primary.withValues(alpha: 0.45) + : colors.surfaceContainerHighest, + ), + const SizedBox(height: 2), + Text( + // l10n-ignore: day-of-month number + '${day.day}', + style: theme.textTheme.labelSmall?.copyWith( + color: label, + fontWeight: isSelected || isToday + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/data/presentation/widgets/moon_glyph.dart b/lib/features/data/presentation/widgets/moon_glyph.dart new file mode 100644 index 000000000..124afdfd4 --- /dev/null +++ b/lib/features/data/presentation/widgets/moon_glyph.dart @@ -0,0 +1,98 @@ +/// A small, flat drawing of the Moon's lit shape at a given phase. +/// +/// Deliberately *not* the photoreal shader. At the size a calendar cell gives +/// it, the NASA maps resolve to grey mush and the only thing a reader can +/// actually take from the mark is the shape — so the shape is all this draws, +/// crisply, at any size and with no textures to load. The hero disc on the +/// same page stays photoreal, where the detail is legible and the point. +/// +/// The terminator is a true half-ellipse, not an offset circle: the boundary +/// between lit and dark is a great circle seen at an angle, which projects to +/// an ellipse of semi-axis `r·cos(phase)`. The offset-circle shortcut that +/// crescent icons usually use gets the quarters right and everything between +/// them subtly wrong. +library; + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class MoonGlyph extends StatelessWidget { + const MoonGlyph({ + super.key, + required this.angle, + required this.size, + required this.lit, + required this.dark, + }); + + /// Phase angle in radians: 0 = new, π = full. + final double angle; + + /// Diameter in logical pixels. + final double size; + + /// The sunlit face. + final Color lit; + + /// The unlit face — a rim, so a new moon is still a visible mark rather than + /// a hole in the layout. + final Color dark; + + @override + Widget build(BuildContext context) => CustomPaint( + size: Size.square(size), + painter: _MoonGlyphPainter(angle: angle, lit: lit, dark: dark), + ); +} + +class _MoonGlyphPainter extends CustomPainter { + const _MoonGlyphPainter({ + required this.angle, + required this.lit, + required this.dark, + }); + + final double angle; + final Color lit; + final Color dark; + + @override + void paint(Canvas canvas, Size size) { + final radius = size.width / 2; + final centre = Offset(radius, radius); + canvas.drawCircle(centre, radius, Paint()..color = dark); + + final waxing = angle < math.pi; + final terminator = radius * math.cos(angle); + + canvas.save(); + canvas.translate(centre.dx, centre.dy); + // The waning half is the waxing half mirrored — cos is even, so the same + // path serves both and only the side it sits on changes. + if (!waxing) canvas.scale(-1, 1); + + final path = Path() + ..moveTo(0, -radius) + // Outer edge: the lit limb, top to bottom the long way round. + ..arcToPoint( + Offset(0, radius), + radius: Radius.circular(radius), + clockwise: true, + ) + // Inner edge: the terminator, bulging toward the lit side when the Moon + // is a crescent and away from it when gibbous. + ..arcToPoint( + Offset(0, -radius), + radius: Radius.elliptical(terminator.abs(), radius), + clockwise: terminator < 0, + ) + ..close(); + canvas.drawPath(path, Paint()..color = lit); + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _MoonGlyphPainter old) => + angle != old.angle || lit != old.lit || dark != old.dark; +} diff --git a/lib/features/disaster_map/domain/aed_detail.freezed.dart b/lib/features/disaster_map/domain/aed_detail.freezed.dart index 50d913dcb..cf75b6c7e 100644 --- a/lib/features/disaster_map/domain/aed_detail.freezed.dart +++ b/lib/features/disaster_map/domain/aed_detail.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'aed_detail.dart'; @@ -9,6 +9,7 @@ part of 'aed_detail.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$AedDetailCopyWithImpl<$Res> /// Create a copy of AedDetail /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? aedId = null,Object? name = null,Object? city = null,Object? district = null,Object? category = null,Object? type = null,Object? place = null,Object? lat = null,Object? lng = null,Object? address = null,Object? description = null,Object? placeDesc = null,Object? weekdayStart = null,Object? weekdayEnd = null,Object? saturdayStart = null,Object? saturdayEnd = null,Object? sundayStart = null,Object? sundayEnd = null,Object? openRemark = null,Object? emergencyPhone = null,Object? placeId = null,}) { - return _then(_self.copyWith( + return _then(AedDetail( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as int,aedId: null == aedId ? _self.aedId : aedId // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/disaster_map/domain/restroom_detail.freezed.dart b/lib/features/disaster_map/domain/restroom_detail.freezed.dart index d488c3a88..d4f533064 100644 --- a/lib/features/disaster_map/domain/restroom_detail.freezed.dart +++ b/lib/features/disaster_map/domain/restroom_detail.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'restroom_detail.dart'; @@ -9,6 +9,7 @@ part of 'restroom_detail.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$RestroomDetailCopyWithImpl<$Res> /// Create a copy of RestroomDetail /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? address = null,Object? latitude = null,Object? longitude = null,Object? type = null,Object? type2 = null,Object? typegrade = null,}) { - return _then(_self.copyWith( + return _then(RestroomDetail( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable as String,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/disaster_map/domain/shelter_detail.freezed.dart b/lib/features/disaster_map/domain/shelter_detail.freezed.dart index 81e2de7ee..e5be96677 100644 --- a/lib/features/disaster_map/domain/shelter_detail.freezed.dart +++ b/lib/features/disaster_map/domain/shelter_detail.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'shelter_detail.dart'; @@ -9,6 +9,7 @@ part of 'shelter_detail.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$ShelterDetailCopyWithImpl<$Res> /// Create a copy of ShelterDetail /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? capacity = null,Object? category = null,Object? indoor = null,Object? outdoor = null,Object? vulnerableOk = null,Object? lat = null,Object? lng = null,Object? address = null,}) { - return _then(_self.copyWith( + return _then(ShelterDetail( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,capacity: null == capacity ? _self.capacity : capacity // ignore: cast_nullable_to_non_nullable @@ -218,7 +219,7 @@ return $default(_that.id,_that.name,_that.capacity,_that.category,_that.indoor,_ @JsonSerializable() class _ShelterDetail implements ShelterDetail { - const _ShelterDetail({this.id = 0, this.name = '', this.capacity = 0, final List category = const [], this.indoor = false, this.outdoor = false, @JsonKey(name: 'vulnerable_ok') this.vulnerableOk = false, this.lat = 0, this.lng = 0, this.address = ''}): _category = category; + const _ShelterDetail({this.id = 0, this.name = '', this.capacity = 0, List category = const [], this.indoor = false, this.outdoor = false, @JsonKey(name: 'vulnerable_ok') this.vulnerableOk = false, this.lat = 0, this.lng = 0, this.address = ''}): _category = category; factory _ShelterDetail.fromJson(Map json) => _$ShelterDetailFromJson(json); @override@JsonKey() final int id; diff --git a/lib/features/earthquake/data/earthquake_api.dart b/lib/features/earthquake/data/earthquake_api.dart index 659830b4c..917305c0b 100644 --- a/lib/features/earthquake/data/earthquake_api.dart +++ b/lib/features/earthquake/data/earthquake_api.dart @@ -133,11 +133,10 @@ class EarthquakeApi { 'cityMaxInt': ?cityMaxInt, }; return (await _client.get( - ApiTier.coreApi, - '/api/v2/eq/report', - query: query, - )) - as List; + ApiTier.coreApi, + '/api/v2/eq/report', + query: query, + )) as List; } /// Full earthquake report by [reportId] (includes area `list`). diff --git a/lib/features/earthquake/data/rts_box_grid_source.dart b/lib/features/earthquake/data/rts_box_grid_source.dart index 450f546cd..24a70d770 100644 --- a/lib/features/earthquake/data/rts_box_grid_source.dart +++ b/lib/features/earthquake/data/rts_box_grid_source.dart @@ -1,11 +1,12 @@ import 'dart:convert'; +import 'dart:io'; import 'package:dpip/features/earthquake/domain/rts_box_grid.dart'; import 'package:flutter/services.dart' show rootBundle; /// Loads the bundled RTS box grid into the domain [RtsBoxGrid]. /// -/// The asset (`assets/box.json`, plain GeoJSON) is a `FeatureCollection` of +/// The asset (`assets/box.json.gz`, gzip → JSON) is a `FeatureCollection` of /// `Polygon` features, each carrying an integer `ID` property matched against /// `Rts.box`'s keys. Kept out of the pure domain (which only consumes the /// parsed grid) so the domain stays Flutter-free. @@ -13,9 +14,10 @@ class RtsBoxGridSource { const RtsBoxGridSource(); Future load() async { - final json = - jsonDecode(await rootBundle.loadString('assets/box.json')) - as Map; + final bytes = await rootBundle.load('assets/box.json.gz'); + final json = jsonDecode( + utf8.decode(gzip.decode(bytes.buffer.asUint8List())), + ) as Map; final rings = >>{ for (final feature in json['features'] as List) ((feature as Map)['properties'] as Map)['ID'] as int: [ diff --git a/lib/features/earthquake/data/seismic_travel_time_source.dart b/lib/features/earthquake/data/seismic_travel_time_source.dart index 0e936e996..c23fc9a3d 100644 --- a/lib/features/earthquake/data/seismic_travel_time_source.dart +++ b/lib/features/earthquake/data/seismic_travel_time_source.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:dpip/features/earthquake/domain/seismic_travel_time.dart'; import 'package:flutter/services.dart' show rootBundle; @@ -17,9 +18,13 @@ class SeismicTravelTimeSource { Future load() async { final bytes = await rootBundle.load('assets/travel_time.json.gz'); - final json = - jsonDecode(utf8.decode(gzip.decode(bytes.buffer.asUint8List()))) - as Map; + // gzip + JSON decode off the UI isolate (a 35 KB asset, and this runs + // when the replay map opens). + final json = await Isolate.run( + () => + jsonDecode(utf8.decode(gzip.decode(bytes.buffer.asUint8List()))) + as Map, + ); final rowsByDepth = >{ for (final entry in json.entries) int.parse(entry.key): [ diff --git a/lib/features/earthquake/data/trem_station_repository_impl.dart b/lib/features/earthquake/data/trem_station_repository_impl.dart index 51073cc26..53d9f9eb5 100644 --- a/lib/features/earthquake/data/trem_station_repository_impl.dart +++ b/lib/features/earthquake/data/trem_station_repository_impl.dart @@ -16,9 +16,10 @@ class TremStationRepositoryImpl implements TremStationRepository { @override Future>> stations() => guardResult( () async { - final data = - await _client.get(ApiTier.legacyApi, '/api/v1/trem/station') - as Map; + final data = await _client.get( + ApiTier.legacyApi, + '/api/v1/trem/station', + ) as Map; final directory = {}; for (final entry in data.entries) { // `{ id: { net, info: [ {code, lat, lon, time}… ], work } }` — the last diff --git a/lib/features/earthquake/domain/earthquake_report.freezed.dart b/lib/features/earthquake/domain/earthquake_report.freezed.dart index af998b702..c8eab757b 100644 --- a/lib/features/earthquake/domain/earthquake_report.freezed.dart +++ b/lib/features/earthquake/domain/earthquake_report.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'earthquake_report.dart'; @@ -9,6 +9,7 @@ part of 'earthquake_report.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -67,7 +68,7 @@ class _$EarthquakeReportCopyWithImpl<$Res> /// Create a copy of EarthquakeReport /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? longitude = null,Object? latitude = null,Object? location = null,Object? depth = null,Object? magnitude = null,Object? list = null,Object? time = null,Object? trem = null,}) { - return _then(_self.copyWith( + return _then(EarthquakeReport( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable as double,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable @@ -218,7 +219,7 @@ return $default(_that.id,_that.longitude,_that.latitude,_that.location,_that.dep @JsonSerializable() class _EarthquakeReport extends EarthquakeReport { - const _EarthquakeReport({required this.id, @JsonKey(name: 'lon') required this.longitude, @JsonKey(name: 'lat') required this.latitude, @JsonKey(name: 'loc') required this.location, required this.depth, @JsonKey(name: 'mag') required this.magnitude, required final Map list, required this.time, required this.trem}): _list = list,super._(); + const _EarthquakeReport({required this.id, @JsonKey(name: 'lon') required this.longitude, @JsonKey(name: 'lat') required this.latitude, @JsonKey(name: 'loc') required this.location, required this.depth, @JsonKey(name: 'mag') required this.magnitude, required Map list, required this.time, required this.trem}): _list = list,super._(); factory _EarthquakeReport.fromJson(Map json) => _$EarthquakeReportFromJson(json); @override final String id; @@ -361,7 +362,7 @@ class _$AreaIntensityCopyWithImpl<$Res> /// Create a copy of AreaIntensity /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? intensity = null,Object? town = null,}) { - return _then(_self.copyWith( + return _then(AreaIntensity( intensity: null == intensity ? _self.intensity : intensity // ignore: cast_nullable_to_non_nullable as int,town: null == town ? _self.town : town // ignore: cast_nullable_to_non_nullable as Map, @@ -505,7 +506,7 @@ return $default(_that.intensity,_that.town);case _: @JsonSerializable() class _AreaIntensity implements AreaIntensity { - const _AreaIntensity({@JsonKey(name: 'int') required this.intensity, required final Map town}): _town = town; + const _AreaIntensity({@JsonKey(name: 'int') required this.intensity, required Map town}): _town = town; factory _AreaIntensity.fromJson(Map json) => _$AreaIntensityFromJson(json); @override@JsonKey(name: 'int') final int intensity; @@ -633,7 +634,7 @@ class _$StationIntensityCopyWithImpl<$Res> /// Create a copy of StationIntensity /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? longitude = null,Object? latitude = null,Object? intensity = null,}) { - return _then(_self.copyWith( + return _then(StationIntensity( longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable as double,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable as double,intensity: null == intensity ? _self.intensity : intensity // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/earthquake/domain/eew.freezed.dart b/lib/features/earthquake/domain/eew.freezed.dart index 5ee00e2e1..25dbb9e82 100644 --- a/lib/features/earthquake/domain/eew.freezed.dart +++ b/lib/features/earthquake/domain/eew.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'eew.dart'; @@ -9,6 +9,7 @@ part of 'eew.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$EewCopyWithImpl<$Res> /// Create a copy of Eew /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? agency = null,Object? id = null,Object? serial = null,Object? status = null,Object? isFinal = null,Object? info = null,}) { - return _then(_self.copyWith( + return _then(Eew( agency: null == agency ? _self.agency : agency // ignore: cast_nullable_to_non_nullable as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,serial: null == serial ? _self.serial : serial // ignore: cast_nullable_to_non_nullable @@ -362,7 +363,7 @@ class _$EewInfoCopyWithImpl<$Res> /// Create a copy of EewInfo /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? longitude = null,Object? latitude = null,Object? depth = null,Object? magnitude = null,Object? location = null,Object? max = null,}) { - return _then(_self.copyWith( + return _then(EewInfo( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable as double,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/earthquake/domain/partial_earthquake_report.freezed.dart b/lib/features/earthquake/domain/partial_earthquake_report.freezed.dart index 7a124890e..0883912d4 100644 --- a/lib/features/earthquake/domain/partial_earthquake_report.freezed.dart +++ b/lib/features/earthquake/domain/partial_earthquake_report.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'partial_earthquake_report.dart'; @@ -9,6 +9,7 @@ part of 'partial_earthquake_report.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -67,7 +68,7 @@ class _$PartialEarthquakeReportCopyWithImpl<$Res> /// Create a copy of PartialEarthquakeReport /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? longitude = null,Object? latitude = null,Object? location = null,Object? depth = null,Object? magnitude = null,Object? intensity = null,Object? time = null,Object? trem = null,Object? md5 = null,}) { - return _then(_self.copyWith( + return _then(PartialEarthquakeReport( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable as double,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/earthquake/domain/rts.freezed.dart b/lib/features/earthquake/domain/rts.freezed.dart index 2513ad756..6c0b23603 100644 --- a/lib/features/earthquake/domain/rts.freezed.dart +++ b/lib/features/earthquake/domain/rts.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'rts.dart'; @@ -9,6 +9,7 @@ part of 'rts.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$RtsCopyWithImpl<$Res> /// Create a copy of Rts /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? station = null,Object? box = null,Object? intensities = null,Object? time = null,}) { - return _then(_self.copyWith( + return _then(Rts( station: null == station ? _self.station : station // ignore: cast_nullable_to_non_nullable as Map,box: null == box ? _self.box : box // ignore: cast_nullable_to_non_nullable as Map,intensities: null == intensities ? _self.intensities : intensities // ignore: cast_nullable_to_non_nullable @@ -212,7 +213,7 @@ return $default(_that.station,_that.box,_that.intensities,_that.time);case _: @JsonSerializable() class _Rts implements Rts { - const _Rts({final Map station = const {}, final Map box = const {}, @JsonKey(name: 'int') final List intensities = const [], this.time = 0}): _station = station,_box = box,_intensities = intensities; + const _Rts({ Map station = const {}, Map box = const {}, @JsonKey(name: 'int') List intensities = const [], this.time = 0}): _station = station,_box = box,_intensities = intensities; factory _Rts.fromJson(Map json) => _$RtsFromJson(json); final Map _station; @@ -305,10 +306,7 @@ as int, /// @nodoc mixin _$RtsStation { - double get pga; double get pgv;@JsonKey(name: 'i') double get intensityRaw;@JsonKey(name: 'I') double get intensity;// The wire carries the trigger flag as 0/1 (absent while calm) — decode it -// like the other API bools, or a triggered station would throw on parse -// and drop the whole snapshot right when the big-event data matters most. -@JsonKey(fromJson: boolishInt, toJson: intFromBool) bool get alert; + double get pga; double get pgv;@JsonKey(name: 'i') double get intensityRaw;@JsonKey(name: 'I') double get intensity;@JsonKey(fromJson: boolishInt, toJson: intFromBool) bool get alert; /// Create a copy of RtsStation /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -359,7 +357,7 @@ class _$RtsStationCopyWithImpl<$Res> /// Create a copy of RtsStation /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? pga = null,Object? pgv = null,Object? intensityRaw = null,Object? intensity = null,Object? alert = null,}) { - return _then(_self.copyWith( + return _then(RtsStation( pga: null == pga ? _self.pga : pga // ignore: cast_nullable_to_non_nullable as double,pgv: null == pgv ? _self.pgv : pgv // ignore: cast_nullable_to_non_nullable as double,intensityRaw: null == intensityRaw ? _self.intensityRaw : intensityRaw // ignore: cast_nullable_to_non_nullable @@ -513,9 +511,6 @@ class _RtsStation implements RtsStation { @override@JsonKey() final double pgv; @override@JsonKey(name: 'i') final double intensityRaw; @override@JsonKey(name: 'I') final double intensity; -// The wire carries the trigger flag as 0/1 (absent while calm) — decode it -// like the other API bools, or a triggered station would throw on parse -// and drop the whole snapshot right when the big-event data matters most. @override@JsonKey(fromJson: boolishInt, toJson: intFromBool) final bool alert; /// Create a copy of RtsStation diff --git a/lib/features/earthquake/presentation/pages/report_detail_page.dart b/lib/features/earthquake/presentation/pages/report_detail_page.dart index bc79b0632..a66672ea9 100644 --- a/lib/features/earthquake/presentation/pages/report_detail_page.dart +++ b/lib/features/earthquake/presentation/pages/report_detail_page.dart @@ -18,6 +18,7 @@ import 'package:dpip/core/settings/region_store.dart'; import 'package:dpip/features/earthquake/domain/earthquake_report.dart'; import 'package:dpip/features/earthquake/domain/intensity.dart'; import 'package:dpip/features/earthquake/domain/report_repository.dart'; +import 'package:dpip/features/earthquake/presentation/widgets/intensity_icon_renderer.dart'; import 'package:dpip/l10n/gen/app_localizations.dart'; import 'package:dpip/shared/map/base_map.dart'; import 'package:dpip/shared/map/camera_fit.dart'; @@ -32,7 +33,6 @@ import 'package:dpip/shared/widgets/map_color_legend.dart'; import 'package:dpip/shared/widgets/loading_view.dart'; import 'package:dpip/shared/widgets/section_header.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -199,15 +199,6 @@ class _ReportMapDetailState extends State<_ReportMapDetail> { 1.7, ]; - /// `intensity-1`…`intensity-9` (+ `-dark`) and `cross` — the legacy app's - /// own map-marker artwork, ported verbatim (same PNGs) so the report map - /// looks exactly as it did before, not a new icon style. - static final List _iconNames = [ - 'cross', - for (var i = 1; i <= 9; i++) 'intensity-$i', - for (var i = 1; i <= 9; i++) 'intensity-$i-dark', - ]; - MapLibreMapController? _controller; bool _iconsLoaded = false; @@ -282,9 +273,9 @@ class _ReportMapDetailState extends State<_ReportMapDetail> { /// Registers every marker icon once — cheap enough (19 small PNGs) to load /// eagerly rather than lazily per feature. Future _loadIcons(MapLibreMapController controller) async { - for (final name in _iconNames) { - final data = await rootBundle.load('assets/map/icons/$name.png'); - await controller.addImage(name, data.buffer.asUint8List()); + final icons = await IntensityIconRenderer.renderAll(); + for (final entry in icons.entries) { + await controller.addImage(entry.key, entry.value); } } diff --git a/lib/features/earthquake/presentation/pages/report_list_page.dart b/lib/features/earthquake/presentation/pages/report_list_page.dart index 2bc69008b..cb3095a39 100644 --- a/lib/features/earthquake/presentation/pages/report_list_page.dart +++ b/lib/features/earthquake/presentation/pages/report_list_page.dart @@ -304,8 +304,13 @@ class _DaySection extends StatelessWidget { if (day == today.subtract(const Duration(days: 1))) { return l10n.reportListYesterday; } - return DateFormat.yMMMEd(locale).format(day); + // Parsing a locale's pattern is not free — memoised per locale. + return _dayFormats + .putIfAbsent(locale, () => DateFormat.yMMMEd(locale)) + .format(day); } + + static final Map _dayFormats = {}; } class _ReportTile extends StatelessWidget { @@ -313,13 +318,18 @@ class _ReportTile extends StatelessWidget { final PartialEarthquakeReport report; + /// Numbered CWA reports — magnitude in gold to mark the official serial set. + static const Color _numberedMagGold = Color(0xFFE8C547); + + static final DateFormat _stampFormat = DateFormat('HH:mm:ss'); + @override Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; final l10n = AppLocalizations.of(context); final taipei = AppTime.taipei(report.originTimeUtc); - final stamp = DateFormat('HH:mm:ss').format(taipei); + final stamp = _stampFormat.format(taipei); final intensity = Intensity.displayForReport( report.intensity, report.originTimeUtc, @@ -336,15 +346,7 @@ class _ReportTile extends StatelessWidget { padding: const EdgeInsets.all(AppSpacing.md), child: Row( children: [ - IntensityBadge( - label: intensity.label, - color: intensityColor, - // 小區域 reports (no CWA serial) draw as a hollow ring over the - // surface — the legacy `IntensityBox(border: !hasNumber)` - // distinction, so numbered reports (solid fill) read apart from - // local-felt ones at a glance without leaning on a text hint. - outlined: !report.hasNumber, - ), + IntensityBadge(label: intensity.label, color: intensityColor), const SizedBox(width: AppSpacing.md), Expanded( child: Column( @@ -374,7 +376,7 @@ class _ReportTile extends StatelessWidget { Text( l10n.reportListMagnitude(mag), style: theme.textTheme.headlineSmall?.copyWith( - color: colors.onSurface, + color: report.hasNumber ? _numberedMagGold : colors.onSurface, fontWeight: FontWeight.w800, height: 1, letterSpacing: -0.5, diff --git a/lib/features/earthquake/presentation/pages/report_replay_page.dart b/lib/features/earthquake/presentation/pages/report_replay_page.dart index b2db67618..2e0c88f00 100644 --- a/lib/features/earthquake/presentation/pages/report_replay_page.dart +++ b/lib/features/earthquake/presentation/pages/report_replay_page.dart @@ -33,6 +33,7 @@ import 'package:dpip/features/earthquake/domain/trem_station_repository.dart'; import 'package:dpip/features/earthquake/presentation/eew_realtime_controller.dart'; import 'package:dpip/features/earthquake/presentation/rts_realtime_controller.dart'; import 'package:dpip/features/earthquake/presentation/widgets/eew_card.dart'; +import 'package:dpip/features/earthquake/presentation/widgets/intensity_icon_renderer.dart'; import 'package:dpip/features/earthquake/replay_session.dart'; import 'package:dpip/l10n/gen/app_localizations.dart'; import 'package:dpip/shared/color_hex.dart'; @@ -53,7 +54,6 @@ import 'package:dpip/shared/widgets/frosted_surface.dart'; import 'package:dpip/shared/widgets/intensity_legend.dart'; import 'package:dpip/shared/widgets/map_color_legend.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -439,8 +439,8 @@ class _ReplayMapState extends State<_ReplayMap> { if (controller == null) return; _dark = Theme.of(context).brightness == Brightness.dark; try { - final data = await rootBundle.load('assets/map/icons/cross.png'); - await controller.addImage(_crossIcon, data.buffer.asUint8List()); + final data = await IntensityIconRenderer.render('cross'); + await controller.addImage(_crossIcon, data); await _loadIntensityIcons(controller); await controller.addSource( @@ -644,6 +644,9 @@ class _ReplayMapState extends State<_ReplayMap> { Future _updateEew() async { final controller = _controller; if (controller == null || !_ready) return; + // The source starts as (and a no-alert payload would be) the empty + // collection — skip the pointless per-tick round trip entirely. + if (widget.eew.alerts.isEmpty) return; try { await controller.setGeoJsonSource(_eewSourceId, _eewGeoJson()); } catch (_) { @@ -749,15 +752,13 @@ class _ReplayMapState extends State<_ReplayMap> { return {'type': 'FeatureCollection', 'features': features}; } - /// Registers the 18 intensity icons (1–9 light + dark) — cheap PNGs, loaded - /// once per style load, mirroring the report detail map. + /// Registers the 18 intensity icons (1–9 light + dark) plus the epicentre + /// cross — drawn in code (see [IntensityIconRenderer]), loaded once per style + /// load, mirroring the report detail map. Future _loadIntensityIcons(MapLibreMapController controller) async { - for (final level in [1, 2, 3, 4, 5, 6, 7, 8, 9]) { - for (final dark in const [false, true]) { - final name = _intensityIcon(level, dark: dark); - final bytes = await rootBundle.load('assets/map/icons/$name.png'); - await controller.addImage(name, bytes.buffer.asUint8List()); - } + final icons = await IntensityIconRenderer.renderAll(); + for (final entry in icons.entries) { + await controller.addImage(entry.key, entry.value); } } @@ -1035,13 +1036,15 @@ class _ReplayStatusBar extends StatelessWidget { ); } + static final DateFormat _clockFormat = DateFormat('HH:mm:ss'); + Widget _buildContent(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final colors = theme.colorScheme; final taipeiTime = AppTime.taipei(clock.now()); - final timeText = DateFormat('HH:mm:ss').format(taipeiTime); + final timeText = _clockFormat.format(taipeiTime); final (Color dot, String? statusWord) = switch (rts.status) { RealtimeStatus.live => (Colors.green, null), diff --git a/lib/features/earthquake/presentation/widgets/intensity_icon_renderer.dart b/lib/features/earthquake/presentation/widgets/intensity_icon_renderer.dart new file mode 100644 index 000000000..30142d1b3 --- /dev/null +++ b/lib/features/earthquake/presentation/widgets/intensity_icon_renderer.dart @@ -0,0 +1,136 @@ +/// The report map's marker artwork, drawn locally instead of shipped as PNGs. +/// +/// The legacy app bundled 19 PNGs (`intensity-1`…`intensity-9`, a `-dark` +/// variant of each, and `cross`). Rendering them on the fly with the same +/// geometry keeps the map visually identical while dropping the assets — and +/// the badge colours come from [IntensityColors], so the markers can never +/// drift from the legend. Each badge is a full-bleed rounded square (white on +/// light maps, black on dark) holding a smaller rounded square in the +/// intensity colour, with the level digit — white, or black on the +/// yellow/orange badges (4–5) for contrast. `cross` is the red × marker for +/// station positions. +library; + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:dpip/shared/seismic/intensity_colors.dart'; +import 'package:flutter/painting.dart'; + +/// Renders the report-map marker icons as PNG bytes for +/// `MapLibreMapController.addImage`. +abstract final class IntensityIconRenderer { + /// The icon names, matching the legacy PNG filenames. + static final List names = [ + 'cross', + for (var i = 1; i <= 9; i++) 'intensity-$i', + for (var i = 1; i <= 9; i++) 'intensity-$i-dark', + ]; + + /// Renders every icon in [names] to PNG bytes, cached after the first call. + static Future> renderAll() async { + return _cache ??= await _renderAll(); + } + + static Future> _renderAll() async { + final icons = {}; + for (final name in names) { + icons[name] = await _paintAndEncode(name); + } + return icons; + } + + static Map? _cache; + + static Future _paintAndEncode(String name) async { + final size = name == 'cross' ? 96 : 64; + final recorder = ui.PictureRecorder(); + _paint(Canvas(recorder), size.toDouble(), name); + final image = await recorder.endRecording().toImage(size, size); + final bytes = await image.toByteData(format: ui.ImageByteFormat.png); + return bytes!.buffer.asUint8List(); + } + + /// Renders one icon by name (see [names]) to PNG bytes, cached after the + /// first call. + static Future render(String name) async { + final icons = await renderAll(); + return icons[name]!; + } + + static void _paint(Canvas canvas, double size, String name) { + if (name == 'cross') { + _paintCross(canvas, size); + return; + } + final level = int.parse(name.split('-')[1]); + final dark = name.endsWith('-dark'); + final bounds = Rect.fromLTWH(0, 0, size, size); + // Outer shell first, then the badge, so the shell shows as a rim. + canvas.drawRRect( + RRect.fromRectAndRadius(bounds, Radius.circular(size * 0.22)), + Paint()..color = dark ? _black : _white, + ); + canvas.drawRRect( + RRect.fromRectAndRadius( + bounds.deflate(size * 0.08), + Radius.circular(size * 0.16), + ), + Paint()..color = IntensityColors.discrete(level), + ); + final digit = TextPainter( + text: TextSpan( + text: '$level', + style: TextStyle( + color: level == 4 || level == 5 ? _black : _white, + fontSize: size * 0.55, + fontWeight: FontWeight.bold, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + digit.paint( + canvas, + bounds.center - Offset(digit.width / 2, digit.height / 2), + ); + } + + /// The red × station marker: a white outline stroke under the red body so it + /// stays visible on any basemap, exactly like the legacy PNG. + static void _paintCross(Canvas canvas, double size) { + final margin = 8.0; + final topLeft = Offset(margin, margin); + final bottomRight = Offset(size - margin, size - margin); + final topRight = Offset(size - margin, margin); + final bottomLeft = Offset(margin, size - margin); + for (final (start, end) in [ + (topLeft, bottomRight), + (topRight, bottomLeft), + ]) { + canvas.drawLine( + start, + end, + Paint() + ..color = _white + ..style = PaintingStyle.stroke + ..strokeWidth = size * 0.38, + ); + } + for (final (start, end) in [ + (topLeft, bottomRight), + (topRight, bottomLeft), + ]) { + canvas.drawLine( + start, + end, + Paint() + ..color = const Color(0xFFFF2C2C) + ..style = PaintingStyle.stroke + ..strokeWidth = size * 0.30, + ); + } + } + + static const Color _white = Color(0xFFFFFFFF); + static const Color _black = Color(0xFF000000); +} diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index 22b790633..d1a3524aa 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -48,6 +48,14 @@ class _HomePageState extends State { HomeSheetExtent? _extent; HomeResetSignal? _resetSignal; + /// Whether this page is the shell's visible tab — gates the sheet's + /// [TickerMode] so its animated backdrop never runs behind another tab. + bool _tabVisible = true; + + /// The shell's visible-tab notifier; `null` outside the shell (tests, + /// previews) means always visible. + VisibleTab? _visibleTab; + /// Peak blur sigma over the exposed map once the sheet is fully up — matches /// the sheet's own frosted blur ([HomeSheet] at full opacity) so the map's /// edge crossing under the sheet doesn't read as a hard transition. @@ -58,6 +66,13 @@ class _HomePageState extends State { /// content and is never dimmed with it. static const double _mapDimPeak = 0.35; + /// The filter instance last handed to the [ImageFiltered] — [ImageFilter] + /// has no value equality, so a fresh `blur(...)` per drag tick would + /// recomposite the full-screen blur every frame even though the sigma + /// quantises to the same step (same pattern as [_CachedBlur] in HomeSheet). + ImageFilter? _mapBlur; + double _mapBlurSigma = -1; + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -67,6 +82,23 @@ class _HomePageState extends State { _resetSignal?.removeListener(_resetSheet); _resetSignal = signal..addListener(_resetSheet); } + // Subscribes to the notifier itself: the scope never notifies dependents + // (same instance handed down — see VisibleTabScope's doc), so this gate + // would otherwise freeze at its first value and the backdrop tickers would + // keep running behind every other tab. + final visibleTab = VisibleTabScope.of(context); + if (identical(visibleTab, _visibleTab)) return; + _visibleTab?.removeListener(_syncTabVisibility); + _visibleTab = visibleTab; + visibleTab?.addListener(_syncTabVisibility); + _syncTabVisibility(); + } + + void _syncTabVisibility() { + final visible = + (_visibleTab?.value ?? HomePage.tabIndex) == HomePage.tabIndex; + if (visible == _tabVisible) return; + setState(() => _tabVisible = visible); } /// Publishes the live extent so the chrome (region bar + bottom nav) can @@ -96,6 +128,7 @@ class _HomePageState extends State { @override void dispose() { + _visibleTab?.removeListener(_syncTabVisibility); _resetSignal?.removeListener(_resetSheet); _sheet.dispose(); super.dispose(); @@ -117,6 +150,13 @@ class _HomePageState extends State { final humidityPct = backdrop.humidity; final humidity = humidityPct == null ? null : humidityPct / 100; final extent = context.read(); + // The shell's IndexedStack keeps every tab mounted, so Home's animated + // backdrop (weather ticker, particle field, card-water shaders) would keep + // running behind any other tab — burning the low-end GPU that page is + // trying to draw with. [TickerMode] mutes every ticker under the sheet + // while Home is hidden; they resume on return (the sky's dt clamp absorbs + // the gap, exactly as a background-resume does). + final visible = _tabVisible; return RefreshOnAppear( tabIndex: HomePage.tabIndex, onAppear: _refresh, @@ -163,8 +203,17 @@ class _HomePageState extends State { valueListenable: extent, builder: (context, extentValue, _) { final t = HomeChrome.mapDim(extentValue); - final sigma = t * _mapBlurPeak; + // Quantised like the sheet's own blur: the exposed map's + // full-screen blur recomposites on level crossings only. + final sigma = _mapBlurPeak * ((t * 6).round() / 6); final dim = t * _mapDimPeak; + if (sigma != _mapBlurSigma) { + _mapBlurSigma = sigma; + _mapBlur = ImageFilter.blur( + sigmaX: sigma, + sigmaY: sigma, + ); + } // The tree's shape never changes — no SizedBox/ImageFiltered // swap at t=0, which would re-parent the subtree right over // the map platform view at the exact edge the sheet starts @@ -172,10 +221,7 @@ class _HomePageState extends State { // `enabled` makes the filter a no-op at rest without // touching the tree. return ImageFiltered( - imageFilter: ImageFilter.blur( - sigmaX: sigma, - sigmaY: sigma, - ), + imageFilter: _mapBlur!, enabled: t > 0, child: ColoredBox( color: Colors.black.withValues(alpha: dim), @@ -188,27 +234,30 @@ class _HomePageState extends State { // The one weather sheet — full-screen behind the region bar so its // weather fills up into (and past) the bar. Positioned.fill( - child: NotificationListener( - onNotification: _onExtentChanged, - child: DraggableScrollableSheet( - controller: _sheet, - // Built-in velocity-aware snapping between the two detents, so - // a flick keeps its momentum and settles up. The old manual - // pointer-up settle snapped by position only (no velocity), so - // any short drag up sprang back to rest with no inertia. - snap: true, - // Floor = rest: the sheet is never smaller than its default. - initialChildSize: HomeSheet.restExtent, - minChildSize: HomeSheet.restExtent, - maxChildSize: HomeSheet.maxExtent, - builder: (context, scrollController) => HomeSheet( - scrollController: scrollController, - extent: extent, - weatherMode: weatherMode, - skyTimeMode: skyTimeMode, - rainIntensity: rainIntensity, - snowIntensity: snowIntensity, - humidity: humidity == null ? null : humidity / 100, + child: TickerMode( + enabled: visible, + child: NotificationListener( + onNotification: _onExtentChanged, + child: DraggableScrollableSheet( + controller: _sheet, + // Built-in velocity-aware snapping between the two detents, so + // a flick keeps its momentum and settles up. The old manual + // pointer-up settle snapped by position only (no velocity), so + // any short drag up sprang back to rest with no inertia. + snap: true, + // Floor = rest: the sheet is never smaller than its default. + initialChildSize: HomeSheet.restExtent, + minChildSize: HomeSheet.restExtent, + maxChildSize: HomeSheet.maxExtent, + builder: (context, scrollController) => HomeSheet( + scrollController: scrollController, + extent: extent, + weatherMode: weatherMode, + skyTimeMode: skyTimeMode, + rainIntensity: rainIntensity, + snowIntensity: snowIntensity, + humidity: humidity == null ? null : humidity / 100, + ), ), ), ), diff --git a/lib/features/home/presentation/widgets/home_content.dart b/lib/features/home/presentation/widgets/home_content.dart index f3b764e2b..04895dacb 100644 --- a/lib/features/home/presentation/widgets/home_content.dart +++ b/lib/features/home/presentation/widgets/home_content.dart @@ -92,9 +92,9 @@ class HomeContent extends StatelessWidget { // overrides it to the bottom-nav bar's reserved height, so this reads // straight off the platform view (see the original `_build` doc). Computed // once here, not per scroll tick. - final bottomSafeArea = MediaQueryData.fromView( - View.of(context), - ).padding.bottom; + final bottomSafeArea = MediaQueryData.fromView(View.of(context)) + .padding + .bottom; // The sky re-bakes rarely; the scroll focus dial moves on every tick. The // ListView's *shell* rebuilds only when the sky changes — the scroll-driven // reveal/focus dial lives one level down, on the panel's own listenable, @@ -536,9 +536,8 @@ class HomeSheetHandle extends StatelessWidget { @override Widget build(BuildContext context) { - final color = Theme.of( - context, - ).colorScheme.onSurfaceVariant.withValues(alpha: 0.4); + final color = Theme.of(context).colorScheme.onSurfaceVariant + .withValues(alpha: 0.4); return Center( child: Container( margin: const EdgeInsets.symmetric(vertical: AppSpacing.md), diff --git a/lib/features/home/presentation/widgets/home_map_backdrop.dart b/lib/features/home/presentation/widgets/home_map_backdrop.dart index 43ec65247..8f454c78e 100644 --- a/lib/features/home/presentation/widgets/home_map_backdrop.dart +++ b/lib/features/home/presentation/widgets/home_map_backdrop.dart @@ -7,6 +7,7 @@ import 'package:dpip/core/settings/home_area.dart'; import 'package:dpip/core/settings/region_store.dart'; import 'package:dpip/features/home/presentation/home_reset_signal.dart'; import 'package:dpip/features/home/presentation/home_sheet_extent.dart'; +import 'package:dpip/features/home/presentation/pages/home_page.dart'; import 'package:dpip/shared/map/admin_outline.dart'; import 'package:dpip/features/weather/domain/radar_repository.dart'; import 'package:dpip/shared/map/base_map.dart'; @@ -451,6 +452,9 @@ class _HomeMapBackdropState extends State } return BaseMap( interactive: false, + // The backdrop sits in the home tab — pause its native render loop + // when the user is elsewhere (indexedStack keeps it mounted). + tabIndex: HomePage.tabIndex, onMapCreated: _onMapCreated, onStyleLoaded: _onStyleLoaded, ); diff --git a/lib/features/home/presentation/widgets/home_rain_trend_section.dart b/lib/features/home/presentation/widgets/home_rain_trend_section.dart index 8a870df24..17806a437 100644 --- a/lib/features/home/presentation/widgets/home_rain_trend_section.dart +++ b/lib/features/home/presentation/widgets/home_rain_trend_section.dart @@ -262,7 +262,12 @@ class _NoData extends StatelessWidget { /// The trend's bar chart: one rod per minute, no mm / numeric Y labels — height /// alone carries intensity; the bottom axis labels minutes from now. -class _Chart extends StatelessWidget { +/// +/// Stateful so the chart object is only rebuilt when an input actually +/// changes: the sheet's scroll tick rebuilds this subtree every frame, and +/// fl_chart re-lays-out the whole 60-rod set on every build even when the data +/// is identical. +class _Chart extends StatefulWidget { const _Chart({ required this.data, required this.now, @@ -287,12 +292,19 @@ class _Chart extends StatelessWidget { static const double _plotBand = _chartHeight - _titleBand; /// Pixels the label occupies, so the tick can be re-centred on its axis. + /// Memoised — the labels are a handful of fixed strings laid out at a fixed + /// font size, and this ran 6× per scroll tick. + static final Map _labelWidths = {}; + static double _textWidth(String text, TextStyle? style) { - final painter = TextPainter( - text: TextSpan(text: text, style: style), - textDirection: TextDirection.ltr, - )..layout(); - return painter.width; + final key = '$text|${style?.fontSize ?? 0}'; + return _labelWidths.putIfAbsent(key, () { + final painter = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr, + )..layout(); + return painter.width; + }); } final RainHourTrend data; @@ -306,7 +318,32 @@ class _Chart extends StatelessWidget { final Color secondary; @override - Widget build(BuildContext context) { + State<_Chart> createState() => _ChartState(); +} + +class _ChartState extends State<_Chart> { + late Widget _chart = _build(); + + @override + void didUpdateWidget(covariant _Chart oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data || + !oldWidget.now.isAtSameMomentAs(widget.now) || + oldWidget.barColor != widget.barColor || + oldWidget.secondary != widget.secondary) { + _chart = _build(); + } + } + + @override + Widget build(BuildContext context) => _chart; + + Widget _build() { + final data = widget.data; + final now = widget.now; + final barColor = widget.barColor; + final secondary = widget.secondary; + final context = this.context; final l10n = AppLocalizations.of(context); // The chart runs from "now"; the data covers [start, start+60 m) only. // Minutes outside that window have no forecast — [headEnd] bars before the @@ -322,7 +359,7 @@ class _Chart extends StatelessWidget { double valueAt(int i) { final source = i + elapsed; if (source < 0 || source >= data.mm.length) return 0; - return math.min(data.mm[source], _maxMm); + return math.min(data.mm[source], _Chart._maxMm); } // Minutes outside the forecast window carry no forecast — render their rods @@ -346,7 +383,7 @@ class _Chart extends StatelessWidget { final bar = BarChart( BarChartData( minY: 0, - maxY: _maxMm, + maxY: _Chart._maxMm, alignment: BarChartAlignment.spaceBetween, groupsSpace: 0, barGroups: [ @@ -423,20 +460,19 @@ class _Chart extends StatelessWidget { interval: 1, getTitlesWidget: (value, meta) { final minute = value.round(); - if (!_ticks.contains(minute)) { + if (!_Chart._ticks.contains(minute)) { return const SizedBox.shrink(); } final label = minute == 0 ? l10n.mapTimelineNow : l10n.homeRainTrendMinute(minute); - final style = Theme.of( - context, - ).textTheme.labelSmall?.copyWith(color: secondary); + final style = Theme.of(context).textTheme.labelSmall + ?.copyWith(color: secondary); // fl_chart centres every title widget on its tick's axis // position, so a bare [tick, label] row would push the tick // left of the bar (X=0's lands off the chart). Mirroring the // label width on the left re-centres the tick on the axis. - final labelWidth = _textWidth(label, style); + final labelWidth = _Chart._textWidth(label, style); return Padding( padding: EdgeInsets.only( left: labelWidth + AppSpacing.xs, @@ -478,7 +514,7 @@ class _Chart extends StatelessWidget { left: 0, top: 0, width: headX, - height: _plotBand, + height: _Chart._plotBand, child: _NoDataLabel( color: secondary, text: l10n.homeRainTrendNoData, @@ -489,7 +525,7 @@ class _Chart extends StatelessWidget { left: tailX, top: 0, right: 0, - height: _plotBand, + height: _Chart._plotBand, child: _NoDataLabel( color: secondary, text: l10n.homeRainTrendNoData, @@ -500,7 +536,7 @@ class _Chart extends StatelessWidget { }, ); - return SizedBox(height: _chartHeight, child: chart); + return SizedBox(height: _Chart._chartHeight, child: chart); } } diff --git a/lib/features/home/presentation/widgets/home_sheet.dart b/lib/features/home/presentation/widgets/home_sheet.dart index ec2bf2d43..468e69cf6 100644 --- a/lib/features/home/presentation/widgets/home_sheet.dart +++ b/lib/features/home/presentation/widgets/home_sheet.dart @@ -93,6 +93,11 @@ class HomeSheet extends StatelessWidget { return lerpDouble(_restAlpha, 1, t)!; } + /// Quantises a 0..1 ramp into [levels] discrete steps — full-screen blur + /// sigmas then only change on level crossings, so the filter isn't fed a + /// fresh value on every drag tick. + static double _step(double t, int levels) => (t * levels).round() / levels; + @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; @@ -113,10 +118,16 @@ class HomeSheet extends StatelessWidget { ); final surfaceAlpha = _surfaceAlpha(e); final weatherOpacity = HomeChrome.weatherReveal(e); + // Quantised so the full-screen backdrop blur re-renders on a few level + // changes through the drag instead of a fresh sigma every pixel — + // same ladder trick as [_ScrollBlurredWeather]'s scroll blur. final blur = 24.0 * - (surfaceAlpha / _restAlpha).clamp(0.0, 1.0) * - (1 - weatherOpacity); + _step( + (surfaceAlpha / _restAlpha).clamp(0.0, 1.0) * + (1 - weatherOpacity), + 6, + ); final borderRadius = BorderRadius.vertical( top: Radius.circular(lerpDouble(AppRadius.lg, 0, flush)!), ); @@ -137,8 +148,8 @@ class HomeSheet extends StatelessWidget { fit: StackFit.expand, children: [ // Frosted map-through backdrop, dominant while collapsed. - BackdropFilter( - filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), + _CachedBlur( + sigma: blur, child: ColoredBox( color: colors.surface.withValues(alpha: surfaceAlpha), ), @@ -192,6 +203,38 @@ class HomeSheet extends StatelessWidget { ((e - _flushFrom) / (maxExtent - _flushFrom)).clamp(0.0, 1.0); } +/// A [BackdropFilter] that reuses its [ImageFilter] instance while [sigma] +/// stays on the same quantised step. +/// +/// [ImageFilter] has no value equality, so a fresh `blur(...)` per drag tick +/// marks the whole-screen blur layer dirty and recomposites it every frame +/// even though the sigma only changes on a few level crossings (the sheet +/// quantises it to 6 steps). Reusing the instance keeps the layer cached +/// between steps — the same pattern [_ScrollBlurredWeather] uses. +class _CachedBlur extends StatefulWidget { + const _CachedBlur({required this.sigma, required this.child}); + + final double sigma; + final Widget child; + + @override + State<_CachedBlur> createState() => _CachedBlurState(); +} + +class _CachedBlurState extends State<_CachedBlur> { + ImageFilter? _filter; + double _sigma = -1; + + @override + Widget build(BuildContext context) { + if (widget.sigma != _sigma) { + _sigma = widget.sigma; + _filter = ImageFilter.blur(sigmaX: _sigma, sigmaY: _sigma); + } + return BackdropFilter(filter: _filter!, child: widget.child); + } +} + /// Blurs and dims [child] in step with [scrollController], so the sky reads as /// depth of field behind the content once the sheet's list scrolls the hero's /// rain trend card up past the fold — the counterpart to `HomeContent`'s hero @@ -217,7 +260,7 @@ class HomeSheet extends StatelessWidget { /// so only this small leaf repaints on every scroll tick — not the sheet's /// whole frosted-chrome tree, which is the mistake `HomeSheet`'s own class doc /// warns against for the drag case. -class _ScrollBlurredWeather extends StatelessWidget { +class _ScrollBlurredWeather extends StatefulWidget { const _ScrollBlurredWeather({ required this.scrollController, required this.child, @@ -226,6 +269,11 @@ class _ScrollBlurredWeather extends StatelessWidget { final ScrollController scrollController; final Widget child; + @override + State<_ScrollBlurredWeather> createState() => _ScrollBlurredWeatherState(); +} + +class _ScrollBlurredWeatherState extends State<_ScrollBlurredWeather> { /// Scroll distance over which blur reaches its peak. Short on purpose: the /// trend card itself is most of a screen's scroll away, and holding the sky /// crisp for that whole distance would make the blur feel disconnected from @@ -244,14 +292,25 @@ class _ScrollBlurredWeather extends StatelessWidget { /// turning the whole backdrop into a black hole at the top of the gesture. static const double _maxDim = 0.45; + /// The filter instance last handed to the [ImageFiltered] — [ImageFilter] + /// has no value equality, so a fresh `blur(...)` every scroll tick would + /// make the full-screen blur layer recomposite on **every** tick even though + /// the sigma quantises to the same value. Reusing the instance keeps the + /// layer cached for the whole 4-step scroll ramp. + ImageFilter? _filter; + + /// Sigma the cached [_filter] was built for — the ladder is quantised, so + /// this only differs on the handful of step crossings. + double _filterSigma = -1; + @override Widget build(BuildContext context) { return ListenableBuilder( - listenable: scrollController, - child: child, + listenable: widget.scrollController, + child: widget.child, builder: (context, child) { - final offset = scrollController.hasClients - ? scrollController.offset + final offset = widget.scrollController.hasClients + ? widget.scrollController.offset : 0.0; final t = (offset / _rampExtent).clamp(0.0, 1.0); // Quantised so the full-screen blur re-renders only on a few level @@ -263,6 +322,10 @@ class _ScrollBlurredWeather extends StatelessWidget { final step = (t * 4).round() / 4; final sigma = _maxSigma * step; final dim = _maxDim * step; + if (sigma != _filterSigma) { + _filterSigma = sigma; + _filter = ImageFilter.blur(sigmaX: sigma, sigmaY: sigma); + } // The tree's SHAPE never changes — a blur that toggles via `enabled` // and an always-present transparent dim. Returning the bare child at // rest (as an early version did) re-parents the sky's element the @@ -275,7 +338,7 @@ class _ScrollBlurredWeather extends StatelessWidget { fit: StackFit.expand, children: [ ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), + imageFilter: _filter!, // No-op at rest, so no offscreen layer is composited — the sheet // spends most of its time here, and ImageFiltered.enabled skips // the filter without touching the tree shape. diff --git a/lib/features/home/presentation/widgets/weather_sky/precipitation_field.dart b/lib/features/home/presentation/widgets/weather_sky/precipitation_field.dart index 375c8d2db..cea01ad31 100644 --- a/lib/features/home/presentation/widgets/weather_sky/precipitation_field.dart +++ b/lib/features/home/presentation/widgets/weather_sky/precipitation_field.dart @@ -120,6 +120,9 @@ class PrecipitationField { // `tint` never varies per particle — only alpha does — so pack its RGB // once and OR in the 8-bit alpha instead of building a `Color` per drop. final rgb = tint.toARGB32() & 0x00FFFFFF; + // The horizontal wind deflection (`fall * wind * 0.6`) folds into a + // single invariant multiplier instead of re-multiplying per drop. + final windX = wind * 0.6; var n = 0; for (var i = 0; i < live; i++) { @@ -130,7 +133,7 @@ class PrecipitationField { final speedScale = 0.12 + 0.88 * depth; final fall = baseFall * speedScale; _p.y[i] += fall; - _p.x[i] += fall * wind * 0.6; + _p.x[i] += fall * windX; // Age is the *only* respawn trigger, as in the reference: a drop that has fallen // off the bottom stays dead for the rest of its 3 s life. Recycling it on diff --git a/lib/features/home/presentation/widgets/weather_sky/sky_clouds.dart b/lib/features/home/presentation/widgets/weather_sky/sky_clouds.dart index 35c8e1567..1c35ce77f 100644 --- a/lib/features/home/presentation/widgets/weather_sky/sky_clouds.dart +++ b/lib/features/home/presentation/widgets/weather_sky/sky_clouds.dart @@ -170,12 +170,15 @@ List placeClouds( // How many instances the current coverage justifies. Below ~0.08 the sky is // clear and nothing is drawn at all. final visible = (layout.clouds.length * coverage.clamp(0.0, 1.0)).ceil(); + // Loop-invariant pieces of the per-cloud math: the wind-scaled deck speed + // and the depth-dependent size/opacity factors are hoisted out of the loop. + final windSpeed = layout.speed * (1.0 + wind * 2.0); for (var i = 0; i < layout.clouds.length && i < visible; i++) { final c = layout.clouds[i]; // Near clouds drift faster — the parallax the reference gets from its 3D layout. - final speed = layout.speed * (1.0 + wind * 2.0) * (1.6 - c.depth); + final speed = windSpeed * (1.6 - c.depth); // Wrap over two screen widths so a sprite is never popped into view. final span = 2.4; var x = (c.x + time * speed) % span; diff --git a/lib/features/home/presentation/widgets/weather_sky/weather_sky_background.dart b/lib/features/home/presentation/widgets/weather_sky/weather_sky_background.dart index ba6000201..fd813f924 100644 --- a/lib/features/home/presentation/widgets/weather_sky/weather_sky_background.dart +++ b/lib/features/home/presentation/widgets/weather_sky/weather_sky_background.dart @@ -2,6 +2,8 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/platform/device_info.dart'; +import 'package:dpip/core/platform/render_tier.dart'; import 'package:dpip/core/realtime/app_time.dart'; import 'package:dpip/core/settings/sky_time_mode.dart'; import 'package:dpip/core/settings/weather_mode.dart'; @@ -88,8 +90,15 @@ class _WeatherSkyBackgroundState extends State /// upscales. This port draws every layer in one pass, so a /// single scale has to serve both; 0.75 matches the precipitation layers, /// which are the ones that suffer most from being softened, and leaves the - /// cloud deck only slightly sharper than the original. - static const double _renderScale = 0.75; + /// cloud deck only slightly sharper than the original. The low tier draws + /// at 0.6 — a softer upscale, not a lower frame rate. + static const double _renderScaleHigh = 0.75; + static const double _renderScaleLow = 0.6; + + /// The device tier, resolved once before the particles are built (their + /// pools depend on it). Null only while the first tier probe is in flight; + /// the backdrop holds its flat fallback colour until [_load] lands. + RenderTier? _tier; /// Number of cloud sprites in `assets/weather/clouds/`. static const int _spriteCount = 12; @@ -101,6 +110,7 @@ class _WeatherSkyBackgroundState extends State static const List _layerAssets = [ WeatherSkyPainter.nightAsset, + WeatherSkyPainter.nightFieldAsset, WeatherSkyPainter.cloudsAsset, WeatherSkyPainter.lightningAsset, WeatherSkyPainter.sunFlareAsset, @@ -123,11 +133,51 @@ class _WeatherSkyBackgroundState extends State /// Wall-clock of the previous frame, for the particle integration step. double _lastFrameSeconds = 0; + /// The painter instance from the last animated frame, reused while the + /// animation is stopped so rebuilds of the subtree above don't repaint the + /// sky (see the [CustomPaint] construction in build). + WeatherSkyPainter? _lastPainter; + + /// UTC day of the cached [_sun] / [_moon] (sunrise/sunset move ~1 min + /// per day; the moon phase by ~2.5 % per day — frame-level recomputation is + /// pure waste, and day-level caching keeps them exact for a whole session). + int _dayKey = -1; + ({double sunrise, double sunset}) _sun = (sunrise: 5.6, sunset: 18.4); + double _moon = 0; + + /// Local minute of the cached [_position] / [_sky]. The keyframe ring only + /// advances ~17 frames over a day, so one tick a minute is ~1/85,000 of the + /// ring — far below the LUT's own resolution. Batching to the minute means + /// the scattering bake (256×256 + 256×141 rasterise + CPU readback + + /// gradient rebuild) runs once a minute instead of once a second, which is + /// the difference between a cheap backdrop and one that drops a frame + /// mid-animation on a low-end GPU. + int _minuteKey = -1; + double _position = 0; + ResolvedSky? _sky; + @override void initState() { super.initState(); _syncRunning(); - _load(); + _init(); + } + + /// Resolves the device tier, then loads shaders/sprites/particles — the + /// particle pools and render scale depend on the tier, so it must land + /// first. A tier probe failure keeps full quality (never degrade an + /// experience on a measurement we didn't get). + Future _init() async { + RenderTier tier; + try { + tier = renderTierFor(await DeviceInfoService.load()); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'Device tier probe failed'); + tier = RenderTier.high; + } + if (!mounted) return; + _tier = tier; + await _load(); } Future _load() async { @@ -229,13 +279,16 @@ class _WeatherSkyBackgroundState extends State ? SkyLutCache(transmittance, skyLut) : null; // The rain atlas packs four width variants; snow is a single cell. + final lowEnd = _tier == RenderTier.low; _rain = rainAtlas == null ? null : PrecipitationField( atlas: rainAtlas, // The reference's storm preset pool; lighter rain uses proportionally - // fewer, which is how it expresses intensity. - capacity: 1792, + // fewer, which is how it expresses intensity. The low tier caps at + // the 大雨 pool — the storm's full 1792-drop budget is the single + // most expensive layer a low-end GPU can be asked to draw. + capacity: lowEnd ? 1024 : 1792, variants: 4, cell: const Size(32, 68), seed: 7, @@ -244,7 +297,7 @@ class _WeatherSkyBackgroundState extends State ? null : PrecipitationField( atlas: snowAtlas, - capacity: 900, + capacity: lowEnd ? 640 : 900, cell: const Size(40, 40), tumble: true, seed: 11, @@ -318,19 +371,29 @@ class _WeatherSkyBackgroundState extends State final dt = (frame.time - _lastFrameSeconds).clamp(0.0, 1 / 20); _lastFrameSeconds = frame.time; - return CustomPaint( - size: Size.infinite, - painter: WeatherSkyPainter( - shaders: _shaders, - lutCache: cache, - cloudSprites: _sprites, - sunTextures: _sunTextures, - frame: frame, - rainField: _rain, - snowField: _snow, - dt: dt, - ), - ); + // While the animation is stopped (`active: false` — the sheet is + // collapsed, or the sky sits under the scroll blur), the scene + // cannot change, so a rebuilt painter is byte-identical. Handing + // the *same* painter instance back lets RenderCustomPaint skip + // the repaint on those rebuilds (it compares by identity) — the + // scroll-driven rebuilds of the subtree above this widget + // (`_ScrollBlurredWeather`) then cost an element update instead + // of a full-screen shader stack re-render on every scroll tick. + final moving = widget.active && dt > 0; + final painter = moving || _lastPainter == null + ? WeatherSkyPainter( + shaders: _shaders, + lutCache: cache, + cloudSprites: _sprites, + sunTextures: _sunTextures, + frame: frame, + rainField: _rain, + snowField: _snow, + dt: dt, + ) + : _lastPainter!; + _lastPainter = painter; + return CustomPaint(size: Size.infinite, painter: painter); }, ); @@ -339,16 +402,19 @@ class _WeatherSkyBackgroundState extends State } // Rasterise small and let the transform upscale bilinearly. + final scale = _tier == RenderTier.low + ? _renderScaleLow + : _renderScaleHigh; return ClipRect( child: Align( alignment: Alignment.topLeft, child: Transform.scale( - scale: 1 / _renderScale, + scale: 1 / scale, alignment: Alignment.topLeft, filterQuality: FilterQuality.low, child: SizedBox( - width: (constraints.maxWidth * _renderScale).ceilToDouble(), - height: (constraints.maxHeight * _renderScale).ceilToDouble(), + width: (constraints.maxWidth * scale).ceilToDouble(), + height: (constraints.maxHeight * scale).ceilToDouble(), child: painted, ), ), @@ -372,32 +438,11 @@ class _WeatherSkyBackgroundState extends State final rain = widget.rainIntensity ?? look.rain; final snow = widget.snowIntensity ?? look.snow; - // Anchor the ring to the day's real sunrise and sunset, so dawn keyframes - // land at dawn in December as well as June. - final sun = sunTimes( - utc, - latitude: widget.latitude, - longitude: widget.longitude, - ); - final hour = - skyTimeHour(widget.timeMode) ?? - (local.hour + local.minute / 60.0 + local.second / 3600.0); - final position = keyframePosition( - hour, - frameCount: frames.length, - sunrise: sun.sunrise, - sunset: sun.sunset, - ); - - final sky = resolveSky( - frames, - position: position, - humidity: widget.humidity, - ); + _syncSky(utc, local, frames); return SkyFrame( time: time, - sky: sky, + sky: _sky!, cloudLayout: look.layout, cloudCoverage: look.coverage, rain: rain, @@ -406,11 +451,47 @@ class _WeatherSkyBackgroundState extends State rainbow: look.rainbow, wind: look.wind, lightning: look.lightning ? _lightningAt(time) : LightningFrame.none, - moonPhase: _moonPhase(utc), - keyframePosition: position, + moonPhase: _moon, + keyframePosition: _position, ); } + /// Recomputes the sky pieces that have their own cadence — the sun anchor and + /// moon phase once a day, the keyframe ring position once a minute — and + /// caches them for the per-frame [SkyFrame] assembly. + /// + /// This is what keeps the per-frame cost of `_buildFrame` down to the parts + /// that genuinely move every frame ([_lightningAt], particle time): before + /// this, the ephemeris ran (and the LUT re-baked, see [_minuteKey]) on every + /// tick. + void _syncSky(DateTime utc, DateTime local, List frames) { + final dayKey = utc.millisecondsSinceEpoch ~/ 86400000; + if (dayKey != _dayKey) { + _dayKey = dayKey; + // Anchor the ring to the day's real sunrise and sunset, so dawn keyframes + // land at dawn in December as well as June. + _sun = sunTimes( + utc, + latitude: widget.latitude, + longitude: widget.longitude, + ); + _moon = _moonPhase(utc); + } + final minuteKey = local.millisecondsSinceEpoch ~/ 60000; + if (minuteKey != _minuteKey) { + _minuteKey = minuteKey; + final hour = + skyTimeHour(widget.timeMode) ?? (local.hour + local.minute / 60.0); + _position = keyframePosition( + hour, + frameCount: frames.length, + sunrise: _sun.sunrise, + sunset: _sun.sunset, + ); + _sky = resolveSky(frames, position: _position, humidity: widget.humidity); + } + } + Color _fallbackColour(WeatherMode mode) => switch (mode) { WeatherMode.thunderstorm => const Color(0xFF2A2F3A), WeatherMode.rain => const Color(0xFF44505F), diff --git a/lib/features/home/presentation/widgets/weather_sky/weather_sky_painter.dart b/lib/features/home/presentation/widgets/weather_sky/weather_sky_painter.dart index 0140593a8..0ede40843 100644 --- a/lib/features/home/presentation/widgets/weather_sky/weather_sky_painter.dart +++ b/lib/features/home/presentation/widgets/weather_sky/weather_sky_painter.dart @@ -157,6 +157,7 @@ class WeatherSkyPainter extends CustomPainter { // Asset keys, matching `pubspec.yaml`. static const String cloudsAsset = 'shaders/cloud/clouds.frag'; static const String nightAsset = 'shaders/weather/night.frag'; + static const String nightFieldAsset = 'shaders/weather/night_field.frag'; static const String lightningAsset = 'shaders/weather/lightning.frag'; static const String sunFlareAsset = 'shaders/weather/sun_flare.frag'; static const String rainbowAsset = 'shaders/weather/rainbow.frag'; @@ -240,9 +241,45 @@ class WeatherSkyPainter extends CustomPainter { // the procedural band stays off. set(0.0); + // The star field is static apart from the per-star shimmer/twinkle the + // display shader animates itself — bake it once (the field tiles, so one + // texture serves any view size) and hand it to the shader. The bake is a + // synchronous GPU rasterisation, but it happens once, not per frame. + if (_nightField == null) { + final bake = shaders[nightFieldAsset]; + if (bake != null) { + bake.setFloat(0, _nightFieldSize.toDouble()); + bake.setFloat(1, _nightFieldSize.toDouble()); + final recorder = ui.PictureRecorder(); + ui.Canvas(recorder).drawRect( + Offset.zero & Size.square(_nightFieldSize.toDouble()), + Paint()..shader = bake, + ); + final picture = recorder.endRecording(); + final image = picture.toImageSync(_nightFieldSize, _nightFieldSize); + picture.dispose(); + _nightField?.dispose(); + _nightField = image; + } + } + if (_nightField == null) { + // Bake failed (shader not loaded yet) — nothing to sample; skip the + // frame rather than render a black sky. + return; + } + shader.setImageSampler(0, _nightField!); _fill(canvas, size, shader); } + /// Baked star-field texture (RGBA = the four star layers), produced once by + /// [night_field.frag]. Shared across painters: the field is view-size + /// independent (it tiles), so one bake serves every sky. + static ui.Image? _nightField; + + /// Bake texture edge, in pixels — 4 bright cells at 128 px each. The 16-cell + /// faint layer's stars stay ~4 px wide at this resolution. + static const int _nightFieldSize = 512; + // --- clouds ------------------------------------------------------------- void _paintClouds(Canvas canvas, Size size) { final shader = shaders[cloudsAsset]; @@ -551,19 +588,37 @@ class WeatherSkyPainter extends CustomPainter { shader.setFloat(0, quarter.width); shader.setFloat(1, quarter.height); - final recorder = ui.PictureRecorder(); - ui.Canvas( - recorder, - ).drawRect(Offset.zero & quarter, Paint()..shader = shader); - final picture = recorder.endRecording(); - final small = picture.toImageSync( - quarter.width.toInt(), - quarter.height.toInt(), + // The bake is a synchronous GPU rasterisation on the UI thread, and the + // sun's motion is slow (a keyframed arc + ~2 rad/s rays) — re-bake only + // when a ~150 ms time bucket (or any other uniform) actually crosses, and + // blit the cached image in between. The key covers every shader input, so + // the cache can never serve a stale frame. + final bakeKey = ( + quarter.width, + quarter.height, + (frame.time / 0.15).floorToDouble(), + sunX, + sunY, + intensity, + golden, + frame.cloudCoverage, ); - picture.dispose(); + if (_sunFlare == null || bakeKey != _sunFlareKey) { + _sunFlareKey = bakeKey; + _sunFlare?.dispose(); + final recorder = ui.PictureRecorder(); + ui.Canvas(recorder) + .drawRect(Offset.zero & quarter, Paint()..shader = shader); + final picture = recorder.endRecording(); + _sunFlare = picture.toImageSync( + quarter.width.toInt(), + quarter.height.toInt(), + ); + picture.dispose(); + } canvas.drawImageRect( - small, + _sunFlare!, Offset.zero & quarter, Offset.zero & size, Paint() @@ -571,9 +626,14 @@ class WeatherSkyPainter extends CustomPainter { ..filterQuality = FilterQuality.low ..blendMode = BlendMode.plus, ); - small.dispose(); } + /// Quarter-res sun flare, re-baked only on input change — see + /// [_paintSunFlare]. One shared image: only the home sky draws the sun. + static ui.Image? _sunFlare; + static (double, double, double, double, double, double, double, double) + _sunFlareKey = (0, 0, 0, 0, 0, 0, 0, 0); + // --- rainbow ------------------------------------------------------------ void _paintRainbow(Canvas canvas, Size size) { final shader = shaders[rainbowAsset]; diff --git a/lib/features/map/presentation/layers/admin_outline_chrome.dart b/lib/features/map/presentation/layers/admin_outline_chrome.dart index 70c5daac3..27fbbb1eb 100644 --- a/lib/features/map/presentation/layers/admin_outline_chrome.dart +++ b/lib/features/map/presentation/layers/admin_outline_chrome.dart @@ -189,9 +189,8 @@ mixin AdminOutlineChrome on RasterTimelineLayer { const SizedBox(height: AppSpacing.sm), Divider( height: 1, - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outlineVariant + .withValues(alpha: 0.5), ), const SizedBox(height: AppSpacing.sm), SymbolLegend(items: overlays), diff --git a/lib/features/map/presentation/layers/lightning_layer.dart b/lib/features/map/presentation/layers/lightning_layer.dart index 24bdf9c70..0eac2bffa 100644 --- a/lib/features/map/presentation/layers/lightning_layer.dart +++ b/lib/features/map/presentation/layers/lightning_layer.dart @@ -132,6 +132,11 @@ class LightningMapLayer with MapLayerDefaults implements MapLayer { MapFrame frame, { bool scrubbing = false, }) async { + // Same frame already on screen — a scrub settle re-shows the same frame. + // The cache check matters: a failed fetch leaves [_shownFrameId] set (with + // an empty payload on screen), and the data may land in the cache later — + // that frame must still be (re)shown. + if (_shownFrameId == frame.id && _cache.containsKey(frame.id)) return; await _ensureImages(controller); await _ensureSource(controller); diff --git a/lib/features/map/presentation/layers/mesh_node_layer.dart b/lib/features/map/presentation/layers/mesh_node_layer.dart new file mode 100644 index 000000000..eba492e43 --- /dev/null +++ b/lib/features/map/presentation/layers/mesh_node_layer.dart @@ -0,0 +1,519 @@ +/// The Meshtastic node map layer — every mesh node that has reported a +/// position, coloured by whether it has been heard recently. +/// +/// A sheet layer, not a timeline one: nodes have no frames, they have a +/// current state. It draws straight from [MeshNodeStore], so it works with no +/// radio attached — the last known mesh is exactly what you want to see when +/// you are trying to reach one. +library; + +import 'dart:async'; +import 'dart:ui'; + +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/features/map/presentation/widgets/mesh_node_sheet.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/map/map_terrain_toggle.dart'; +import 'package:dpip/shared/map/map_town_labels.dart'; +import 'package:dpip/shared/widgets/map_chip_button.dart'; +import 'package:dpip/shared/widgets/map_color_legend.dart'; +import 'package:dpip/shared/widgets/map_menu_toggle_row.dart'; +import 'package:dpip/shared/map/map_layer.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:dpip/shared/map/map_station_labels.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +class MeshNodeMapLayer with MapLayerDefaults implements MapLayer { + MeshNodeMapLayer(this._store); + + final MeshNodeStore _store; + + static const String _sourceId = 'mesh-node-src'; + static const String _circleId = 'mesh-node-circle'; + static const String _labelId = 'mesh-node-label'; + + /// Zoom at which a node's readings join its name on the map. Below this the + /// dots are a distribution; above it the user is looking at individuals. + static const double _detailZoom = 12; + + /// Tap tolerance, in the same logical pixels a touch target is measured in. + /// + /// A fixed degree radius is only ever right at one zoom: 0.05° is five + /// kilometres, which is most of the screen in a street view and a couple of + /// pixels when the whole island is showing — so zoomed out, the dots became + /// almost unhittable. Screen distance is what the finger actually works in, + /// and 44 px is the standard minimum touch target. + static const double _tapRadiusPx = 44; + + /// Heard within [MeshNodeStore.onlineWindow] — a node that is part of the + /// mesh right now. + static const String _onlineColor = '#4CAF50'; + + /// Known, but silent for a while. Kept on the map rather than hidden: a + /// repeater that was there yesterday is still where you would go looking. + static const String _offlineColor = '#9E9E9E'; + + /// Only ever heard through an MQTT bridge. A different hue, not a shade of + /// the same one: it is a different *kind* of thing — an internet report of a + /// node, not a radio contact — so it must not read as "a slightly less + /// online node". + static const String _mqttColor = '#7E57C2'; + static const String _strokeColor = '#FFFFFF'; + + /// Ring around the tapped node — the map has to answer "which one did I + /// tap?" without the user having to compare the sheet to the dots. + static const String _selectedColor = '#1E88E5'; + + MapLibreMapController? _controller; + bool _added = false; + bool _listening = false; + + /// The tapped node, or null. A [ValueNotifier] because the sheet is a widget + /// and this class is not — the scaffold rebuilds it from this. + final ValueNotifier _selected = ValueNotifier(null); + + /// Bumped on every selecting tap, so tapping the same node again re-pops a + /// sheet the user had collapsed — a same-value notifier would not notify. + final ValueNotifier _selectionRevision = ValueNotifier(0); + + @override + String get id => 'meshtastic'; + + @override + IconData get icon => Icons.hub_outlined; + + @override + String label(BuildContext context) => + AppLocalizations.of(context).mapLayerMeshtastic; + + @override + String? subtitle(BuildContext context) => + AppLocalizations.of(context).mapLayerMeshtasticSubtitle; + + @override + bool get usesTimeline => false; + + @override + Future render(MapLibreMapController controller) async { + _controller = controller; + await _removeFromMap(controller); + await controller.addSource( + _sourceId, + GeojsonSourceProperties(data: _geoJson()), + ); + await controller.addCircleLayer( + _sourceId, + _circleId, + _circleProps(), + // **false on purpose.** An interactive layer fires `feature#onTap` + // instead of `map#onMapClick`, and nothing in this app listens to + // feature taps — a tap on the dot would go nowhere while a tap on + // empty sea still reached onMapTap. Every other layer is false too. + enableInteraction: false, + ); + // Names only from mid zoom: a dense urban mesh would otherwise be a wall + // of overlapping labels. + await controller.addSymbolLayer( + _sourceId, + _labelId, + _labelProps(), + minzoom: 9, + enableInteraction: false, + ); + _added = true; + if (!_listening) { + _store.addListener(_onNodes); + _listening = true; + } + } + + void _onNodes() => unawaited(_push()); + + @override + Future onMapTap(LatLng latLng, MapLibreMapController controller) async { + if (_store.positioned.isEmpty) return; + final best = await _nodeNear(latLng, controller); + if (best == null) return; + _selected.value = best; + _selectionRevision.value++; + // The selected node draws with a ring, so the map has to be re-pushed. + await _push(); + } + + /// The node under (or near) a tap. + /// + /// Computes screen distances in Dart instead of asking MapLibre to + /// hit-test: a rendered-feature query on a **circle** layer is not reliable + /// for tapping — the engine's point hit test can shrink the reachable area + /// far below the drawn dot (and the two platforms even differ), which is why + /// the old 44 px box still felt unhittable while the disaster map's big + /// symbol icons never did. Projecting the nodes ourselves makes the target + /// exactly the pixel radius it looks like — one batch call for all of them, + /// one distance loop, no query semantics to get wrong. + Future _nodeNear( + LatLng latLng, + MapLibreMapController controller, + ) async { + try { + final tap = await controller.toScreenLocation(latLng); + final nodes = _store.positioned; + final points = await controller.toScreenLocationBatch([ + // [positioned] already filtered out nulls — the list this mirrors. + for (final node in nodes) LatLng(node.latitude!, node.longitude!), + ]); + final reach = _tapRadiusPx * _screenScale; + final reachSquared = reach * reach; + int? best; + var bestSquared = reachSquared; + for (var i = 0; i < points.length; i++) { + final dx = points[i].x.toDouble() - tap.x.toDouble(); + final dy = points[i].y.toDouble() - tap.y.toDouble(); + final distanceSquared = dx * dx + dy * dy; + if (distanceSquared < bestSquared) { + bestSquared = distanceSquared; + best = nodes[i].num; + } + } + return best; + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'mesh node hit test'); + } + return null; + } + + /// Android reports screen coordinates in device pixels, iOS in logical + /// points — so the same tolerance means different things unless it is + /// scaled. + double get _screenScale => defaultTargetPlatform == TargetPlatform.android + ? PlatformDispatcher.instance.views.first.devicePixelRatio + : 1.0; + + @override + Widget buildSheet(BuildContext context) => MeshNodeSheet( + store: _store, + selected: _selected, + selectionRevision: _selectionRevision, + onClose: () { + _selected.value = null; + unawaited(_push()); + }, + ); + + /// The collapsed peek the scaffold frames around — the sheet is always + /// present, just resting at its handle when nothing is selected. + @override + double get bottomChromeFraction => MeshNodeSheet.peekExtent; + + Future _push() async { + final controller = _controller; + if (controller == null || !_added) return; + try { + await controller.setGeoJsonSource(_sourceId, _geoJson()); + } catch (_) { + // The style can reload underneath us; the next render re-adds. + } + } + + Map _geoJson() => { + 'type': 'FeatureCollection', + 'features': [ + for (final node in _store.positioned) + { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [node.longitude, node.latitude], + }, + 'properties': { + 'num': node.num, + 'name': _shortLabel(node), + // Precomposed rather than concatenated in the style: an empty + // detail would otherwise leave a trailing blank line under the + // name. + 'label': _twoLineLabel(node), + 'online': _store.isOnline(node) ? 1 : 0, + 'mqtt': node.viaMqtt ? 1 : 0, + 'selected': node.num == _selected.value ? 1 : 0, + }, + }, + ], + }; + + /// Node names run long (`故宮南院 東側山線 (Heltec V3)`); the map shows a + /// readable stub and the mesh page has the full list. + String _shortLabel(MeshNode node) { + final name = node.displayName.trim(); + if (name.isEmpty) return '0x${node.num.toRadixString(16)}'; + return name.characters.length <= 12 ? name : '${name.characters.take(12)}…'; + } + + /// Name over its readings, for the close-in zooms. + /// + /// Deliberately **no "heard N minutes ago"** here. This string is baked into + /// the map source and only rewritten when the store changes — so for a node + /// that has gone quiet, which is exactly the node whose age matters, the + /// label would freeze at whatever it said when the node last reported and + /// then keep claiming it. A frozen age is worse than none: the dot's colour + /// already separates heard-recently from silent, and the sheet carries a + /// live "last heard" because it rebuilds. + // l10n-ignore: numeric readouts + String _twoLineLabel(MeshNode node) { + final name = _shortLabel(node); + final parts = [ + if (node.batteryLevel != null) + node.batteryLevel! > 100 ? 'DC' : '${node.batteryLevel}%', + if (node.snr != 0) 'SNR ${node.snr.toStringAsFixed(1)}', + ]; + return parts.isEmpty ? name : '$name\n${parts.join(' · ')}'; + } + + CircleLayerProperties _circleProps() => CircleLayerProperties( + // Grown from 3–7: a node is a thing to tap, and the dot has to look like + // the target the hit test actually allows. + circleRadius: [ + Expressions.interpolate, + ['linear'], + [Expressions.zoom], + 6, + 4.5, + 12, + 9.0, + ], + circleColor: [ + Expressions.caseExpression, + [ + Expressions.equal, + [Expressions.get, 'mqtt'], + 1, + ], + _mqttColor, + [ + Expressions.equal, + [Expressions.get, 'online'], + 1, + ], + _onlineColor, + _offlineColor, + ], + circleStrokeWidth: [ + Expressions.caseExpression, + [ + Expressions.equal, + [Expressions.get, 'selected'], + 1, + ], + 3.0, + 1.0, + ], + circleStrokeColor: [ + Expressions.caseExpression, + [ + Expressions.equal, + [Expressions.get, 'selected'], + 1, + ], + _selectedColor, + _strokeColor, + ], + circleOpacity: [ + Expressions.caseExpression, + [ + Expressions.equal, + [Expressions.get, 'online'], + 1, + ], + 1.0, + 0.55, + ], + ); + + /// **One** symbol layer, not two. + /// + /// `stationLabelProps` is built for a single two-line label — it owns the + /// offset under the dot and the CJK-aware line height. Two stacked layers + /// would each claim that same offset (drawing the second line on top of the + /// first) and then collide-avoid *each other*, so a node would keep losing + /// one of its own lines. A `step` on zoom switches between the one-line and + /// two-line strings instead: the readings appear only once the map is close + /// enough to attribute them to a specific node. + SymbolLayerProperties _labelProps() => stationLabelProps( + textField: [ + 'step', + [Expressions.zoom], + [Expressions.get, 'name'], + _detailZoom, + [Expressions.get, 'label'], + ], + opacity: 1, + ); + + @override + Future clear(MapLibreMapController controller) async { + if (_listening) { + _store.removeListener(_onNodes); + _listening = false; + } + _selected.value = null; + await _removeFromMap(controller); + _controller = null; + } + + Future _removeFromMap(MapLibreMapController controller) async { + if (!_added) return; + _added = false; + for (final layerId in [_labelId, _circleId]) { + try { + await controller.removeLayer(layerId); + } catch (_) { + // Not present — the style may have been rebuilt under us. + } + } + try { + await controller.removeSource(_sourceId); + } catch (_) { + // Same. + } + } + + /// The key for the two dot colours. + /// + /// Without it the map shows green and grey dots and never says which is + /// which — the one thing a reader cannot work out by looking. + @override + Widget buildLegend(BuildContext context) { + final l10n = AppLocalizations.of(context); + // `MapLegendCard` is the surface every other layer's legend sits on — + // without it the swatches float straight on the map and the labels fight + // whatever tiles happen to be underneath. + return MapLegendCard( + child: ListenableBuilder( + listenable: _store, + builder: (context, _) => SymbolLegend( + items: [ + SymbolLegendItem( + swatch: const LegendDot(color: Color(0xFF4CAF50)), + label: l10n.meshtasticOnline, + ), + SymbolLegendItem( + swatch: const LegendDot(color: Color(0xFF9E9E9E)), + label: l10n.meshtasticSilent, + ), + // Only keyed when such nodes can actually be on the map: a legend + // row for something the filter is hiding is noise. + if (!_store.excludeMqtt) + SymbolLegendItem( + swatch: const LegendDot(color: Color(0xFF7E57C2)), + label: l10n.meshtasticViaMqtt, + ), + ], + ), + ), + ); + } + + /// Layer-specific chrome beside the layer switcher: the MQTT filter, plus + /// the shared base-map toggles so the user has one menu rather than three. + @override + Widget buildTopTrailingChrome( + BuildContext context, { + required ValueListenable showTownLabels, + required ValueChanged onShowTownLabelsChanged, + required ValueListenable showTerrain, + required ValueChanged onShowTerrainChanged, + required Future Function() onReloadActive, + }) => _MeshNodeMenu( + store: _store, + onExcludeMqttChanged: (exclude) async { + await _store.setExcludeMqtt(exclude: exclude); + await _push(); + }, + showTownLabels: showTownLabels, + onShowTownLabelsChanged: onShowTownLabelsChanged, + showTerrain: showTerrain, + onShowTerrainChanged: onShowTerrainChanged, + ); + + @override + void onStyleReset() { + _added = false; + _controller = null; + } +} + +/// The mesh layer's overlay menu. +class _MeshNodeMenu extends StatelessWidget { + const _MeshNodeMenu({ + required this.store, + required this.onExcludeMqttChanged, + required this.showTownLabels, + required this.onShowTownLabelsChanged, + required this.showTerrain, + required this.onShowTerrainChanged, + }); + + final MeshNodeStore store; + final ValueChanged onExcludeMqttChanged; + final ValueListenable showTownLabels; + final ValueChanged onShowTownLabelsChanged; + final ValueListenable showTerrain; + final ValueChanged onShowTerrainChanged; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return ListenableBuilder( + listenable: Listenable.merge([store, showTownLabels, showTerrain]), + builder: (context, _) { + final excludeMqtt = store.excludeMqtt; + final hidden = store.hiddenMqttCount; + // The chip marks a *deviation from the defaults*, and hiding MQTT is + // the default here — so only the opposite lights it up. + final active = + !excludeMqtt || !showTownLabels.value || !showTerrain.value; + return MenuAnchor( + alignmentOffset: const Offset(0, 4), + style: MapChipButton.menuStyle(context), + builder: (context, controller, _) => MapChipButton( + icon: Icons.tune, + tooltip: l10n.meshtasticLayerOptions, + active: active, + onTap: controller.isOpen ? controller.close : controller.open, + ), + menuChildren: [ + MapMenuScrollView( + children: [ + SectionHeader(l10n.meshtasticNodes), + MapMenuToggleRow( + selected: excludeMqtt, + icon: Icons.cloud_off_outlined, + title: l10n.meshtasticExcludeMqtt, + subtitle: excludeMqtt && hidden > 0 + ? l10n.meshtasticExcludeMqttHidden(hidden) + : l10n.meshtasticExcludeMqttSubtitle, + tooltip: l10n.meshtasticExcludeMqttSubtitle, + onTap: () => onExcludeMqttChanged(!excludeMqtt), + ), + const MapMenuDivider(), + // The shared base-map rows, not copies of them: this menu + // replaces the standalone base-map chip, so the toggles have + // to be the same ones the user finds on every other layer. + MapTownLabelsRow( + showTownLabels: showTownLabels, + onShowTownLabelsChanged: onShowTownLabelsChanged, + ), + MapTerrainRow( + showTerrain: showTerrain, + onShowTerrainChanged: onShowTerrainChanged, + ), + ], + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/map/presentation/layers/radar_scan_range.dart b/lib/features/map/presentation/layers/radar_scan_range.dart index 508d0d1ab..62639145f 100644 --- a/lib/features/map/presentation/layers/radar_scan_range.dart +++ b/lib/features/map/presentation/layers/radar_scan_range.dart @@ -118,6 +118,10 @@ abstract final class RadarScanRange { /// /// [step] is the latitude sampling interval; it defaults to the grid's own /// resolution, which puts a vertex on every row the data actually has. + /// Pure constant function of nothing — memoised (lazily) so re-adding the + /// layer never re-runs the ~880-row trig sweep. + static final List> _cachedRing = ring(); + static List> ring({double step = gridResolution}) { final n = ((north - south) / step).round(); final left = >[]; @@ -170,7 +174,7 @@ abstract final class RadarScanRange { }, 'geometry': { 'type': 'Polygon', - 'coordinates': [ring()], + 'coordinates': [_cachedRing], }, }, ], diff --git a/lib/features/map/presentation/layers/rts_layer.dart b/lib/features/map/presentation/layers/rts_layer.dart index 00846d449..c67096d77 100644 --- a/lib/features/map/presentation/layers/rts_layer.dart +++ b/lib/features/map/presentation/layers/rts_layer.dart @@ -125,6 +125,11 @@ class RtsMapLayer with MapLayerDefaults implements MapLayer { SeismicTravelTimeTable? _travelTime; Timer? _eewTicker; + /// Identity of the last payload pushed to the source: `null` when offline + /// (empty collection was sent), else the feed's data object — skips the + /// per-tick round trip when a status change re-notifies without new data. + Object? _lastSent; + static const String _sourceId = 'rts-src'; static const String _circleId = 'rts-circle'; static const String _labelId = 'rts-label'; @@ -245,11 +250,18 @@ class RtsMapLayer with MapLayerDefaults implements MapLayer { // Never present aged shaking as current: hide the dots when the feed is // offline, and dim them while stale (the monitor panel flags the status too). final offline = status == RealtimeStatus.offline; + // A status-only change (stale→live etc.) re-notifies without new data — + // don't re-send the same payload, just re-apply the opacity below. + final data = _feed.state.data; + final payloadKey = offline ? null : data; try { - await controller.setGeoJsonSource( - _sourceId, - offline ? _emptyCollection : _geoJson(), - ); + if (!identical(payloadKey, _lastSent)) { + _lastSent = payloadKey; + await controller.setGeoJsonSource( + _sourceId, + offline ? _emptyCollection : _geoJson(), + ); + } if (status != _appliedStatus) { _appliedStatus = status; final opacity = status == RealtimeStatus.live diff --git a/lib/features/map/presentation/layers/satellite_layer.dart b/lib/features/map/presentation/layers/satellite_layer.dart index 034b166c8..f932674fc 100644 --- a/lib/features/map/presentation/layers/satellite_layer.dart +++ b/lib/features/map/presentation/layers/satellite_layer.dart @@ -35,11 +35,11 @@ class SatelliteMapLayer extends RasterTimelineLayer { SatelliteStyle.gray, ); - /// Whether 國界 (world country borders) are redrawn above the imagery. Off - /// by default: on a Taiwan-focused surface the neighbours' outlines are - /// usually noise, and a reader who wants them can ask — the same default as - /// the radar / wind frame ([AdminOutlineChrome.showGlobalOutline]). - final ValueNotifier showGlobalOutline = ValueNotifier(false); + /// Whether 國界 (world country borders) are redrawn above the imagery. On by + /// default — the same default as the radar / wind frame + /// ([AdminOutlineChrome.showGlobalOutline]): the neighbours' outlines are + /// the frame a reader navigates by on a basin-wide view. + final ValueNotifier showGlobalOutline = ValueNotifier(true); bool _globalShown = false; @@ -163,7 +163,7 @@ class SatelliteMapLayer extends RasterTimelineLayer { try { // County frame in bright yellow, township mesh in dark yellow — the hue // set that stays legible on imagery which is overall dark. The 國界 - // border is the [showGlobalOutline] toggle (off by default), so only the + // border is the [showGlobalOutline] toggle (on by default), so only the // two admin frames are drawn unconditionally. await controller.addLineLayer( 'exptech', diff --git a/lib/features/map/presentation/layers/scan_range_overlay_chrome.dart b/lib/features/map/presentation/layers/scan_range_overlay_chrome.dart index 73e5ef8be..ee485d7a7 100644 --- a/lib/features/map/presentation/layers/scan_range_overlay_chrome.dart +++ b/lib/features/map/presentation/layers/scan_range_overlay_chrome.dart @@ -149,9 +149,8 @@ mixin ScanRangeOverlayChrome on AdminOutlineChrome { const SizedBox(height: AppSpacing.sm), Divider( height: 1, - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outlineVariant + .withValues(alpha: 0.5), ), const SizedBox(height: AppSpacing.sm), SymbolLegend(items: overlays), diff --git a/lib/features/map/presentation/layers/typhoon_layer.dart b/lib/features/map/presentation/layers/typhoon_layer.dart index bcf72e5a6..01a9eb1e9 100644 --- a/lib/features/map/presentation/layers/typhoon_layer.dart +++ b/lib/features/map/presentation/layers/typhoon_layer.dart @@ -879,18 +879,14 @@ class TyphoonMapLayer with MapLayerDefaults implements MapLayer { if (wantBoundaries.contains(boundary) == shown) continue; if (shown) { _boundariesShown.remove(boundary); - await AdminOutline.remove(controller, boundary); + await _removeBoundaryFrame(controller, boundary); } else { _boundariesShown.add(boundary); - await AdminOutline.add( + await _addBoundaryFrame( controller, boundary, - lineColor: isRadar - ? AdminOutline.lineColor - : boundary == AdminBoundary.town - ? satelliteTownOutlineColor - : satelliteOutlineColor, - belowLayerId: below, + isRadar: isRadar, + below: below, ); } } @@ -908,6 +904,60 @@ class TyphoonMapLayer with MapLayerDefaults implements MapLayer { } } + /// Adds [boundary]'s frame in the look the current underlay wants. + /// + /// The satellite underlay draws the county frame exactly like the standalone + /// B13 layer does — a bare bright-yellow line. The shared cased stroke reads + /// as a *black* border over opaque IR: the dark casing dominates the thin + /// yellow core, which is the look the user did not ask for. + Future _addBoundaryFrame( + MapLibreMapController controller, + AdminBoundary boundary, { + required bool isRadar, + required String below, + }) async { + if (!isRadar && boundary == AdminBoundary.county) { + await controller.addLineLayer( + AdminOutline.sourceId, + satelliteCountyOutlineLayerId, + LineLayerProperties(lineColor: satelliteOutlineColor, lineWidth: 1.0), + sourceLayer: 'city', + belowLayerId: below, + enableInteraction: false, + ); + return; + } + await AdminOutline.add( + controller, + boundary, + lineColor: isRadar + ? AdminOutline.lineColor + : boundary == AdminBoundary.town + ? satelliteTownOutlineColor + : satelliteOutlineColor, + belowLayerId: below, + ); + } + + /// Removes [boundary]'s frame, undoing whichever look [_addBoundaryFrame] + /// drew — the satellite county line lives under its own layer id. + /// + /// Both looks are removed unconditionally: [setWeatherOverlay] flips + /// `weatherOverlay` *before* the queued sync runs, so a teardown cannot know + /// which look a boundary was drawn with — guessing leaves one of them behind + /// over the other underlay. + Future _removeBoundaryFrame( + MapLibreMapController controller, + AdminBoundary boundary, + ) async { + if (boundary == AdminBoundary.county) { + try { + await controller.removeLayer(satelliteCountyOutlineLayerId); + } catch (_) {} + } + await AdminOutline.remove(controller, boundary); + } + Future _syncWeatherOverlay(MapLibreMapController controller) async { await _removeWeatherRaster(controller); final kind = weatherOverlay.value; @@ -994,7 +1044,7 @@ class TyphoonMapLayer with MapLayerDefaults implements MapLayer { } for (final boundary in _boundariesShown.toList()) { _boundariesShown.remove(boundary); - await AdminOutline.remove(controller, boundary); + await _removeBoundaryFrame(controller, boundary); } try { await controller.removeLayer(_wxLyr); @@ -1017,13 +1067,16 @@ class TyphoonMapLayer with MapLayerDefaults implements MapLayer { _queue(() async { final showProb = showProbability.value; final showL7 = stormBand.value == TyphoonStormBand.level7; - await _setLayerVisibility(controller, _probLyr, showProb); - await _setLayerVisibility(controller, _coneLyr, !showProb); - await _setLayerVisibility(controller, _warnLyr, showWarningAreas.value); - await _setLayerVisibility(controller, _c15Lyr, showL7); - await _setLayerVisibility(controller, _avg15Lyr, showL7); - await _setLayerVisibility(controller, _c25Lyr, !showL7); - await _setLayerVisibility(controller, _avg25Lyr, !showL7); + // Each call swallows its own failure — parallelise the round trips. + await Future.wait([ + _setLayerVisibility(controller, _probLyr, showProb), + _setLayerVisibility(controller, _coneLyr, !showProb), + _setLayerVisibility(controller, _warnLyr, showWarningAreas.value), + _setLayerVisibility(controller, _c15Lyr, showL7), + _setLayerVisibility(controller, _avg15Lyr, showL7), + _setLayerVisibility(controller, _c25Lyr, !showL7), + _setLayerVisibility(controller, _avg25Lyr, !showL7), + ]); }); } diff --git a/lib/features/map/presentation/layers/wind_particle_sim.dart b/lib/features/map/presentation/layers/wind_particle_sim.dart index e407b7495..f51594002 100644 --- a/lib/features/map/presentation/layers/wind_particle_sim.dart +++ b/lib/features/map/presentation/layers/wind_particle_sim.dart @@ -234,6 +234,11 @@ double _wrapWorld(double dx, double world) => /// its latitude span) so advection is projection-free. Nothing remembers where /// the particle has been — the streak behind it is the trail buffer's business, /// not the particle's. +/// +/// Screen position is two plain doubles plus a flag instead of a nullable +/// [Offset]: the simulation allocates nothing per particle per frame, and the +/// stamp pass has the coordinates it needs without unwrapping an object the +/// GC would have to collect. class WindParticle { WindParticle(this.x, this.y); @@ -244,9 +249,13 @@ class WindParticle { /// is stamped, so faster air reads as a brighter streak. double speed = 0; - /// Where the particle is on screen, or null when it is off the field or out - /// of view and so has nothing to stamp this frame. - Offset? screen; + /// Screen x/y of the last step, valid only when [visible]. + double sx = 0; + double sy = 0; + + /// Whether the last step landed the particle inside the viewport, where it + /// has something to stamp. + bool visible = false; } /// The animation state — a population advected through a [WindField]. @@ -256,12 +265,31 @@ class WindParticleSim { /// at z3 down to 1024 at z7 and a fixed number matches neither end. WindParticleSim(this.field, {int count = 6400, math.Random? random}) : _random = random ?? math.Random(), + _mercY = _buildMercY(field), + _secLat = _buildSecLat(field), particles = [for (var i = 0; i < count; i++) WindParticle(0, 0)]; final WindField field; final math.Random _random; final List particles; + /// Field-space y → mercator-y lookup, so the projection per particle per + /// frame is two array reads instead of a `log` + `tan`. + /// + /// The web's GPU shader evaluates the projection per vertex for free; the + /// CPU port would spend ~6000 transcendentals a frame on the same thing. + /// The LUT is built over this field's own latitude span (y ∈ [0, 1]) with + /// linear interpolation; its error stays under a pixel at any zoom this + /// layer can show. + final Float64List _mercY; + + /// Field-space y → 1/cos(latitude) — the longitude-step correction + /// ([step]'s `u / cos(lat)` term). The latitude axis is the same linear + /// span as [_mercY], so the same table shape and interpolant serve it. + final Float64List _secLat; + + static const int _mercYEntries = 1024; + bool _seeded = false; /// Moves every particle one frame: sample the wind, step, work out where @@ -289,27 +317,37 @@ class WindParticleSim { final sinR = math.sin(r); final halfWidth = size.width / 2; final halfHeight = size.height / 2; - final latScale = field.dLat * field.height; - const degToRad = math.pi / 180; + // `lon0 + x·360` folds into a per-frame constant plus one multiply. + final xOffset = (field.lon0 + 180) / 360 * world; + final mercY = _mercY; + final secLat = _secLat; for (final p in particles) { final (u, v) = _sampleUV(p.x, p.y); p.speed = math.sqrt(u * u + v * v); - final lat = field.lat0 + p.y * latScale; - p.x = (p.x + u / math.cos(lat * degToRad) * fieldStep) % 1.0; + // 1/cos(latitude) comes from a LUT (see [_secLat]) — a `cos` per + // particle per frame was 6400 transcendentals on this same loop. + p.x = (p.x + u * _lutAt(secLat, p.y) * fieldStep) % 1.0; p.y -= v * fieldStep; - final wx = (field.lon0 + p.x * 360 + 180) / 360 * world; - final wy = mercatorY(field.lat0 + p.y * latScale) * world; - final dx = _wrapWorld(wx - cx, world); - final dy = wy - cy; - final screen = Offset( - halfWidth + dx * cosR - dy * sinR, - halfHeight + dx * sinR + dy * cosR, - ); - final onField = p.y >= 0 && p.y <= 1; - final inView = onField && _inView(screen, size); - p.screen = inView ? screen : null; + // Off the grid is nothing to stamp: mark it invisible and recycle + // without paying for a projection nobody will see. + if (p.y < 0 || p.y > 1) { + p.visible = false; + _respawn(p, fieldSpace); + continue; + } + + final dx = _wrapWorld(xOffset + p.x * world - cx, world); + final dy = _lutAt(mercY, p.y) * world - cy; + p.sx = halfWidth + dx * cosR - dy * sinR; + p.sy = halfHeight + dx * sinR + dy * cosR; + final inView = + p.sx >= -0.1 * size.width && + p.sx <= 1.1 * size.width && + p.sy >= -0.1 * size.height && + p.sy <= 1.1 * size.height; + p.visible = inView; // Recycle a particle that has left, and occasionally a healthy one — the // field would otherwise empty out of wherever the density weighting is @@ -338,11 +376,38 @@ class WindParticleSim { } } - bool _inView(Offset screen, Size size) => - screen.dx >= -0.1 * size.width && - screen.dx <= 1.1 * size.width && - screen.dy >= -0.1 * size.height && - screen.dy <= 1.1 * size.height; + static Float64List _buildMercY(WindField field) { + final lut = Float64List(_mercYEntries); + final latScale = field.dLat * field.height; + for (var i = 0; i < _mercYEntries; i++) { + lut[i] = mercatorY(field.lat0 + i / (_mercYEntries - 1) * latScale); + } + return lut; + } + + static Float64List _buildSecLat(WindField field) { + final lut = Float64List(_mercYEntries); + final latScale = field.dLat * field.height; + const degToRad = math.pi / 180; + for (var i = 0; i < _mercYEntries; i++) { + lut[i] = + 1 / + math.cos( + (field.lat0 + i / (_mercYEntries - 1) * latScale) * degToRad, + ); + } + return lut; + } + + /// Linear interpolation into a field-space-y LUT ([_buildMercY] / + /// [_buildSecLat]). Out-of-range y (a particle about to respawn) extends + /// the edge value linearly — the same way the raw trig it replaces behaved. + static double _lutAt(Float64List lut, double y) { + final t = y * (_mercYEntries - 1); + final i = math.min(_mercYEntries - 2, math.max(0, t.floor())); + final f = t - i; + return lut[i] + (lut[i + 1] - lut[i]) * f; + } /// The viewport's rectangle in field space (fractions of the field's /// longitude / latitude span), for seeding and respawning. @@ -412,13 +477,14 @@ class WindParticleSim { // Columns wrap — the grid's last column neighbours its first, and clamping // there flattens the wind along the whole seam. Rows do not: there is no // cell north of the north pole. - final i0 = fx.floor() % field.width; + final fxFloor = fx.floor(); + final i0 = fxFloor % field.width; final i1 = (i0 + 1) % field.width; var j0 = fy.floor(); if (j0 < 0) j0 = 0; if (j0 >= field.height) j0 = field.height - 1; final j1 = j0 + 1 < field.height ? j0 + 1 : j0; - final tx = fx - fx.floorToDouble(); + final tx = fx - fxFloor.toDouble(); final ty = (fy - j0).clamp(0.0, 1.0); final row0 = j0 * field.width; final row1 = j1 * field.width; @@ -446,9 +512,13 @@ class WindParticleSim { final d = plane[row1 + i1]; final top = a + (b - a) * tx; final bottom = c + (d - c) * tx; - return lo + (top + (bottom - top) * ty) / 255 * (hi - lo); + return lo + (top + (bottom - top) * ty) * _unitScale(hi - lo); } + /// Precomputed `(span) / 255` — the per-call division was one per plane per + /// particle per frame. + static double _unitScale(double span) => span / 255; + double _lerp(double a, double b, double t) => a + (b - a) * t; } diff --git a/lib/features/map/presentation/pages/map_page.dart b/lib/features/map/presentation/pages/map_page.dart index 13fdb0082..f8b0a456f 100644 --- a/lib/features/map/presentation/pages/map_page.dart +++ b/lib/features/map/presentation/pages/map_page.dart @@ -10,7 +10,9 @@ import 'package:dpip/features/earthquake/domain/seismic_travel_time.dart'; import 'package:dpip/features/earthquake/domain/trem_station_repository.dart'; import 'package:dpip/features/map/presentation/layers/disaster_map_layer.dart'; import 'package:dpip/features/map/presentation/layers/humidity_layer.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; import 'package:dpip/features/map/presentation/layers/lightning_layer.dart'; +import 'package:dpip/features/map/presentation/layers/mesh_node_layer.dart'; import 'package:dpip/features/map/presentation/layers/pressure_layer.dart'; import 'package:dpip/features/map/presentation/layers/qpesums_layer.dart'; import 'package:dpip/features/map/presentation/layers/radar_layer.dart'; @@ -47,6 +49,10 @@ import 'package:provider/provider.dart'; class MapPage extends StatefulWidget { const MapPage({super.key}); + /// Shell branch index of the map tab — [BaseMap] pauses its native render + /// loop while this tab is hidden. + static const int tabIndex = 2; + @override State createState() => _MapPageState(); } @@ -89,6 +95,7 @@ class _MapPageState extends State { WindMapLayer(context.read()), RainMapLayer(context.read()), DisasterMapLayer(context.read()), + MeshNodeMapLayer(context.read()), ]; @override @@ -98,6 +105,7 @@ class _MapPageState extends State { key: ValueKey(initialId), layers: _layers, initialLayerId: initialId, + tabIndex: MapPage.tabIndex, ); } } diff --git a/lib/features/map/presentation/widgets/dpm_sheet.dart b/lib/features/map/presentation/widgets/dpm_sheet.dart index c8522618f..00441b51d 100644 --- a/lib/features/map/presentation/widgets/dpm_sheet.dart +++ b/lib/features/map/presentation/widgets/dpm_sheet.dart @@ -291,9 +291,8 @@ class _AedBody extends StatelessWidget { callPhoneNumber(phone).then((opened) { if (!opened && context.mounted) { final l10n = AppLocalizations.of(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.mapAppCallFailed))); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(l10n.mapAppCallFailed))); } }); } @@ -553,9 +552,8 @@ class _Divider extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Divider( height: 1, - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outlineVariant + .withValues(alpha: 0.5), ), ); } diff --git a/lib/features/map/presentation/widgets/forecast_overlay_menu.dart b/lib/features/map/presentation/widgets/forecast_overlay_menu.dart index 0662d21dc..000be2612 100644 --- a/lib/features/map/presentation/widgets/forecast_overlay_menu.dart +++ b/lib/features/map/presentation/widgets/forecast_overlay_menu.dart @@ -62,10 +62,10 @@ class ForecastOverlayMenu extends StatelessWidget { builder: (context, controller, _) => MapChipButton( icon: Icons.tune, tooltip: l10n.windForecastOverlayMenuTooltip, - // The dot marks "not the defaults". County and town ship on, 國界 - // and labels off, so it lights up when one has moved. + // The dot marks "not the defaults". County, town, and 國界 ship on, + // labels off, so it lights up when one has moved. active: - showGlobal || + !showGlobal || !showCounty || !showTown || !showLabels || diff --git a/lib/features/map/presentation/widgets/mesh_node_sheet.dart b/lib/features/map/presentation/widgets/mesh_node_sheet.dart new file mode 100644 index 000000000..a2b5d9c25 --- /dev/null +++ b/lib/features/map/presentation/widgets/mesh_node_sheet.dart @@ -0,0 +1,584 @@ +/// The draggable detail sheet for the Meshtastic node layer. +/// +/// Same mechanics as the station sheet (and the typhoon panel before it): +/// bottom-aligned, `expand: false` so the map above stays tappable, and a key +/// remount to pop or collapse it. Sharing that skeleton is the point — a map +/// sheet the user has already learned to drag should not behave differently +/// just because this layer is newer. +/// +/// It is always mounted, resting at its handle when nothing is selected, so +/// there is a permanent affordance saying "there is something to drag here". +library; + +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/empty_view.dart'; +import 'package:dpip/shared/widgets/sheet_extent.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class MeshNodeSheet extends StatefulWidget { + const MeshNodeSheet({ + super.key, + required this.store, + required this.selected, + required this.selectionRevision, + required this.onClose, + }); + + final MeshNodeStore store; + + /// The tapped node's number, or null. + final ValueListenable selected; + + /// Bumped on every selecting tap, so re-tapping the same node re-pops a + /// sheet the user collapsed. + final ValueListenable selectionRevision; + + final VoidCallback onClose; + + /// Collapsed peek height — also what the layer reports as its bottom chrome. + /// + /// Matches the station sheet's, and deliberately not smaller: at 0.12 the + /// tray read as a hairline the user had to discover, rather than a handle + /// that is obviously there to be pulled. + static const double peekExtent = 0.14; + static const double _rest = 0.38; + static const double _expanded = 1; + + @override + State createState() => _MeshNodeSheetState(); +} + +class _MeshNodeSheetState extends State { + /// Live sheet fraction — drives the chrome (grip, flush top) only. + final ValueNotifier _extent = ValueNotifier(MeshNodeSheet.peekExtent); + + int? _seededRevision; + int? _seededNode; + + @override + void dispose() { + _extent.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: widget.selectionRevision, + builder: (context, revision, _) => ValueListenableBuilder( + valueListenable: widget.selected, + builder: (context, nodeNum, _) { + final initial = nodeNum == null + ? MeshNodeSheet.peekExtent + : MeshNodeSheet._rest; + // The key remount is what sets `initialChildSize`; seed the chrome to + // match, because the sheet only notifies its extent after a drag. + if (revision != _seededRevision || nodeNum != _seededNode) { + _seededRevision = revision; + _seededNode = nodeNum; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _extent.value = initial; + }); + } + return ExtentSheetChrome( + extent: _extent, + keyValue: nodeNum == null ? 'peek' : 'sel-$revision', + initial: initial, + min: MeshNodeSheet.peekExtent, + max: MeshNodeSheet._expanded, + snapSizes: const [MeshNodeSheet._rest], + content: (context, scrollController) => _Body( + store: widget.store, + nodeNum: nodeNum, + scrollController: scrollController, + extent: _extent, + onClose: widget.onClose, + ), + ); + }, + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + required this.store, + required this.nodeNum, + required this.scrollController, + required this.extent, + required this.onClose, + }); + + final MeshNodeStore store; + final int? nodeNum; + final ScrollController scrollController; + final ValueNotifier extent; + final VoidCallback onClose; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + // Rebuilt on the store too, so charge and last-heard keep moving while the + // sheet is open — a panel frozen at the moment of the tap is wrong within + // a minute. + return ListenableBuilder( + listenable: store, + builder: (context, _) { + final node = nodeNum == null ? null : store.byNum(nodeNum!); + return ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + SheetGrip(extent: extent), + if (node == null) + Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.xl), + child: EmptyView( + icon: Icons.hub_outlined, + message: l10n.meshtasticTapNode, + ), + ) + else + _NodeDetail( + node: node, + store: store, + online: store.isOnline(node), + onClose: onClose, + ), + ], + ); + }, + ); + } +} + +class _NodeDetail extends StatelessWidget { + const _NodeDetail({ + required this.node, + required this.store, + required this.online, + required this.onClose, + }); + + final MeshNode node; + final MeshNodeStore store; + final bool online; + final VoidCallback onClose; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final colors = theme.colorScheme; + + return Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + 0, + AppSpacing.sm, + AppSpacing.xl, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Identity — the name at title weight, its id beneath at label + // weight. One thing to read first, not five things at once. + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + node.displayName.isEmpty + // l10n-ignore: node id fallback + ? '0x${node.num.toRadixString(16)}' + : node.displayName, + style: theme.textTheme.titleLarge, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: AppSpacing.xs), + Text( + // l10n-ignore: node id in the hex form the mesh uses + '!${node.num.toRadixString(16)}', + style: theme.textTheme.labelMedium?.copyWith( + color: colors.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + IconButton( + onPressed: onClose, + icon: const Icon(Icons.close), + tooltip: l10n.commonClose, + ), + ], + ), + const SizedBox(height: AppSpacing.md), + + // 2. State — chips, because these are categories, not measurements. + Wrap( + spacing: AppSpacing.sm, + runSpacing: AppSpacing.sm, + children: [ + _StatusChip( + icon: online ? Icons.circle : Icons.circle_outlined, + label: online ? l10n.meshtasticOnline : l10n.meshtasticSilent, + tone: online ? colors.primary : colors.outline, + ), + if (node.viaMqtt) + _StatusChip( + icon: Icons.cloud_outlined, + label: l10n.meshtasticViaMqtt, + tone: const Color(0xFF7E57C2), + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + + // 3. Readings — the numbers, given room to be read as numbers. + Row( + children: [ + if (node.batteryLevel != null) + _Metric( + icon: node.batteryLevel! > 100 + ? Icons.power_outlined + : Icons.battery_std_outlined, + label: l10n.meshtasticBattery, + value: node.batteryLevel! > 100 + ? l10n.meshtasticExternalPower + // l10n-ignore: percentage readout + : '${node.batteryLevel}%', + ), + if (node.snr != 0) + _Metric( + icon: Icons.network_check_outlined, + // l10n-ignore: signal-to-noise label + label: 'SNR', + // l10n-ignore: decibel readout + value: '${node.snr.toStringAsFixed(1)} dB', + ), + if (node.lastHeard != null) + _Metric( + icon: Icons.schedule_outlined, + label: l10n.meshtasticLastHeard, + value: _relative(node.lastHeard!), + ), + ], + ), + + // 4. Position last — the least-read line, so it does not compete. + if (node.latitude != null && node.longitude != null) ...[ + const SizedBox(height: AppSpacing.lg), + Row( + children: [ + Icon( + Icons.place_outlined, + size: 16, + color: colors.onSurfaceVariant, + ), + const SizedBox(width: AppSpacing.sm), + Text( + // l10n-ignore: coordinates + '${node.latitude!.toStringAsFixed(4)}, ' + '${node.longitude!.toStringAsFixed(4)}', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + if (store.distanceToMyRadioKm(node) != null) ...[ + const SizedBox(width: AppSpacing.sm), + Icon( + Icons.near_me_outlined, + size: 14, + color: colors.onSurfaceVariant, + ), + const SizedBox(width: AppSpacing.xs), + Text( + '${l10n.meshtasticDistance} ' + // l10n-ignore: metric distance readout + '${_formatKm(store.distanceToMyRadioKm(node)!)}', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ], + ), + ], + + // 5. Trends — the last hour of this node's own telemetry, so SNR + // and battery read as a story rather than a single number. + ..._trends(context), + ], + ), + ); + } + + List _trends(BuildContext context) { + final samples = store.historyOf(node.num); + if (samples.length < 2) return const []; + + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final colors = theme.colorScheme; + final snr = [ + for (final s in samples) + if (s.snr != 0) s.snr, + ]; + final battery = [ + for (final s in samples) + if (s.battery != null && s.battery! <= 100) s.battery!.toDouble(), + ]; + if (snr.length < 2 && battery.length < 2) return const []; + + return [ + const SizedBox(height: AppSpacing.lg), + if (snr.length >= 2) ...[ + _TrendBlock( + icon: Icons.network_check_outlined, + label: l10n.meshtasticSnrTrend, + // l10n-ignore: decibel unit + unit: ' dB', + values: snr, + tone: online ? colors.primary : colors.outline, + ), + const SizedBox(height: AppSpacing.md), + ], + if (battery.length >= 2) + _TrendBlock( + icon: Icons.battery_std_outlined, + label: l10n.meshtasticBatteryTrend, + // l10n-ignore: percent unit + unit: '%', + values: battery, + tone: colors.tertiary, + ), + ]; + } + + // l10n-ignore: compact distance readout + String _formatKm(double km) { + if (km < 1) return '${(km * 1000).round()} m'; + if (km < 100) return '${km.toStringAsFixed(1)} km'; + return '${km.round()} km'; + } + + // l10n-ignore: compact age readout + String _relative(DateTime at) { + final age = DateTime.now().difference(at); + if (age.inMinutes < 1) return 'now'; + if (age.inMinutes < 60) return '${age.inMinutes}m'; + if (age.inHours < 24) return '${age.inHours}h'; + return '${age.inDays}d'; + } +} + +/// A category, not a number — so it wears a chip rather than a value slot. +class _StatusChip extends StatelessWidget { + const _StatusChip({ + required this.icon, + required this.label, + required this.tone, + }); + + final IconData icon; + final String label; + final Color tone; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: AppRadius.small, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: tone), + const SizedBox(width: AppSpacing.sm), + Text(label, style: theme.textTheme.labelMedium), + ], + ), + ); + } +} + +/// One reading: the value large, its name small underneath — the number is +/// what the user came for, the label only says which number it is. +class _Metric extends StatelessWidget { + const _Metric({required this.icon, required this.label, required this.value}); + + final IconData icon; + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 16, color: colors.onSurfaceVariant), + const SizedBox(height: AppSpacing.xs), + Text( + value, + style: theme.textTheme.titleMedium?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: colors.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} + +/// A labelled sparkline: the trend's name and current value on top, the line +/// itself below. Deliberately not a fl_chart — a 2-line plot needs no axes, +/// no tooltips, and no dependency draw, just a painter. +class _TrendBlock extends StatelessWidget { + const _TrendBlock({ + required this.icon, + required this.label, + required this.unit, + required this.values, + required this.tone, + }); + + final IconData icon; + final String label; + final String unit; + final List values; + final Color tone; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 14, color: colors.onSurfaceVariant), + const SizedBox(width: AppSpacing.xs), + Text(label, style: theme.textTheme.labelMedium), + const Spacer(), + Text( + // l10n-ignore: current-value readout + '${values.last.toStringAsFixed(values.last.abs() < 100 ? 1 : 0)}' + '$unit', + style: theme.textTheme.labelMedium?.copyWith( + color: tone, + fontWeight: FontWeight.w600, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + SizedBox( + height: 44, + width: double.infinity, + child: CustomPaint( + painter: _SparklinePainter(values: values, tone: tone), + ), + ), + ], + ); + } +} + +/// Draws a thin line through [values] with a soft fill beneath it, scaled so +/// the extremes sit on the block's top and bottom edges — a flat line should +/// still fill the block, or the eye reads it as "nothing happened". +class _SparklinePainter extends CustomPainter { + const _SparklinePainter({required this.values, required this.tone}); + + final List values; + final Color tone; + + @override + void paint(Canvas canvas, Size size) { + var lo = values.first, hi = values.first; + for (final v in values) { + if (v < lo) lo = v; + if (v > hi) hi = v; + } + if (hi - lo < 1e-9) { + // A perfectly flat series needs a band to breathe in, or the line is + // invisible against the fill. + lo -= 1; + hi += 1; + } + final span = hi - lo; + + Offset point(int i) { + final x = size.width * i / (values.length - 1); + final y = size.height - size.height * (values[i] - lo) / span; + return Offset(x, y); + } + + final path = Path()..moveTo(point(0).dx, point(0).dy); + for (var i = 1; i < values.length; i++) { + path.lineTo(point(i).dx, point(i).dy); + } + + final fill = Path.from(path) + ..lineTo(size.width, size.height) + ..lineTo(0, size.height) + ..close(); + canvas.drawPath( + fill, + Paint() + ..shader = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [tone.withValues(alpha: 0.22), tone.withValues(alpha: 0.02)], + ).createShader(Offset.zero & size), + ); + + final stroke = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..strokeJoin = StrokeJoin.round + ..strokeCap = StrokeCap.round + ..color = tone; + canvas.drawPath(path, stroke); + + final last = point(values.length - 1); + canvas.drawCircle(last, 3, Paint()..color = tone); + } + + @override + bool shouldRepaint(_SparklinePainter oldDelegate) => + oldDelegate.values != values || oldDelegate.tone != tone; +} diff --git a/lib/features/map/presentation/widgets/satellite_legend.dart b/lib/features/map/presentation/widgets/satellite_legend.dart index c6d41914b..8e25771fc 100644 --- a/lib/features/map/presentation/widgets/satellite_legend.dart +++ b/lib/features/map/presentation/widgets/satellite_legend.dart @@ -59,9 +59,8 @@ class SatelliteLegend extends StatelessWidget { const SizedBox(height: AppSpacing.sm), Divider( height: 1, - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outlineVariant + .withValues(alpha: 0.5), ), const SizedBox(height: AppSpacing.sm), _boundaries(context), diff --git a/lib/features/map/presentation/widgets/satellite_style_menu.dart b/lib/features/map/presentation/widgets/satellite_style_menu.dart index 224c160c2..3a2b403b1 100644 --- a/lib/features/map/presentation/widgets/satellite_style_menu.dart +++ b/lib/features/map/presentation/widgets/satellite_style_menu.dart @@ -55,7 +55,7 @@ class SatelliteStyleMenu extends StatelessWidget { final showRelief = showTerrain.value; final active = style != SatelliteStyle.gray || - showGlobal || + !showGlobal || !showLabels || !showRelief; return MenuAnchor( @@ -179,9 +179,9 @@ class SatelliteReferenceMenu extends StatelessWidget { builder: (context, controller, _) => MapChipButton( icon: Icons.tune, tooltip: l10n.mapOverlaySectionReference, - // The dot marks "not the defaults". 國界 ships off and labels on, - // so it lights up when either has moved. - active: showGlobal || !showLabels || !showRelief, + // The dot marks "not the defaults". 國界 and labels ship on, so it + // lights up when either has moved. + active: !showGlobal || !showLabels || !showRelief, onTap: () => controller.isOpen ? controller.close() : controller.open(), ), diff --git a/lib/features/map/presentation/widgets/typhoon_panel.dart b/lib/features/map/presentation/widgets/typhoon_panel.dart index ff6815bdd..948ef7f41 100644 --- a/lib/features/map/presentation/widgets/typhoon_panel.dart +++ b/lib/features/map/presentation/widgets/typhoon_panel.dart @@ -1067,9 +1067,8 @@ class _TappedWaypoint extends StatelessWidget { ), ), IconButton( - tooltip: MaterialLocalizations.of( - context, - ).closeButtonTooltip, + tooltip: MaterialLocalizations.of(context) + .closeButtonTooltip, icon: const Icon(Icons.close, size: 18), onPressed: layer.clearForecastSelection, ), @@ -1177,9 +1176,8 @@ class _WaypointDivider extends StatelessWidget { ), child: Divider( height: 1, - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outlineVariant + .withValues(alpha: 0.5), ), ); } diff --git a/lib/features/map/presentation/widgets/wind_particle_overlay.dart b/lib/features/map/presentation/widgets/wind_particle_overlay.dart index 609dfe66d..00986a3b5 100644 --- a/lib/features/map/presentation/widgets/wind_particle_overlay.dart +++ b/lib/features/map/presentation/widgets/wind_particle_overlay.dart @@ -4,10 +4,16 @@ library; import 'dart:math' as math; +import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/platform/device_info.dart'; +import 'package:dpip/core/platform/render_tier.dart'; import 'package:dpip/features/map/presentation/layers/wind_forecast_layer.dart'; import 'package:dpip/features/map/presentation/layers/wind_particle_sim.dart'; +import 'package:dpip/features/map/presentation/pages/map_page.dart'; +import 'package:dpip/shared/navigation/refresh_on_appear.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -24,8 +30,10 @@ import 'package:flutter/scheduler.dart'; /// track pan/zoom/rotate exactly; the trail buffer is screen-space, so it is /// dropped on any camera change — a streak drawn for one view is wrong for the /// next, and the web renderer's alternative is to smear the old one across the -/// pan. The ticker only runs while a wind field is loaded, so an empty layer -/// costs nothing. +/// pan. The ticker only runs while a wind field is loaded *and* the map tab is +/// the visible one — the shell keeps hidden tabs mounted, so without the +/// visibility check a wind field would keep animating (and rasterising full +/// screen) behind every other tab. class WindParticleOverlay extends StatefulWidget { const WindParticleOverlay({super.key, required this.layer}); @@ -47,6 +55,13 @@ class _WindParticleOverlayState extends State WindCamera? _lastCamera; final _TrailBuffer _trails = _TrailBuffer(); + /// Whether the map tab is the shell's visible one — see [_syncVisibility]. + bool _visible = true; + + /// The shell's visible-tab notifier; `null` outside the shell means the + /// overlay is always visible. + VisibleTab? _visibleTab; + @override void initState() { super.initState(); @@ -54,6 +69,62 @@ class _WindParticleOverlayState extends State // Straight assignment: this runs inside the first build, where asking for // another one throws. _adoptField(); + _probeTier(); + } + + /// Device class decides the trail buffer's internal resolution (see + /// [_TrailBuffer.scale]) — the dominant cost of this overlay is rasterising + /// that buffer, so the low tier cuts it by ~9× while the soft streaks stay + /// visually the same. Fire-and-forget: until the probe lands the high + /// setting stands in, and the buffer rebuilds itself at the new size. + Future _probeTier() async { + RenderTier tier; + try { + tier = renderTierFor(await DeviceInfoService.load()); + } catch (error, stackTrace) { + Log.handle(error, stackTrace, 'Device tier probe failed'); + tier = RenderTier.high; + } + if (!mounted) return; + _trails.scale = tier == RenderTier.low ? 0.33 : 0.5; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Subscribes to the notifier itself, not to an InheritedWidget rebuild: + // the shell hands every page the same [VisibleTab] instance, so waiting + // for a scope update would never re-run this (see VisibleTabScope's doc). + final visibleTab = VisibleTabScope.of(context); + if (identical(visibleTab, _visibleTab)) return; + _visibleTab?.removeListener(_syncVisibility); + _visibleTab = visibleTab; + visibleTab?.addListener(_syncVisibility); + _syncVisibility(); + } + + /// Stops the animation when the map tab is hidden, starts it on return. + /// + /// The shell's IndexedStack keeps this overlay mounted behind other tabs; + /// without this the per-frame raster would keep burning the GPU the user is + /// no longer looking at. + void _syncVisibility() { + final visible = + (_visibleTab?.value ?? MapPage.tabIndex) == MapPage.tabIndex; + if (visible == _visible) return; + _visible = visible; + _updateTicker(); + } + + /// Runs the ticker only when there is a field to draw *and* the map tab is + /// on screen. + void _updateTicker() { + final shouldRun = _sim != null && _visible; + if (shouldRun && !_ticker.isActive) { + _ticker.start(); + } else if (!shouldRun && _ticker.isActive) { + _ticker.stop(); + } } /// The layer loaded a new field (frame scrub or first show) — swap the @@ -76,11 +147,7 @@ class _WindParticleOverlayState extends State _sim = field == null ? null : WindParticleSim(field); // A new frame's streaks must not grow out of the previous frame's. _trails.clear(); - if (field == null) { - _ticker.stop(); - } else if (!_ticker.isActive) { - _ticker.start(); - } + _updateTicker(); } void _onTick(Duration _) { @@ -126,6 +193,7 @@ class _WindParticleOverlayState extends State @override void dispose() { + _visibleTab?.removeListener(_syncVisibility); widget.layer.field.removeListener(_onField); _ticker.dispose(); _repaint.dispose(); @@ -146,18 +214,48 @@ class _WindParticleOverlayState extends State /// /// Held by the [State] rather than the painter, because a painter is rebuilt /// whenever the widget is and the buffer has to survive that. +/// +/// The buffer renders at [scale] of the device resolution: every frame it is +/// rasterised with `toImageSync` (a synchronous GPU round-trip on the UI +/// thread), so at full resolution it was the dominant cost — and heat — of +/// this page. Streaks are soft, semi-transparent white, so upscaling the +/// buffer reads the same; [scale] 0.5 cuts that raster 4× and the low tier's +/// 0.33 cuts it ~9×. class _TrailBuffer { ui.Image? _image; - /// Speed buckets for [_stamp], reused across frames — the stamp path runs - /// once a frame, so allocating fresh lists for it (16 × up to 6400 points) - /// is garbage the collector pays for on the hottest path in the app. - final List> _buckets = List.generate( + /// Speed buckets for [_stamp], reused across frames. Each bucket is an + /// interleaved `x,y` [Float32List] for [Canvas.drawRawPoints], so the stamp + /// path allocates nothing per frame — `drawPoints` would need a fresh + /// [Offset] per visible particle (up to 6400 of them) for the collector to + /// chase on the hottest path in the app. + /// + /// Typed-data lists are fixed-length, so capacity is preallocated (the worst + /// case: the whole population in one speed bucket) and [_counts] tracks how + /// much of it this frame is live; [drawRawPoints] gets a zero-copy sublist + /// view of just that. + final List _buckets = List.generate( 16, - (_) => [], + (_) => Float32List(_bucketCapacity), growable: false, ); + /// Live point count per bucket this frame. + /// + /// 16 bits, not 8: the whole population (6400) can land in one speed bucket + /// under strong wind, and an 8-bit counter wraps at 255 — the bucket then + /// draws the wrong point count (or none at all, when the count wraps to 0), + /// which reads as particles vanishing and stale trails outliving a rotation. + final Uint16List _counts = Uint16List(16); + + /// Floats per bucket: the largest population (6400 at z3) as x,y pairs. + static const int _bucketCapacity = 6400 * 2; + + /// Internal render resolution as a fraction of the device's — see the class + /// doc. Written by the state's device-tier probe; changing it recreates the + /// buffer on the next [advance] (the size check below). + double scale = 0.5; + /// Live camera zoom and device pixel ratio, written by the ticker. The /// painter needs both at paint time and neither is known at build time — /// the widget does not rebuild when the map zooms. @@ -174,7 +272,7 @@ class _TrailBuffer { /// Fades what is there, stamps [particles] over it, and returns the result. ui.Image advance(Iterable particles, Size size) { - final dpr = devicePixelRatio; + final dpr = devicePixelRatio * scale; final w = math.max(1, (size.width * dpr).round()); final h = math.max(1, (size.height * dpr).round()); final previous = _image; @@ -190,9 +288,8 @@ class _TrailBuffer { old, Offset.zero, Paint() - ..color = const Color( - 0xFFFFFFFF, - ).withValues(alpha: fadeOpacityFor(zoom)), + ..color = const Color(0xFFFFFFFF) + .withValues(alpha: fadeOpacityFor(zoom)), ); } _stamp(canvas, particles, dpr); @@ -203,7 +300,8 @@ class _TrailBuffer { return next; } - /// Stamps one dot per visible particle, in device pixels. + /// Stamps one dot per visible particle, in buffer pixels ([dpr] already + /// includes the buffer's own [scale]). /// /// The web gives every point its own alpha from a fragment shader; a /// [Canvas] carries one colour per call, so the field is bucketed by speed @@ -213,24 +311,31 @@ class _TrailBuffer { void _stamp(Canvas canvas, Iterable particles, double dpr) { const buckets = 16; final points = _buckets; - for (final bucket in points) { - bucket.clear(); - } + final counts = _counts; + counts.fillRange(0, buckets, 0); for (final p in particles) { - final screen = p.screen; - if (screen == null) continue; + if (!p.visible) continue; final t = (p.speed / kWindSpeedScale).clamp(0.0, 1.0); - points[math.min(buckets - 1, (t * buckets).floor())].add(screen * dpr); + final bi = math.min(buckets - 1, (t * buckets).floor()); + final i = counts[bi]++; + final b = points[bi]; + b[i * 2] = p.sx * dpr; + b[i * 2 + 1] = p.sy * dpr; } final paint = Paint() ..strokeWidth = pointSizeFor(zoom) * dpr ..strokeCap = StrokeCap.round; for (var i = 0; i < buckets; i++) { - if (points[i].isEmpty) continue; + final count = counts[i]; + if (count == 0) continue; // The bucket's midpoint, on the web's ramp: brighter where it is faster. final t = (i + 0.5) / buckets; paint.color = Color.fromRGBO(255, 255, 255, 0.35 + 0.55 * t); - canvas.drawPoints(ui.PointMode.points, points[i], paint); + canvas.drawRawPoints( + ui.PointMode.points, + Float32List.sublistView(points[i], 0, count * 2), + paint, + ); } } } @@ -255,7 +360,9 @@ class _WindParticlePainter extends CustomPainter { image, Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), Offset.zero & size, - Paint()..filterQuality = FilterQuality.none, + // The buffer is scaled down relative to the screen, so bilinear (not + // `none`, which would show the upscale as hard squares on the dots). + Paint()..filterQuality = FilterQuality.low, ); } diff --git a/lib/features/meshtastic/meshtastic_providers.dart b/lib/features/meshtastic/meshtastic_providers.dart new file mode 100644 index 000000000..aadf2beca --- /dev/null +++ b/lib/features/meshtastic/meshtastic_providers.dart @@ -0,0 +1,29 @@ +/// Providers for the LoRa mesh feature — the chat controller behind the mesh +/// page. (The BLE transport itself lives in `core/meshtastic` and is provided +/// by the core providers.) +library; + +import 'package:dpip/core/di/shared_deps.dart'; +import 'package:dpip/features/meshtastic/presentation/mesh_chat_controller.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; + +/// The mesh chat state: nodes and the persisted message log. +/// +/// **Eager** (`lazy: false`), unlike most feature state. `MeshLink` reconnects +/// to a saved radio at startup and the message stream is a broadcast stream: +/// anything arriving with no listener is gone. A log that only starts +/// recording when the user happens to open the page would lose exactly the +/// messages that arrived while they weren't looking. Creating it costs a prefs +/// read and two stream subscriptions — no BLE work of its own. +List meshtasticProviders(SharedDeps deps) => [ + ChangeNotifierProvider( + lazy: false, + create: (_) => MeshChatController( + deps.meshtastic, + deps.meshLink, + deps.meshNodes, + deps.meshStore, + ), + ), +]; diff --git a/lib/features/meshtastic/presentation/mesh_chat_controller.dart b/lib/features/meshtastic/presentation/mesh_chat_controller.dart new file mode 100644 index 000000000..0bb5474a8 --- /dev/null +++ b/lib/features/meshtastic/presentation/mesh_chat_controller.dart @@ -0,0 +1,316 @@ +/// Presentation state for the mesh page: connection, nodes, and the message +/// log. +/// +/// Lives in the provider tree rather than in the page's `State` for two +/// reasons: the radio keeps delivering while the page is closed, and the log is +/// persisted (in SQLite — see [MeshStore]), so it survives navigation *and* an +/// app restart. That persistence is not a nicety — the radio's own replay queue +/// is small, shared with telemetry/position traffic, and emptied once read, so +/// it can never be the app's message history. +/// +/// The in-memory list is a **window** onto that store, not the log itself: the +/// newest [MeshChatController.windowSize] messages, which is what a chat screen +/// can show. Retention lives in the store. +/// +/// The controller owns no BLE: it only listens to [MeshtasticService] and +/// turns its streams into a snapshot the page renders. +library; + +import 'dart:async'; + +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:flutter/foundation.dart'; + +/// One line in the message log — a packet heard from the mesh, or a message +/// this device sent ([outgoing]). +@immutable +class MeshChatMessage { + const MeshChatMessage({ + required this.from, + required this.channel, + required this.text, + required this.timestamp, + this.outgoing = false, + }); + + /// Adapts a stored row. + MeshChatMessage.stored(MeshStoredMessage row) + : from = row.from, + channel = row.channel, + text = row.text, + timestamp = row.timestamp, + outgoing = row.outgoing; + + /// Sender node number (0 for [outgoing] — the local radio's own number is + /// not part of the transport's surface, and the bubble never shows it). + final int from; + + /// Mesh channel index the message travelled on (0 = primary). + final int channel; + + final String text; + final DateTime timestamp; + + /// Whether this device sent it (the radio does not echo our own packets + /// back, so a sent message is recorded locally). + final bool outgoing; + + MeshStoredMessage toStored() => MeshStoredMessage( + from: from, + channel: channel, + text: text, + timestamp: timestamp, + outgoing: outgoing, + ); +} + +class MeshChatController extends ChangeNotifier { + MeshChatController(this._service, this._link, this._nodes, this._store) { + unawaited(_restore()); + // Nodes come from the shared store, which persists them — this page is one + // of two surfaces showing the same table (the map layer is the other). + _nodes.addListener(notifyListeners); + _messageSub = _service.messageStream.listen(_onMessage); + } + + /// How many messages the in-memory window holds. Not a retention limit — + /// the store keeps far more (see [MeshStore.messageRetention]); this is just + /// how much of it a chat screen keeps materialised. + static const int windowSize = 300; + + final MeshtasticService _service; + + /// Connection lifecycle lives in [MeshLink] (app-wide, survives this page); + /// this controller only owns the conversation. + final MeshLink _link; + final MeshNodeStore _nodes; + + /// Null when the database couldn't be opened — the log then lives only for + /// this session rather than the page failing to work at all. + final MeshStore? _store; + + StreamSubscription? _messageSub; + StreamSubscription? _scanSub; + + final List _messages = []; + final Map _counts = {}; + final List _devices = []; + bool _scanning = false; + String? _connectingId; + String? _scanError; + + /// Whether the radio is connected *and* configured — the only state in which + /// sending works. Owned by [MeshLink]; mirrored here so the composer doesn't + /// need both objects. + bool get isConnected => _link.isConnected; + + /// How many stored messages each channel holds — what the channel tabs + /// badge, so a quiet channel with history is distinguishable from an empty + /// one. + /// The last 24 hours of utilization samples, oldest first — what the chart + /// plots. Read on demand rather than held: it is only looked at when the + /// radio panel is open. + Future> metricsHistory() async => + await _store?.metrics() ?? const []; + + /// Counted in SQL at load and kept up to date locally, so a channel whose + /// history falls outside the in-memory window still reports its real total. + Map get messageCountsByChannel => Map.unmodifiable(_counts); + + /// The message log, **newest first** (the page renders it reversed). + List get messages => List.unmodifiable(_messages); + + /// Every node heard so far, online first, then most-recently-heard. + List get nodes => _nodes.nodes; + + /// Whether [node] has been heard recently enough to count as online. + bool isOnline(MeshNode node) => _nodes.isOnline(node); + + /// Radios found by the current/last scan. + List get devices => List.unmodifiable(_devices); + + bool get scanning => _scanning; + + /// Id of the device a connect is in flight for, so its row can show it. + String? get connectingId => _connectingId; + + /// Why the last scan failed (permission denied, adapter off), else null. + String? get scanError => _scanError; + + /// The sender's node name once the mesh has told us about it, else its id. + String senderLabel(int num) { + final name = _nodes.byNum(num)?.displayName; + return (name != null && name.isNotEmpty) + ? name + : '0x${num.toRadixString(16)}'; + } + + /// Starts a scan, replacing the previous results. Safe to call repeatedly — + /// a running scan is cancelled first. + Future startScan() async { + await _scanSub?.cancel(); + _scanSub = null; + _devices.clear(); + _scanError = null; + _scanning = true; + notifyListeners(); + + final completion = Completer(); + _scanSub = _service.scanForDevices().listen( + (device) { + if (_devices.any((d) => d.id == device.id)) return; + _devices.add(device); + notifyListeners(); + }, + onError: (Object error, StackTrace stackTrace) { + Log.handle(error, stackTrace, 'mesh scan'); + // A scan/init failure arrives as a StateError carrying the underlying + // failure's message (which permission was denied, adapter off …). + _scanError = error is StateError && error.message.isNotEmpty + ? error.message + : '$error'; + _scanning = false; + notifyListeners(); + if (!completion.isCompleted) completion.complete(); + }, + onDone: () { + _scanning = false; + notifyListeners(); + if (!completion.isCompleted) completion.complete(); + }, + cancelOnError: true, + ); + return completion.future; + } + + /// Stops a running scan (e.g. the picker was dismissed). + Future stopScan() async { + await _scanSub?.cancel(); + _scanSub = null; + if (!_scanning) return; + _scanning = false; + notifyListeners(); + } + + /// Adopts [device] as the app's radio — [MeshLink] then keeps it attached + /// for the rest of the app's life, page changes included. + /// + /// Returns null on success, [MeshLink.busySentinel] when another app holds + /// the radio (the caller asks the user, then retries with [force]), else a + /// message to show. + Future connect(MeshDevice device, {bool force = false}) async { + _connectingId = device.id; + notifyListeners(); + final failure = await _link.attach(device, force: force); + _connectingId = null; + notifyListeners(); + return failure; + } + + /// Forgets the radio and disconnects — the only thing that stops + /// reconnection short of closing the app. + Future disconnect() async { + await _link.detach(); + return null; + } + + /// Broadcasts [text] on [channel] and records it locally — the radio does + /// not echo our own packets back. Returns null on success. + Future send(String text, {int channel = 0}) async { + final trimmed = text.trim(); + if (trimmed.isEmpty) return null; + final result = await _service.sendText(trimmed, channel: channel); + if (result case Err(:final failure)) return failure.message; + _add( + MeshChatMessage( + from: 0, + channel: channel, + text: trimmed, + timestamp: AppTime.utc.toLocal(), + outgoing: true, + ), + ); + return null; + } + + /// Empties the log, on screen and on disk. + void clearMessages() { + if (_messages.isEmpty) return; + _messages.clear(); + _counts.clear(); + notifyListeners(); + unawaited(_store?.clearMessages()); + } + + void _onMessage(MeshMessage message) { + _add( + MeshChatMessage( + from: message.from, + channel: message.channel, + text: message.text, + timestamp: message.timestamp, + ), + ); + } + + void _add(MeshChatMessage message) { + final store = _store; + if (store == null) { + // No database: fall back to an in-memory log, deduplicated here instead + // of by the store's unique index. + if (_messages.any((m) => _sameMessage(m, message))) return; + _remember(message); + return; + } + unawaited( + store.addMessage(message.toStored()).then((isNew) { + // A reconnect replays packets the log may already hold; the store's + // unique index is what decides, so the UI follows its answer. + if (isNew) _remember(message); + }), + ); + } + + void _remember(MeshChatMessage message) { + _messages.insert(0, message); + if (_messages.length > windowSize) { + _messages.removeRange(windowSize, _messages.length); + } + _counts[message.channel] = (_counts[message.channel] ?? 0) + 1; + notifyListeners(); + } + + bool _sameMessage(MeshChatMessage a, MeshChatMessage b) => + a.from == b.from && + a.channel == b.channel && + a.timestamp == b.timestamp && + a.text == b.text; + + Future _restore() async { + final store = _store; + if (store == null) return; + final rows = await store.messages(limit: windowSize); + _messages + ..clear() + ..addAll([for (final row in rows) MeshChatMessage.stored(row)]); + _counts + ..clear() + ..addAll(await store.messageCountsByChannel()); + Log.debug('mesh chat: loaded ${_messages.length} message(s)'); + notifyListeners(); + } + + @override + void dispose() { + _nodes.removeListener(notifyListeners); + unawaited(_messageSub?.cancel()); + unawaited(_scanSub?.cancel()); + super.dispose(); + } +} diff --git a/lib/features/meshtastic/presentation/pages/meshtastic_page.dart b/lib/features/meshtastic/presentation/pages/meshtastic_page.dart new file mode 100644 index 000000000..012fc0dda --- /dev/null +++ b/lib/features/meshtastic/presentation/pages/meshtastic_page.dart @@ -0,0 +1,1725 @@ +/// The LoRa mesh (Meshtastic) page — a chat surface over the attached radio. +/// +/// Laid out message-first, like any messaging screen: the log fills the page, +/// the composer is pinned to the bottom, and everything else (radio picker, +/// node list) opens in a sheet from the app bar. State lives in +/// [MeshChatController] in the provider tree, so leaving the page neither stops +/// reception nor drops the log. +library; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:dpip/app/theme/app_motion.dart'; +import 'package:dpip/app/theme/app_radius.dart'; +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_alerts.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/platform/screen_wake.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/features/meshtastic/presentation/mesh_chat_controller.dart'; +import 'package:dpip/features/meshtastic/presentation/widgets/mesh_utilization_chart.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/widgets/empty_view.dart'; +import 'package:dpip/shared/widgets/section_header.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class MeshtasticPage extends StatefulWidget { + const MeshtasticPage({super.key}); + + @override + State createState() => _MeshtasticPageState(); +} + +class _MeshtasticPageState extends State { + /// Which channel is being read and written, once the user has picked one. + /// + /// Deliberately **not** persisted and not remembered across visits: DPIP is + /// the channel this app exists for, so every entry starts there and a + /// detour to another channel lasts only as long as the visit. + int? _channel; + + /// Held rather than looked up in [dispose]: by then the element is + /// deactivated and reaching for an ancestor throws. + MeshAlerts? _alerts; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _alerts = context.read(); + } + + @override + void dispose() { + // Nothing on screen any more, so every channel is worth announcing again. + _alerts?.setVisibleChannel(null); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final controller = context.watch(); + final link = context.watch(); + final service = context.read(); + final status = link.status; + + // Channels come from two places, because either can be missing: the radio + // knows the table but only while connected, and the stored log knows which + // channels actually carry history. Their union is what the user can pick + // from — so a disconnected app still offers every conversation it holds + // instead of collapsing them into one list. + final dpipIndex = link.lastKnownDpipChannel; + final channels = _channelOptions( + fromRadio: service.channels, + fromLog: controller.messageCountsByChannel.keys, + ); + // Falls back through: the user's pick → the DPIP channel → the first + // option. Re-derived on every build, so a channel that disappears (a + // different radio, a rewritten table) can't leave the page pointing at a + // slot that no longer exists. + final int selected; + if (channels.any((c) => c.index == _channel)) { + selected = _channel!; + } else if (channels.any((c) => c.index == dpipIndex)) { + selected = dpipIndex!; + } else { + selected = channels.isEmpty ? 0 : channels.first.index; + } + + // A message arriving in the conversation the user is reading is not news; + // anything else still is. + context.read().setVisibleChannel(selected); + + // The screen stays awake for as long as this page is up: a conversation you + // are watching for a reply is exactly the case where the display timing out + // costs you the thing you were waiting for. Released on dispose. + return ScreenWakeScope( + child: Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.meshtasticTitle), + Text( + _statusLabel(l10n, status), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + actions: [ + IconButton( + tooltip: l10n.meshtasticNodes, + onPressed: () => _showNodes(context), + icon: Badge( + isLabelVisible: controller.nodes.isNotEmpty, + label: Text('${controller.nodes.length}'), + child: const Icon(Icons.hub_outlined), + ), + ), + _OverflowMenu( + controller: controller, + link: link, + alerts: context.watch(), + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + _ConnectionBanner(link: link), + _RadioStrip(link: link), + if (channels.length > 1) + _ChannelPicker( + channels: channels, + selected: selected, + dpipIndex: dpipIndex, + counts: controller.messageCountsByChannel, + radio: service.radioInfo, + onSelected: (index) => setState(() => _channel = index), + ), + Expanded( + child: _MessageLog(controller: controller, channel: selected), + ), + _Composer(controller: controller, channel: selected), + ], + ), + ), + ), + ); + } + + /// Every channel worth offering, index-ordered: the radio's enabled slots, + /// plus any channel the stored log holds messages for. + /// + /// A log-only channel is synthesised with no name — all that is known about + /// it is its index, which is exactly what its label falls back to. + List _channelOptions({ + required List fromRadio, + required Iterable fromLog, + }) { + final byIndex = { + for (final channel in fromRadio) + if (channel.enabled) channel.index: channel, + }; + for (final index in fromLog) { + byIndex.putIfAbsent( + index, + () => MeshChannel(index: index, name: '', psk: const [], enabled: true), + ); + } + return byIndex.values.toList()..sort((a, b) => a.index.compareTo(b.index)); + } + + String _statusLabel(AppLocalizations l10n, MeshConnectionStatus status) { + final label = switch (status.state) { + MeshConnectionState.disconnected => l10n.meshtasticStateDisconnected, + MeshConnectionState.connecting => l10n.meshtasticStateConnecting, + MeshConnectionState.configuring => l10n.meshtasticStateConfiguring, + MeshConnectionState.connected => l10n.meshtasticStateConnected, + MeshConnectionState.error => l10n.meshtasticStateError, + }; + final name = status.deviceName; + return name == null || name.isEmpty ? label : '$label · $name'; + } +} + +/// Opens the radio picker, scanning while it is on screen. +Future _showDevices(BuildContext context) async { + final controller = context.read(); + unawaited(controller.startScan()); + await showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (_) => const _DeviceSheet(), + ); + await controller.stopScan(); +} + +Future _showNodes(BuildContext context) => showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (_) => const _NodeSheet(), +); + +Future _showRadio(BuildContext context) => showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (_) => const _RadioSheet(), +); + +void _toast(BuildContext context, String message) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(message))); +} + +/// App-bar overflow: connect/disconnect and log housekeeping. +class _OverflowMenu extends StatelessWidget { + const _OverflowMenu({ + required this.controller, + required this.link, + required this.alerts, + }); + + final MeshChatController controller; + final MeshLink link; + final MeshAlerts alerts; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (action) => action(), + // The item builder's context belongs to the menu route, which is already + // gone by the time `onSelected` runs — every action closes over this + // widget's context instead. + itemBuilder: (_) => [ + PopupMenuItem( + value: () => unawaited(_showDevices(context)), + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.bluetooth_searching_outlined), + title: Text(l10n.meshtasticSelectDevice), + ), + ), + PopupMenuItem( + enabled: link.savedRadioId != null, + value: () async { + final failure = await controller.disconnect(); + if (failure != null && context.mounted) _toast(context, failure); + }, + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.bluetooth_disabled_outlined), + title: Text(l10n.meshtasticDisconnect), + ), + ), + CheckedPopupMenuItem( + checked: alerts.messagesEnabled, + value: () => unawaited( + alerts.setMessagesEnabled(enabled: !alerts.messagesEnabled), + ), + child: Text(l10n.meshtasticNotifyMessages), + ), + CheckedPopupMenuItem( + checked: alerts.nodesEnabled, + value: () => + unawaited(alerts.setNodesEnabled(enabled: !alerts.nodesEnabled)), + child: Text(l10n.meshtasticNotifyNodes), + ), + PopupMenuItem( + enabled: controller.messages.isNotEmpty, + value: controller.clearMessages, + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.delete_sweep_outlined), + title: Text(l10n.meshtasticClearMessages), + ), + ), + ], + ); + } +} + +/// A slim strip above the log carrying whatever the connection needs from the +/// user right now. Nothing is shown once the radio is connected — the app bar +/// already names it. +class _ConnectionBanner extends StatelessWidget { + const _ConnectionBanner({required this.link}); + + final MeshLink link; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final colors = Theme.of(context).colorScheme; + final status = link.status; + if (status.state == MeshConnectionState.connected) { + return const SizedBox.shrink(); + } + + // A pending reconnect is "busy" too: the link is coming back on its own, + // so the user is told to wait rather than offered a scan they don't need. + final busy = + status.state == MeshConnectionState.connecting || + status.state == MeshConnectionState.configuring || + link.reconnecting; + final failed = status.state == MeshConnectionState.error; + final background = failed + ? colors.errorContainer + : busy + ? colors.primaryContainer + : colors.surfaceContainerHighest; + final foreground = failed + ? colors.onErrorContainer + : busy + ? colors.onPrimaryContainer + : colors.onSurfaceVariant; + final message = failed + ? (status.errorMessage ?? l10n.meshtasticStateError) + : busy + ? switch (status.state) { + MeshConnectionState.configuring => l10n.meshtasticStateConfiguring, + MeshConnectionState.connecting => l10n.meshtasticStateConnecting, + _ => l10n.meshtasticReconnecting, + } + : l10n.meshtasticNotConnected; + + return Material( + color: background, + child: Column( + children: [ + if (busy) LinearProgressIndicator(color: colors.primary), + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.sm, + AppSpacing.sm, + AppSpacing.sm, + ), + child: Row( + children: [ + Icon( + failed + ? Icons.error_outline + : busy + ? Icons.bluetooth_searching_outlined + : Icons.bluetooth_disabled_outlined, + size: 18, + color: foreground, + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + message, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: foreground), + ), + ), + if (!busy) + TextButton( + onPressed: () => unawaited(_showDevices(context)), + child: Text(l10n.meshtasticScan), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// What DPIP itself needs from the attached radio: the private channel its +/// The strip under the app bar: proof the link is alive, plus what DPIP needs +/// from the radio. Tapping it opens the full radio panel. +/// +/// A working mesh link is mostly silent — between events nothing on screen +/// separates "connected and listening" from "died ten minutes ago". So the top +/// row is deliberately *live*: a pulse on every received packet, running +/// counters, and how long ago the last packet arrived. +class _RadioStrip extends StatefulWidget { + const _RadioStrip({required this.link}); + + final MeshLink link; + + @override + State<_RadioStrip> createState() => _RadioStripState(); +} + +class _RadioStripState extends State<_RadioStrip> { + Timer? _ticker; + + @override + void initState() { + super.initState(); + _syncTicker(); + } + + @override + void didUpdateWidget(_RadioStrip oldWidget) { + super.didUpdateWidget(oldWidget); + _syncTicker(); + } + + /// Runs the 1 Hz tick **only while there is a link**. + /// + /// The "last packet Ns ago" readout has to age on its own — without a tick it + /// would freeze at whatever it said when the last packet arrived, which is + /// exactly the reassurance-without-evidence this strip exists to avoid. But + /// the strip renders nothing when disconnected, so ticking then would rebuild + /// a hidden widget once a second forever (and never let a widget test settle). + void _syncTicker() { + final needed = widget.link.isConnected; + if (needed == (_ticker != null)) return; + if (needed) { + _ticker = Timer.periodic( + const Duration(seconds: 1), + (_) => setState(() {}), + ); + } else { + _ticker?.cancel(); + _ticker = null; + } + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final link = widget.link; + if (!link.isConnected) return const SizedBox.shrink(); + final service = context.read(); + final theme = Theme.of(context); + final colors = theme.colorScheme; + + return Material( + color: colors.surfaceContainerLow, + child: InkWell( + onTap: () => _showRadio(context), + child: Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.sm, + AppSpacing.md, + AppSpacing.sm, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamBuilder( + initialData: service.traffic, + stream: service.trafficStream, + builder: (context, snapshot) => _VitalsRow( + traffic: snapshot.data ?? const MeshTraffic(), + radio: service.radioInfo, + ), + ), + const SizedBox(height: AppSpacing.xs), + _DpipRow(link: link), + ], + ), + ), + ), + ); + } +} + +/// Heartbeat, packet counters and battery — the always-on vitals. +class _VitalsRow extends StatelessWidget { + const _VitalsRow({required this.traffic, required this.radio}); + + final MeshTraffic traffic; + final MeshRadioInfo? radio; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + final style = theme.textTheme.labelMedium?.copyWith( + color: colors.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ); + final battery = radio?.batteryPercent; + + return Row( + children: [ + _Heartbeat(lastRx: traffic.lastRx), + const SizedBox(width: AppSpacing.sm), + // l10n-ignore: counters and units, not prose + Text('↓ ${traffic.rxPackets}', style: style), + const SizedBox(width: AppSpacing.md), + // l10n-ignore: counters and units, not prose + Text('↑ ${traffic.txPackets}', style: style), + const SizedBox(width: AppSpacing.md), + Text(_sinceLabel(traffic.lastRx), style: style), + const Spacer(), + if (battery != null) ...[ + Icon( + radio!.isPluggedIn ? Icons.power_outlined : _batteryIcon(battery), + size: 16, + color: !radio!.isPluggedIn && battery <= 20 + ? colors.error + : colors.onSurfaceVariant, + ), + const SizedBox(width: AppSpacing.xs), + // l10n-ignore: percentage readout, not prose + Text(radio!.isPluggedIn ? 'DC' : '$battery%', style: style), + const SizedBox(width: AppSpacing.sm), + ], + Icon(Icons.chevron_right, size: 18, color: colors.onSurfaceVariant), + ], + ); + } + + IconData _batteryIcon(int percent) => switch (percent) { + >= 80 => Icons.battery_full_outlined, + >= 50 => Icons.battery_5_bar_outlined, + >= 20 => Icons.battery_3_bar_outlined, + _ => Icons.battery_1_bar_outlined, + }; +} + +/// A dot that pulses once per received packet and fades as the link goes quiet. +/// +/// The colour is the honest part: it is driven by how long ago the last packet +/// arrived, so a link that stopped delivering goes grey by itself instead of +/// sitting there looking connected. +class _Heartbeat extends StatefulWidget { + const _Heartbeat({required this.lastRx}); + + final DateTime? lastRx; + + @override + State<_Heartbeat> createState() => _HeartbeatState(); +} + +class _HeartbeatState extends State<_Heartbeat> + with SingleTickerProviderStateMixin { + late final AnimationController _pulse = AnimationController( + vsync: this, + duration: AppMotion.medium, + ); + + @override + void didUpdateWidget(_Heartbeat oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.lastRx != oldWidget.lastRx) _pulse.forward(from: 0); + } + + @override + void dispose() { + _pulse.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + final age = widget.lastRx == null + ? null + : DateTime.now().difference(widget.lastRx!); + final color = switch (age) { + null => colors.outline, + final a when a < const Duration(minutes: 2) => colors.primary, + final a when a < const Duration(minutes: 15) => colors.tertiary, + _ => colors.outline, + }; + return AnimatedBuilder( + animation: _pulse, + builder: (context, _) { + // One outward ring per packet, over a dot that stays put. + final t = _pulse.value; + return SizedBox.square( + dimension: 16, + child: Stack( + alignment: Alignment.center, + children: [ + if (_pulse.isAnimating) + Container( + width: 6 + 10 * t, + height: 6 + 10 * t, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color.withValues(alpha: (1 - t) * 0.4), + ), + ), + Container( + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ), + ], + ), + ); + }, + ); + } +} + +/// How long ago something happened, compact and unit-suffixed. +// l10n-ignore: numeric age readout used in diagnostics rows +String _sinceLabel(DateTime? at) { + if (at == null) return '—'; + return '${_durationLabel(DateTime.now().difference(at))} ago'; +} + +// l10n-ignore: numeric duration readout used in diagnostics rows +String _durationLabel(Duration d) { + if (d.inSeconds < 60) return '${d.inSeconds}s'; + if (d.inMinutes < 60) return '${d.inMinutes}m'; + if (d.inHours < 24) return '${d.inHours}h ${d.inMinutes % 60}m'; + return '${d.inDays}d ${d.inHours % 24}h'; +} + +/// What DPIP needs from the radio: the private channel its disaster payloads +/// travel on, and the LoRa region that decides who can hear them. +class _DpipRow extends StatelessWidget { + const _DpipRow({required this.link}); + + final MeshLink link; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final colors = theme.colorScheme; + + final (IconData icon, String label, bool warn) = switch (link.provision) { + MeshProvisionState.working => ( + Icons.hourglass_empty, + l10n.meshtasticChannelWorking, + false, + ), + MeshProvisionState.ready => ( + Icons.verified_outlined, + // l10n-ignore: channel name and index, not prose + '${l10n.meshtasticChannelReady} · ${DpipMeshChannel.name} ' + 'CH${link.dpipChannel}', + false, + ), + MeshProvisionState.noFreeSlot => ( + Icons.warning_amber_outlined, + l10n.meshtasticChannelNoSlot, + true, + ), + MeshProvisionState.conflict => ( + Icons.warning_amber_outlined, + link.provisionError ?? l10n.meshtasticChannelFailed, + true, + ), + MeshProvisionState.failed => ( + Icons.error_outline, + link.provisionError ?? l10n.meshtasticChannelFailed, + true, + ), + MeshProvisionState.idle => ( + Icons.hourglass_empty, + l10n.meshtasticChannelWorking, + false, + ), + }; + final mismatch = link.regionState == MeshRegionState.mismatch; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: warn ? colors.error : colors.primary), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + label, + style: theme.textTheme.labelMedium?.copyWith( + color: warn ? colors.error : colors.onSurfaceVariant, + ), + ), + ), + ], + ), + if (mismatch) + Row( + children: [ + Icon(Icons.public_outlined, size: 16, color: colors.error), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + l10n.meshtasticRegionMismatch(link.region ?? ''), + style: theme.textTheme.labelMedium?.copyWith( + color: colors.error, + ), + ), + ), + TextButton( + onPressed: () => unawaited(_confirmRegion(context, link)), + child: Text(l10n.meshtasticRegionSwitch), + ), + ], + ), + ], + ); + } + + /// The region change reboots the radio and moves *all* of its traffic, so it + /// never happens without a yes. + Future _confirmRegion(BuildContext context, MeshLink link) async { + final l10n = AppLocalizations.of(context); + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.public_outlined), + title: Text(l10n.meshtasticRegionSwitch), + content: Text(l10n.meshtasticRegionConfirm), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.commonCancel), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.meshtasticRegionSwitch), + ), + ], + ), + ); + if (confirmed != true) return; + final failure = await link.applyRegion(); + if (failure != null && context.mounted) _toast(context, failure); + } +} + +/// Which channel is being read and written, as a dropdown. +/// +/// A dropdown rather than a row of tabs because the channels are a list the +/// user picks *one* of, and most radios carry several: tabs would either wrap +/// or scroll sideways, hiding the very choice they exist to present. It also +/// keeps a permanent, readable answer to "which channel am I in" on screen. +class _ChannelPicker extends StatelessWidget { + const _ChannelPicker({ + required this.channels, + required this.selected, + required this.dpipIndex, + required this.counts, + required this.radio, + required this.onSelected, + }); + + final List channels; + final int selected; + + /// Which slot DPIP occupies, matched by index rather than by name: a + /// channel restored from the log has no name, and a radio is free to carry + /// DPIP under any index. + final int? dpipIndex; + final Map counts; + final MeshRadioInfo? radio; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + // DPIP first: it is the channel this app is responsible for, the others + // are the user's own and are simply passed through. + final ordered = [...channels] + ..sort((a, b) { + final aDpip = a.index == dpipIndex; + final bDpip = b.index == dpipIndex; + if (aDpip != bDpip) return aDpip ? -1 : 1; + return a.index.compareTo(b.index); + }); + final current = ordered.firstWhere( + (c) => c.index == selected, + orElse: () => ordered.first, + ); + + return Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), + child: PopupMenuButton( + initialValue: selected, + onSelected: onSelected, + borderRadius: AppRadius.medium, + itemBuilder: (_) => [ + for (final channel in ordered) + CheckedPopupMenuItem( + value: channel.index, + checked: channel.index == selected, + child: Row( + children: [ + if (channel.index == dpipIndex) ...[ + Icon( + Icons.shield_outlined, + size: 16, + color: colors.primary, + ), + const SizedBox(width: AppSpacing.sm), + ], + Text(_channelLabel(channel, radio)), + const SizedBox(width: AppSpacing.sm), + if ((counts[channel.index] ?? 0) > 0) + Text( + // l10n-ignore: message count + '${counts[channel.index]}', + style: theme.textTheme.labelSmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + current.index == dpipIndex + ? Icons.shield_outlined + : Icons.tag, + size: 18, + color: colors.primary, + ), + const SizedBox(width: AppSpacing.sm), + Text( + _channelLabel(current, radio), + style: theme.textTheme.titleSmall, + ), + Icon( + Icons.arrow_drop_down, + size: 20, + color: colors.onSurfaceVariant, + ), + ], + ), + ), + ), + ), + ); + } +} + +/// What to call a channel: its name, or for an unnamed primary the modem +/// preset the firmware names it after, or the slot number. +// l10n-ignore: channel index fallback +String _channelLabel(MeshChannel channel, MeshRadioInfo? radio) { + if (channel.name.isNotEmpty) return channel.name; + final preset = radio?.modemPreset; + if (channel.index == 0 && preset != null && preset.isNotEmpty) return preset; + return 'CH${channel.index}'; +} + +/// The message log — newest at the bottom, grouped by day. +class _MessageLog extends StatelessWidget { + const _MessageLog({required this.controller, required this.channel}); + + final MeshChatController controller; + + /// Which channel to show. Always filtered, connected or not — channels are + /// separate conversations, and interleaving them is wrong however little + /// else is known about the radio. + final int channel; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final messages = [ + for (final message in controller.messages) + if (message.channel == channel) message, + ]; + if (messages.isEmpty) { + return EmptyView( + icon: controller.isConnected + ? Icons.forum_outlined + : Icons.bluetooth_disabled_outlined, + message: controller.isConnected + ? l10n.meshtasticNoMessages + : l10n.meshtasticNotConnected, + ); + } + + return ListView.builder( + reverse: true, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + itemCount: messages.length, + itemBuilder: (context, index) { + final message = messages[index]; + // Reversed: the next index is the *older* message, so the day label + // belongs to the oldest message of each day. + final older = index + 1 < messages.length ? messages[index + 1] : null; + final startsDay = + older == null || !_sameDay(older.timestamp, message.timestamp); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (startsDay) _DayLabel(date: message.timestamp), + _Bubble(message: message, controller: controller), + ], + ); + }, + ); + } + + bool _sameDay(DateTime a, DateTime b) => + a.year == b.year && a.month == b.month && a.day == b.day; +} + +class _DayLabel extends StatelessWidget { + const _DayLabel({required this.date}); + + final DateTime date; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.md), + child: Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: AppRadius.small, + ), + child: Text( + MaterialLocalizations.of(context).formatMediumDate(date), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + } +} + +/// One message. Received messages sit left with their sender; sent ones sit +/// right in the primary tint. +class _Bubble extends StatelessWidget { + const _Bubble({required this.message, required this.controller}); + + final MeshChatMessage message; + final MeshChatController controller; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final colors = theme.colorScheme; + final outgoing = message.outgoing; + final empty = message.text.isEmpty; + final background = outgoing + ? colors.primaryContainer + : colors.surfaceContainerHigh; + final foreground = outgoing ? colors.onPrimaryContainer : colors.onSurface; + final muted = foreground.withValues(alpha: 0.7); + + return Align( + alignment: outgoing ? Alignment.centerRight : Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.78, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.sm), + child: Material( + color: background, + borderRadius: AppRadius.medium, + child: InkWell( + borderRadius: AppRadius.medium, + onLongPress: empty + ? null + : () async { + await Clipboard.setData( + ClipboardData(text: message.text), + ); + if (context.mounted) { + _toast(context, l10n.meshtasticCopied); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (!outgoing) + Text( + controller.senderLabel(message.from), + style: theme.textTheme.labelSmall?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w600, + ), + ), + Text( + empty ? l10n.meshtasticEmptyMessage : message.text, + style: theme.textTheme.bodyMedium?.copyWith( + color: empty ? muted : foreground, + fontStyle: empty ? FontStyle.italic : null, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + MaterialLocalizations.of(context).formatTimeOfDay( + TimeOfDay.fromDateTime(message.timestamp), + ), + style: theme.textTheme.labelSmall?.copyWith(color: muted), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} + +/// The pinned composer. Disabled until the radio is connected *and* configured +/// — sending earlier is rejected by the transport anyway. +class _Composer extends StatefulWidget { + const _Composer({required this.controller, required this.channel}); + + final MeshChatController controller; + + /// The channel a message goes out on — whatever tab is open. + final int channel; + + @override + State<_Composer> createState() => _ComposerState(); +} + +class _ComposerState extends State<_Composer> { + final TextEditingController _text = TextEditingController(); + final FocusNode _focus = FocusNode(); + bool _sending = false; + + @override + void dispose() { + _text.dispose(); + _focus.dispose(); + super.dispose(); + } + + Future _send() async { + final text = _text.text.trim(); + if (text.isEmpty || _sending) return; + setState(() => _sending = true); + final failure = await widget.controller.send(text, channel: widget.channel); + if (!mounted) return; + setState(() => _sending = false); + if (failure != null) { + _toast(context, failure); + return; + } + _text.clear(); + _focus.requestFocus(); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final colors = Theme.of(context).colorScheme; + final enabled = widget.controller.isConnected && !_sending; + return DecoratedBox( + decoration: BoxDecoration( + border: Border(top: BorderSide(color: colors.outlineVariant)), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.sm, + AppSpacing.sm, + AppSpacing.sm, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _text, + focusNode: _focus, + enabled: enabled, + minLines: 1, + maxLines: 4, + textInputAction: TextInputAction.send, + textCapitalization: TextCapitalization.sentences, + // Byte-based, not character-based: the frame budget is in + // UTF-8 bytes, and `maxLength` counts characters — it + // would promise a CJK writer three times the room there is. + inputFormatters: composerInputFormatters, + onSubmitted: (_) => unawaited(_send()), + decoration: InputDecoration( + hintText: l10n.meshtasticSendHint, + filled: true, + isDense: true, + border: const OutlineInputBorder( + borderRadius: AppRadius.large, + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ), + ), + ), + ValueListenableBuilder( + valueListenable: _text, + builder: (context, value, _) => _QuotaBar(text: value.text), + ), + ], + ), + ), + const SizedBox(width: AppSpacing.sm), + // Rebuilds with the field so the button lights up only once + // there's something to send. + ValueListenableBuilder( + valueListenable: _text, + builder: (context, value, _) => IconButton.filled( + tooltip: l10n.meshtasticSend, + onPressed: enabled && value.text.trim().isNotEmpty + ? () => unawaited(_send()) + : null, + icon: _sending + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.send), + ), + ), + ], + ), + ), + ); + } +} + +/// What the composer feeds its `TextField`. Public so the cap can be tested +/// against a real field rather than by reaching into a private class. +const List composerInputFormatters = [ + _ByteLimitFormatter(MeshPorts.maxTextBytes), +]; + +/// Caps the field at [maxBytes] **UTF-8 bytes**. +/// +/// A mesh frame's budget is bytes, and the alphabet decides the exchange rate: +/// one Latin letter costs 1, one Chinese character costs 3. Flutter's +/// `maxLength` counts characters, so it would let a Chinese message grow to +/// roughly three times what can actually be transmitted, and the send would +/// then fail at the radio with a message the user can't act on. +/// +/// Over-long input is truncated rather than rejected, so pasting a long text +/// keeps as much as fits instead of dropping all of it — cut on grapheme +/// clusters so an emoji or a combining mark is never split in half. +class _ByteLimitFormatter extends TextInputFormatter { + const _ByteLimitFormatter(this.maxBytes); + + final int maxBytes; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (_utf8Length(newValue.text) <= maxBytes) return newValue; + final buffer = StringBuffer(); + var used = 0; + for (final cluster in newValue.text.characters) { + final cost = _utf8Length(cluster); + if (used + cost > maxBytes) break; + buffer.write(cluster); + used += cost; + } + final text = buffer.toString(); + return TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); + } +} + +int _utf8Length(String text) => utf8.encode(text).length; + +/// How much of one mesh frame the draft already occupies. +/// +/// Shown as a bar rather than a number alone because the limit is in bytes: +/// "148/221" means little on its own, but a bar that is two-thirds full is +/// immediately readable. +class _QuotaBar extends StatelessWidget { + const _QuotaBar({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + if (text.isEmpty) return const SizedBox(height: AppSpacing.sm); + final theme = Theme.of(context); + final colors = theme.colorScheme; + final used = _utf8Length(text); + final ratio = (used / MeshPorts.maxTextBytes).clamp(0.0, 1.0); + final full = used >= MeshPorts.maxTextBytes; + final color = full + ? colors.error + : ratio > 0.8 + ? colors.tertiary + : colors.primary; + + return Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + AppSpacing.xs, + AppSpacing.md, + 0, + ), + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(2), + child: LinearProgressIndicator( + value: ratio, + minHeight: 3, + backgroundColor: colors.surfaceContainerHighest, + color: color, + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + // l10n-ignore: byte budget readout + Text( + '$used/${MeshPorts.maxTextBytes}', + style: theme.textTheme.labelSmall?.copyWith( + color: full ? colors.error : colors.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ); + } +} + +/// Radio picker — scans while open, connects on tap. +class _DeviceSheet extends StatelessWidget { + const _DeviceSheet(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final controller = context.watch(); + final devices = controller.devices; + final error = controller.scanError; + + return _SheetFrame( + title: l10n.meshtasticSelectDevice, + trailing: IconButton( + tooltip: l10n.meshtasticScan, + onPressed: controller.scanning + ? null + : () => unawaited(controller.startScan()), + icon: const Icon(Icons.refresh), + ), + children: [ + if (controller.scanning) const LinearProgressIndicator(), + if (error != null) + Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Text( + error, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + ) + else if (devices.isEmpty) + EmptyView( + icon: Icons.bluetooth_searching_outlined, + message: controller.scanning + ? l10n.meshtasticScanning + : l10n.meshtasticNoDevices, + ) + else + for (final device in devices) + ListTile( + leading: const Icon(Icons.router_outlined), + title: Text(device.name.isEmpty ? device.id : device.name), + subtitle: Text( + device.id, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: controller.connectingId == device.id + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.chevron_right), + onTap: controller.connectingId != null + ? null + : () => unawaited(_pick(context, controller, device)), + ), + ], + ); + } +} + +/// Connects to [device], asking first when another app already holds it. +/// +/// Neither iOS nor Android can evict another app's GATT link — the radio would +/// then be answering two clients that consume each other's packets, so the +/// honest move is to say so and let the user decide. +Future _pick( + BuildContext context, + MeshChatController controller, + MeshDevice device, +) async { + final l10n = AppLocalizations.of(context); + final navigator = Navigator.of(context); + // Stop scanning before connecting. The picker's scan would otherwise run for + // the rest of its timeout *through* the connect, and issuing a GATT connect + // while a scan is active is a classic source of Android's status-133 + // failures. + await controller.stopScan(); + var failure = await controller.connect(device); + + if (failure == MeshLink.busySentinel) { + if (!context.mounted) return; + final force = await showDialog( + context: context, + builder: (context) => AlertDialog( + icon: const Icon(Icons.phonelink_off_outlined), + title: Text(l10n.meshtasticBusyTitle), + content: Text(l10n.meshtasticBusyBody), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.commonCancel), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.meshtasticConnectAnyway), + ), + ], + ), + ); + if (force != true) return; + failure = await controller.connect(device, force: true); + } + + if (!context.mounted) return; + if (failure != null) { + _toast(context, failure); + return; + } + navigator.pop(); +} + +/// Everything the attached radio knows about itself, in one place: identity, +/// firmware, power, radio settings, channel table and session traffic. +/// +/// Diagnostics, so the values are shown raw — a wrong-looking number here is +/// the point. +class _RadioSheet extends StatelessWidget { + const _RadioSheet(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final service = context.read(); + final link = context.watch(); + final radio = service.radioInfo; + + return _SheetFrame( + title: l10n.meshtasticRadio, + children: [ + if (radio == null) + EmptyView( + icon: Icons.settings_input_antenna_outlined, + message: l10n.meshtasticNotConnected, + ) + else ...[ + _InfoSection( + title: l10n.meshtasticDevice, + rows: _device(l10n, radio), + ), + _InfoSection(title: l10n.meshtasticPower, rows: _power(l10n, radio)), + _InfoSection( + title: l10n.meshtasticRadioSettings, + rows: _lora(l10n, radio, link), + ), + SectionHeader(l10n.meshtasticUtilization), + FutureBuilder>( + future: context.read().metricsHistory(), + builder: (context, snapshot) => + MeshUtilizationChart(samples: snapshot.data ?? const []), + ), + StreamBuilder( + initialData: service.traffic, + stream: service.trafficStream, + builder: (context, snapshot) => _InfoSection( + title: l10n.meshtasticTraffic, + rows: _traffic(l10n, snapshot.data ?? const MeshTraffic()), + ), + ), + _InfoSection( + title: l10n.meshtasticChannels, + rows: [ + for (final channel in service.channels) + if (channel.enabled) + ( + // l10n-ignore: channel index label + 'CH${channel.index}', + channel.name.isEmpty + // l10n-ignore: firmware's own name for an unnamed channel + ? '(default)' + : channel.name, + ), + ], + ), + ], + ], + ); + } + + List<(String, String)> _device( + AppLocalizations l10n, + MeshRadioInfo radio, + ) => [ + (l10n.meshtasticName, radio.longName ?? '—'), + // l10n-ignore: node id in the hex form the mesh uses + (l10n.meshtasticNodeId, '!${radio.nodeNum.toRadixString(16)}'), + if (radio.shortName != null) (l10n.meshtasticShortName, radio.shortName!), + (l10n.meshtasticHardware, radio.hardware ?? '—'), + (l10n.meshtasticFirmware, radio.firmware ?? '—'), + (l10n.meshtasticRole, radio.role ?? '—'), + ]; + + List<(String, String)> _power(AppLocalizations l10n, MeshRadioInfo radio) => [ + ( + l10n.meshtasticBattery, + radio.isPluggedIn + ? l10n.meshtasticExternalPower + : radio.batteryPercent == null + ? '—' + // l10n-ignore: percentage readout + : '${radio.batteryPercent}%', + ), + if (radio.voltage != null) + // l10n-ignore: volts + (l10n.meshtasticVoltage, '${radio.voltage!.toStringAsFixed(2)} V'), + if (radio.uptime != null) + (l10n.meshtasticUptime, _durationLabel(radio.uptime!)), + // The age matters: the radio broadcasts telemetry every few minutes, so a + // charge figure with no timestamp reads as live when it isn't. + (l10n.meshtasticReadingAge, _sinceLabel(radio.metricsAt)), + ]; + + List<(String, String)> _lora( + AppLocalizations l10n, + MeshRadioInfo radio, + MeshLink link, + ) => [ + (l10n.meshtasticRegionLabel, radio.region ?? '—'), + (l10n.meshtasticPreset, radio.modemPreset ?? '—'), + if (radio.hopLimit != null) (l10n.meshtasticHopLimit, '${radio.hopLimit}'), + if (radio.txPower != null) + // l10n-ignore: dBm + (l10n.meshtasticTxPower, '${radio.txPower} dBm'), + if (radio.channelUtilization != null) + ( + l10n.meshtasticChannelUse, + // l10n-ignore: percentage readout + '${radio.channelUtilization!.toStringAsFixed(1)}%', + ), + if (radio.airUtilTx != null) + // l10n-ignore: percentage readout + (l10n.meshtasticAirtime, '${radio.airUtilTx!.toStringAsFixed(1)}%'), + ( + l10n.meshtasticDpipChannel, + link.dpipChannel == null + ? '—' + // l10n-ignore: channel index label + : '${DpipMeshChannel.name} CH${link.dpipChannel}', + ), + ]; + + List<(String, String)> _traffic(AppLocalizations l10n, MeshTraffic traffic) => + [ + ( + l10n.meshtasticReceived, + // l10n-ignore: packet/byte counters + '${traffic.rxPackets} · ${_bytesLabel(traffic.rxBytes)}', + ), + ( + l10n.meshtasticSent, + // l10n-ignore: packet/byte counters + '${traffic.txPackets} · ${_bytesLabel(traffic.txBytes)}', + ), + if (traffic.rxUndecoded > 0) + (l10n.meshtasticUndecoded, '${traffic.rxUndecoded}'), + (l10n.meshtasticLastReceived, _sinceLabel(traffic.lastRx)), + (l10n.meshtasticLastSent, _sinceLabel(traffic.lastTx)), + for (final entry in traffic.rxByPort.entries) + (_portLabel(entry.key), '${entry.value}'), + ]; +} + +/// A titled block of label/value rows. +class _InfoSection extends StatelessWidget { + const _InfoSection({required this.title, required this.rows}); + + final String title; + final List<(String, String)> rows; + + @override + Widget build(BuildContext context) { + if (rows.isEmpty) return const SizedBox.shrink(); + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionHeader(title), + for (final (label, value) in rows) + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + AppSpacing.xs, + AppSpacing.lg, + AppSpacing.xs, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: AppSpacing.md), + Text( + value, + textAlign: TextAlign.end, + style: theme.textTheme.bodyMedium?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ], + ); + } +} + +/// Meshtastic app ports, named where the name helps and numbered otherwise. +// l10n-ignore: protocol port names +String _portLabel(int portnum) => switch (portnum) { + 1 => 'Text', + 3 => 'Position', + 4 => 'Node info', + 5 => 'Routing', + 6 => 'Admin', + 8 => 'Waypoint', + 10 => 'Detection', + 67 => 'Telemetry', + 70 => 'Traceroute', + 71 => 'Neighbour info', + MeshPorts.private => 'DPIP', + _ => 'Port $portnum', +}; + +// l10n-ignore: byte counter +String _bytesLabel(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'; +} + +/// Everything the radio has heard, online first. +class _NodeSheet extends StatelessWidget { + const _NodeSheet(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final colors = Theme.of(context).colorScheme; + final controller = context.watch(); + final nodes = controller.nodes; + + return _SheetFrame( + title: l10n.meshtasticNodes, + trailing: Padding( + padding: const EdgeInsets.only(right: AppSpacing.md), + child: Text( + '${nodes.length}', + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(color: colors.onSurfaceVariant), + ), + ), + children: [ + if (nodes.isEmpty) + EmptyView(icon: Icons.hub_outlined, message: l10n.meshtasticNoNodes) + else + for (final node in nodes) + ListTile( + leading: Icon( + // Derived from `lastHeard`, not the flag the node was stored + // with: a node saved as online yesterday is not online now. + controller.isOnline(node) + ? Icons.circle + : Icons.circle_outlined, + size: 12, + color: controller.isOnline(node) + ? colors.primary + : colors.outline, + ), + title: Text(node.displayName), + subtitle: Text(_nodeDetail(node)), + ), + ], + ); + } + + // l10n-ignore: node id / battery / SNR readouts, not prose + String _nodeDetail(MeshNode node) => [ + '0x${node.num.toRadixString(16)}', + if (node.batteryLevel != null) '${node.batteryLevel}%', + if (node.snr != 0) 'SNR ${node.snr.toStringAsFixed(1)}', + ].join(' · '); +} + +/// Shared chrome for the two sheets: a title row over a scrollable body that +/// never grows past two thirds of the screen. +class _SheetFrame extends StatelessWidget { + const _SheetFrame({ + required this.title, + required this.children, + this.trailing, + }); + + final String title; + final List children; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.7, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + 0, + AppSpacing.sm, + AppSpacing.sm, + ), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ?trailing, + ], + ), + ), + Flexible(child: ListView(shrinkWrap: true, children: children)), + ], + ), + ), + ); + } +} diff --git a/lib/features/meshtastic/presentation/widgets/mesh_utilization_chart.dart b/lib/features/meshtastic/presentation/widgets/mesh_utilization_chart.dart new file mode 100644 index 000000000..20297782a --- /dev/null +++ b/lib/features/meshtastic/presentation/widgets/mesh_utilization_chart.dart @@ -0,0 +1,254 @@ +/// The radio's airtime over the last 24 hours: how busy the channel was, and +/// how much of that was this radio transmitting. +/// +/// **One axis, on purpose.** Both series are a percentage of airtime, so they +/// share a scale and can be read against each other — that comparison *is* the +/// question ("is the congestion mine or everyone's?"). Two y-scales would let +/// any pair of shapes be drawn to look alike, which is the most common way a +/// chart lies. +/// +/// Air time is drawn as a filled area under the channel line rather than a +/// second free-floating line, because it is a *part* of the total: the fill is +/// this radio's share, the gap above it everyone else's. +library; + +import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class MeshUtilizationChart extends StatelessWidget { + const MeshUtilizationChart({super.key, required this.samples}); + + /// Oldest first, inside [MeshStore.metricRetention]. + final List samples; + + /// Series colours, stepped per mode from the same two hues and validated + /// against each surface (lightness band, chroma, CVD separation at + /// ΔE ≥ 8, and 3:1 contrast). Dark is its own step, not a flip. + static const Color _channelLight = Color(0xFF1E88E5); + static const Color _airLight = Color(0xFFD97706); + static const Color _channelDark = Color(0xFF3D96E8); + static const Color _airDark = Color(0xFFBF8517); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); + final dark = theme.brightness == Brightness.dark; + final channelColor = dark ? _channelDark : _channelLight; + final airColor = dark ? _airDark : _airLight; + + final points = [ + for (final sample in samples) + if (sample.channelUtilization != null || sample.airUtilTx != null) + sample, + ]; + if (points.length < 2) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + child: Text( + l10n.meshtasticNoHistory, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ); + } + + final first = points.first.at.millisecondsSinceEpoch.toDouble(); + final last = points.last.at.millisecondsSinceEpoch.toDouble(); + // A flat 0–100 axis would squash a mesh that idles under 10%; the ceiling + // follows the data instead, with a floor so a quiet radio isn't magnified + // into a dramatic-looking one. + final peak = points.fold(0, (max, sample) { + final channel = sample.channelUtilization ?? 0; + final air = sample.airUtilTx ?? 0; + return [max, channel, air].reduce((a, b) => a > b ? a : b); + }); + final ceiling = peak <= 10 ? 10.0 : (peak * 1.2).ceilToDouble(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.lg, + 0, + AppSpacing.lg, + AppSpacing.sm, + ), + child: Wrap( + spacing: AppSpacing.lg, + runSpacing: AppSpacing.xs, + children: [ + _LegendEntry( + color: channelColor, + label: l10n.meshtasticChannelUse, + value: points.last.channelUtilization, + ), + _LegendEntry( + color: airColor, + label: l10n.meshtasticAirtime, + value: points.last.airUtilTx, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, + 0, + AppSpacing.lg, + AppSpacing.sm, + ), + child: SizedBox( + height: 140, + child: LineChart( + LineChartData( + minX: first, + maxX: last, + minY: 0, + maxY: ceiling, + clipData: const FlClipData.all(), + lineTouchData: const LineTouchData(enabled: false), + gridData: FlGridData( + drawVerticalLine: false, + horizontalInterval: ceiling / 2, + getDrawingHorizontalLine: (_) => FlLine( + // Recessive: the grid orients, it doesn't compete. + color: theme.colorScheme.outlineVariant.withValues( + alpha: 0.5, + ), + strokeWidth: 1, + ), + ), + borderData: FlBorderData(show: false), + titlesData: FlTitlesData( + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 34, + interval: ceiling / 2, + getTitlesWidget: (value, meta) => Text( + // l10n-ignore: percentage axis tick + '${value.round()}%', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 20, + // Ends only: a 24-hour window needs its bounds, not a + // tick every hour. + interval: (last - first).clamp(1, double.infinity), + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsets.only(top: AppSpacing.xs), + child: Text( + _hourLabel(value), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ), + lineBarsData: [ + // Air time first so the channel line draws over its fill. + _series(points, (s) => s.airUtilTx, airColor, filled: true), + _series(points, (s) => s.channelUtilization, channelColor), + ], + ), + ), + ), + ), + ], + ); + } + + LineChartBarData _series( + List points, + double? Function(MeshMetricSample) value, + Color color, { + bool filled = false, + }) => LineChartBarData( + spots: [ + for (final sample in points) + if (value(sample) != null) + FlSpot(sample.at.millisecondsSinceEpoch.toDouble(), value(sample)!), + ], + color: color, + barWidth: 2, + isCurved: true, + curveSmoothness: 0.2, + preventCurveOverShooting: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: filled, + color: color.withValues(alpha: 0.18), + ), + ); + + // l10n-ignore: clock tick on the time axis + String _hourLabel(double millis) { + final at = DateTime.fromMillisecondsSinceEpoch(millis.round()); + return '${at.hour.toString().padLeft(2, '0')}:' + '${at.minute.toString().padLeft(2, '0')}'; + } +} + +/// A legend entry that also carries the latest reading — identity never rests +/// on colour alone, and the number people actually want is the current one. +class _LegendEntry extends StatelessWidget { + const _LegendEntry({ + required this.color, + required this.label, + required this.value, + }); + + final Color color; + final String label; + final double? value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: AppSpacing.sm), + Text( + label, + // Text wears text tokens; the swatch beside it carries identity. + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: AppSpacing.xs), + Text( + // l10n-ignore: percentage readout + value == null ? '—' : '${value!.toStringAsFixed(1)}%', + style: theme.textTheme.labelMedium?.copyWith( + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ); + } +} diff --git a/lib/features/more/presentation/pages/more_page.dart b/lib/features/more/presentation/pages/more_page.dart index 8003836d7..67eea94f0 100644 --- a/lib/features/more/presentation/pages/more_page.dart +++ b/lib/features/more/presentation/pages/more_page.dart @@ -103,6 +103,11 @@ class MorePage extends StatelessWidget { title: l10n.moreDeveloper, onTap: () => context.pushNamed(AppRoutes.developer), ), + _MoreTile( + icon: Icons.router_outlined, + title: l10n.meshtasticTitle, + onTap: () => context.pushNamed(AppRoutes.meshtastic), + ), ], ), SectionHeader(l10n.moreSectionLinks), @@ -172,8 +177,7 @@ class MorePage extends StatelessWidget { icon: Icons.android, title: l10n.moreGooglePlay, host: 'play.google.com', - url: - 'https://play.google.com/store/apps/details?id=com.exptech.dpip', + url: 'https://play.google.com/store/apps/details?id=com.exptech.dpip', ), _MoreLinkTile( icon: Icons.apple, @@ -350,9 +354,8 @@ class _SavedRegionsTileState extends State<_SavedRegionsTile> { saved.length, RegionStore.maxSaved, ), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: colors.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelSmall + ?.copyWith(color: colors.onSurfaceVariant), ), ), ), diff --git a/lib/features/notification/data/notify_api.dart b/lib/features/notification/data/notify_api.dart index e3df1f269..dcddd84bf 100644 --- a/lib/features/notification/data/notify_api.dart +++ b/lib/features/notification/data/notify_api.dart @@ -28,10 +28,8 @@ class NotifyApi { String token, int channel, int status, - ) async => - (await _client.get( - ApiTier.coreExclusiveApi, - '${ApiPaths.notify}$token/$channel/$status', - )) - as List; + ) async => (await _client.get( + ApiTier.coreExclusiveApi, + '${ApiPaths.notify}$token/$channel/$status', + )) as List; } diff --git a/lib/features/settings/presentation/pages/developer_page.dart b/lib/features/settings/presentation/pages/developer_page.dart index d0290fc42..0c1234c0d 100644 --- a/lib/features/settings/presentation/pages/developer_page.dart +++ b/lib/features/settings/presentation/pages/developer_page.dart @@ -18,7 +18,9 @@ import 'package:dpip/core/network/network_usage_store.dart'; import 'package:dpip/core/notifications/notification_service.dart'; import 'package:dpip/core/platform/device_info.dart'; import 'package:dpip/core/settings/experimental_settings.dart'; +import 'package:dpip/core/storage/app_storage_scan.dart'; import 'package:dpip/features/settings/presentation/widgets/network_usage_chart.dart'; +import 'package:dpip/features/settings/presentation/widgets/storage_breakdown.dart'; import 'package:dpip/shared/map/map_tile_cache.dart'; import 'package:dpip/shared/widgets/loading_view.dart'; import 'package:dpip/shared/widgets/section_header.dart'; @@ -36,19 +38,6 @@ typedef _Field = ({String label, String? value}); /// Shared by the row, the dialog title, and its confirm button. const String _clearCacheTitle = 'Clear cache'; -/// Formats a byte count as a compact human string (B / KB / MB / GB). -String _formatBytes(int bytes) { - if (bytes < 1024) return '$bytes B'; - const units = ['KB', 'MB', 'GB', 'TB']; - var value = bytes / 1024; - var unit = 0; - while (value >= 1024 && unit < units.length - 1) { - value /= 1024; - unit++; - } - return '${value.toStringAsFixed(value < 10 ? 1 : 0)} ${units[unit]}'; -} - /// Formats a cache hit rate as `NN% (hits/total)`, or a dash when the window /// saw no cacheable request at all — 0% would read as "the cache is failing". String _formatRate(double rate, int hits, int total) => @@ -65,6 +54,7 @@ class _DeveloperPageState extends State { List<({String title, List<_Field> fields})>? _sections; List? _usageHistory; List? _usageWeek; + StorageScan? _storage; bool _clearing = false; /// Version-row taps toward the experimental unlock. Deliberately not @@ -92,6 +82,7 @@ class _DeveloperPageState extends State { hours: 24 * 7, bucketHours: 6, ); + final storage = await const StorageScanner().scan(); final device = await DeviceInfoService.load(); // Track the build by the git commit it was built from (kGitCommit is kept // current by the .githooks generator — see tool/setup.sh), falling back to @@ -146,10 +137,36 @@ class _DeveloperPageState extends State { ), ( label: 'Size on disk', - value: cacheStats == null ? '—' : _formatBytes(cacheStats.bytes), + value: cacheStats == null ? '—' : formatBytes(cacheStats.bytes), ), ], ), + // What iOS Settings ("文件與資料") and Android Settings report, split + // into the app's own categories. The SQLite body budget (350 MB) is not + // the whole story — the DB file carries page overhead and the OS-level + // caches are separate. + ( + title: 'Storage', + fields: [ + (label: 'Total on disk', value: formatBytes(storage.totalBytes)), + for (final slice in storageBreakdown(storage)) + ( + label: slice.label, + value: + '${formatBytes(slice.bytes)} ' + '(${(slice.bytes / storage.totalBytes * 100).toStringAsFixed(1)}%)', + ), + // Why the total is what it is: the biggest individual files. A + // runaway in tmp (MapLibre's transient tile work, aborted native + // writes) shows up here by name long before the pie chart explains + // anything. + if (storage.files.isNotEmpty) ...[ + (label: 'Largest files', value: null), + for (final file in storage.files.take(8)) + (label: file.shortPath, value: formatBytes(file.bytes)), + ], + ], + ), // Every figure here is the same pair of trailing windows, so they can be // read against each other. ( @@ -157,19 +174,19 @@ class _DeveloperPageState extends State { fields: [ ( label: 'Downloaded · last 24h', - value: usage == null ? '—' : _formatBytes(usage.last24h), + value: usage == null ? '—' : formatBytes(usage.last24h), ), ( label: 'Downloaded · last 7d', - value: usage == null ? '—' : _formatBytes(usage.last7d), + value: usage == null ? '—' : formatBytes(usage.last7d), ), ( label: 'Traffic saved · last 24h', - value: usage == null ? '—' : _formatBytes(usage.saved24h), + value: usage == null ? '—' : formatBytes(usage.saved24h), ), ( label: 'Traffic saved · last 7d', - value: usage == null ? '—' : _formatBytes(usage.saved7d), + value: usage == null ? '—' : formatBytes(usage.saved7d), ), ( label: 'Hit rate · last 24h', @@ -191,6 +208,7 @@ class _DeveloperPageState extends State { _sections = sections; _usageHistory = usageHistory; _usageWeek = usageWeek; + _storage = storage; }); } } @@ -240,8 +258,9 @@ class _DeveloperPageState extends State { title: const Text(_clearCacheTitle), content: const Text( 'Stored map tiles and API responses will be deleted and downloaded ' - 'again next time they are needed. Traffic and hit-rate figures reset ' - 'to zero.', + 'again next time they are needed. The database file is compacted ' + 'and the OS-level HTTP cache is cleared as well. Traffic and ' + 'hit-rate figures reset to zero.', ), actions: [ TextButton( @@ -263,6 +282,9 @@ class _DeveloperPageState extends State { setState(() => _clearing = true); try { await etagCache?.clear(); + // SQLite keeps free pages after a delete, so the file stays fat until + // compacted — the user asked to reclaim space, not just to empty rows. + await etagCache?.compact(); await networkUsage?.clear(); // The mirror would otherwise keep serving bytes the store no longer has, // so "cleared" would not look cleared until the app restarted. @@ -270,6 +292,13 @@ class _DeveloperPageState extends State { // MapLibre's own ambient DB is separate — poisoned immutable tiles // (e.g. bad Content-Encoding) survive SQLite clears otherwise. await clearAmbientCache(); + // The OS-level HTTP cache is invisible to every clear above — iOS + // NSURLCache keeps its own copy of responses behind the app's back. + await const StorageScanner().clearSystemHttpCache(); + // iOS tmp is where transient native work (MapLibre tile handling, + // aborted snapshot writes) accumulates — nothing the app owns lives + // there, so it can be dropped wholesale. + await const StorageScanner().clearTmp(); } catch (error, stackTrace) { Log.handle(error, stackTrace, 'dev: clear cache'); } @@ -383,6 +412,11 @@ class _DeveloperPageState extends State { history: _usageHistory!, week: _usageWeek!, ), + if (sections[i].title == 'Storage' && _storage != null) + StorageBreakdown( + slices: storageBreakdown(_storage!), + total: _storage!.totalBytes, + ), ], const SectionHeader('Maintenance'), ListTile( diff --git a/lib/features/settings/presentation/widgets/storage_breakdown.dart b/lib/features/settings/presentation/widgets/storage_breakdown.dart new file mode 100644 index 000000000..df4c739d4 --- /dev/null +++ b/lib/features/settings/presentation/widgets/storage_breakdown.dart @@ -0,0 +1,102 @@ +/// Storage breakdown: a donut chart of what is on disk plus a labelled legend +/// with sizes and percentages. +/// +/// Deliberately English-only like the rest of the Developer page (it exists to +/// be screenshotted into a bug report). No tooltips — a pie label balloon was +/// exactly what overflowed the screen in the network chart, and a legend +/// beside the chart is readable at a glance anyway. +library; + +import 'package:dpip/core/storage/app_storage_scan.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +/// Colour for the [index]th slice — a fixed, distinguishable palette. +const List storageSliceColors = [ + Color(0xFFE53935), + Color(0xFF1E88E5), + Color(0xFF43A047), + Color(0xFFFB8C00), + Color(0xFF8E24AA), + Color(0xFF00ACC1), + Color(0xFFD81B60), + Color(0xFF6D4C41), + Color(0xFF546E7A), +]; + +class StorageBreakdown extends StatelessWidget { + const StorageBreakdown({ + super.key, + required this.slices, + required this.total, + }); + + /// Already-sorted slices; the pie is drawn from these. + final List slices; + + /// The total the percentages are measured against. + final int total; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + if (total <= 0) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 150, + child: PieChart( + PieChartData( + centerSpaceRadius: 30, + sectionsSpace: 2, + sections: [ + for (var i = 0; i < slices.length; i++) + PieChartSectionData( + value: slices[i].bytes.toDouble(), + color: storageSliceColors[i % storageSliceColors.length], + radius: 46, + showTitle: false, + ), + ], + ), + ), + ), + const SizedBox(height: 8), + for (var i = 0; i < slices.length; i++) _legendRow(theme, i), + ], + ), + ); + } + + Widget _legendRow(ThemeData theme, int i) { + final slice = slices[i]; + final percent = total == 0 ? 0.0 : slice.bytes / total * 100; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: storageSliceColors[i % storageSliceColors.length], + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded(child: Text(slice.label, style: theme.textTheme.bodySmall)), + Text( + '${formatBytes(slice.bytes)} · ${percent.toStringAsFixed(1)}%', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontFamily: 'monospace', + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/typhoon/domain/storm_circle.freezed.dart b/lib/features/typhoon/domain/storm_circle.freezed.dart index 49f153360..e2b5e7113 100644 --- a/lib/features/typhoon/domain/storm_circle.freezed.dart +++ b/lib/features/typhoon/domain/storm_circle.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'storm_circle.dart'; @@ -9,6 +9,7 @@ part of 'storm_circle.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -71,7 +72,7 @@ class _$StormCircleCopyWithImpl<$Res> /// Create a copy of StormCircle /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? avg = null,Object? ne = null,Object? se = null,Object? sw = null,Object? nw = null,}) { - return _then(_self.copyWith( + return _then(StormCircle( avg: null == avg ? _self.avg : avg // ignore: cast_nullable_to_non_nullable as double,ne: null == ne ? _self.ne : ne // ignore: cast_nullable_to_non_nullable as double,se: null == se ? _self.se : se // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/typhoon/domain/typhoon_cyclone.freezed.dart b/lib/features/typhoon/domain/typhoon_cyclone.freezed.dart index ef135e2e5..c17f58f5d 100644 --- a/lib/features/typhoon/domain/typhoon_cyclone.freezed.dart +++ b/lib/features/typhoon/domain/typhoon_cyclone.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'typhoon_cyclone.dart'; @@ -9,6 +9,7 @@ part of 'typhoon_cyclone.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -76,7 +77,7 @@ class _$TyphoonCycloneCopyWithImpl<$Res> /// Create a copy of TyphoonCyclone /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? cwaName = freezed,Object? year = null,Object? tdNo = freezed,Object? tyNo = freezed,Object? time = null,Object? latitude = null,Object? longitude = null,Object? wind = freezed,Object? gust = freezed,Object? pressure = freezed,Object? speed = freezed,Object? direction = freezed,}) { - return _then(_self.copyWith( + return _then(TyphoonCyclone( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,cwaName: freezed == cwaName ? _self.cwaName : cwaName // ignore: cast_nullable_to_non_nullable as String?,year: null == year ? _self.year : year // ignore: cast_nullable_to_non_nullable @@ -385,7 +386,7 @@ class _$CycloneIndexCopyWithImpl<$Res> /// Create a copy of CycloneIndex /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? updated = null,Object? cyclones = null,}) { - return _then(_self.copyWith( + return _then(CycloneIndex( updated: null == updated ? _self.updated : updated // ignore: cast_nullable_to_non_nullable as int,cyclones: null == cyclones ? _self.cyclones : cyclones // ignore: cast_nullable_to_non_nullable as List, @@ -529,7 +530,7 @@ return $default(_that.updated,_that.cyclones);case _: @JsonSerializable() class _CycloneIndex implements CycloneIndex { - const _CycloneIndex({required this.updated, required final List cyclones}): _cyclones = cyclones; + const _CycloneIndex({required this.updated, required List cyclones}): _cyclones = cyclones; factory _CycloneIndex.fromJson(Map json) => _$CycloneIndexFromJson(json); @override final int updated; diff --git a/lib/features/typhoon/domain/typhoon_potential.freezed.dart b/lib/features/typhoon/domain/typhoon_potential.freezed.dart index b50b65b69..04b95e80b 100644 --- a/lib/features/typhoon/domain/typhoon_potential.freezed.dart +++ b/lib/features/typhoon/domain/typhoon_potential.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'typhoon_potential.dart'; @@ -9,6 +9,7 @@ part of 'typhoon_potential.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -68,7 +69,7 @@ class _$ForecastPointCopyWithImpl<$Res> /// Create a copy of ForecastPoint /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? label = null,Object? latitude = null,Object? longitude = null,}) { - return _then(_self.copyWith( + return _then(ForecastPoint( label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable as String,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable @@ -344,7 +345,7 @@ class _$TyphoonPotentialCopyWithImpl<$Res> /// Create a copy of TyphoonPotential /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? tdNo = freezed,Object? name = freezed,Object? past = null,Object? forecast = null,Object? cone = null,Object? circle = freezed,Object? current = freezed,Object? points = null,}) { - return _then(_self.copyWith( + return _then(TyphoonPotential( tdNo: freezed == tdNo ? _self.tdNo : tdNo // ignore: cast_nullable_to_non_nullable as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String?,past: null == past ? _self.past : past // ignore: cast_nullable_to_non_nullable @@ -494,7 +495,7 @@ return $default(_that.tdNo,_that.name,_that.past,_that.forecast,_that.cone,_that class _TyphoonPotential implements TyphoonPotential { - const _TyphoonPotential({this.tdNo, this.name, required final List past, required final List forecast, required final List cone, final List? circle, this.current, required final List points}): _past = past,_forecast = forecast,_cone = cone,_circle = circle,_points = points; + const _TyphoonPotential({this.tdNo, this.name, required List past, required List forecast, required List cone, List? circle, this.current, required List points}): _past = past,_forecast = forecast,_cone = cone,_circle = circle,_points = points; /// CWA tropical-depression number — unique within a snapshot. @@ -667,7 +668,7 @@ class _$PotentialPayloadCopyWithImpl<$Res> /// Create a copy of PotentialPayload /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? updated = null,Object? cyclones = null,}) { - return _then(_self.copyWith( + return _then(PotentialPayload( updated: null == updated ? _self.updated : updated // ignore: cast_nullable_to_non_nullable as int,cyclones: null == cyclones ? _self.cyclones : cyclones // ignore: cast_nullable_to_non_nullable as List, @@ -811,7 +812,7 @@ return $default(_that.updated,_that.cyclones);case _: class _PotentialPayload implements PotentialPayload { - const _PotentialPayload({required this.updated, required final List cyclones}): _cyclones = cyclones; + const _PotentialPayload({required this.updated, required List cyclones}): _cyclones = cyclones; @override final int updated; diff --git a/lib/features/typhoon/domain/typhoon_probability.freezed.dart b/lib/features/typhoon/domain/typhoon_probability.freezed.dart index 94e02d3d1..d3be21e71 100644 --- a/lib/features/typhoon/domain/typhoon_probability.freezed.dart +++ b/lib/features/typhoon/domain/typhoon_probability.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'typhoon_probability.dart'; @@ -9,6 +9,7 @@ part of 'typhoon_probability.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; /// @nodoc @@ -65,7 +66,7 @@ class _$ProbabilityLevelCopyWithImpl<$Res> /// Create a copy of ProbabilityLevel /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? p = null,Object? coords = null,}) { - return _then(_self.copyWith( + return _then(ProbabilityLevel( p: null == p ? _self.p : p // ignore: cast_nullable_to_non_nullable as int,coords: null == coords ? _self.coords : coords // ignore: cast_nullable_to_non_nullable as List, @@ -209,7 +210,7 @@ return $default(_that.p,_that.coords);case _: class _ProbabilityLevel implements ProbabilityLevel { - const _ProbabilityLevel({required this.p, required final List coords}): _coords = coords; + const _ProbabilityLevel({required this.p, required List coords}): _coords = coords; /// Strike probability (%). @@ -335,7 +336,7 @@ class _$CycloneProbabilityCopyWithImpl<$Res> /// Create a copy of CycloneProbability /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? tdNo = freezed,Object? levels = null,}) { - return _then(_self.copyWith( + return _then(CycloneProbability( tdNo: freezed == tdNo ? _self.tdNo : tdNo // ignore: cast_nullable_to_non_nullable as String?,levels: null == levels ? _self.levels : levels // ignore: cast_nullable_to_non_nullable as List, @@ -479,7 +480,7 @@ return $default(_that.tdNo,_that.levels);case _: class _CycloneProbability implements CycloneProbability { - const _CycloneProbability({this.tdNo, required final List levels}): _levels = levels; + const _CycloneProbability({this.tdNo, required List levels}): _levels = levels; /// CWA tropical-depression number; may be blank when upstream can't match. @@ -602,7 +603,7 @@ class _$TyphoonProbabilityCopyWithImpl<$Res> /// Create a copy of TyphoonProbability /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? updated = null,Object? cyclones = null,}) { - return _then(_self.copyWith( + return _then(TyphoonProbability( updated: null == updated ? _self.updated : updated // ignore: cast_nullable_to_non_nullable as int,cyclones: null == cyclones ? _self.cyclones : cyclones // ignore: cast_nullable_to_non_nullable as List, @@ -746,7 +747,7 @@ return $default(_that.updated,_that.cyclones);case _: class _TyphoonProbability implements TyphoonProbability { - const _TyphoonProbability({required this.updated, required final List cyclones}): _cyclones = cyclones; + const _TyphoonProbability({required this.updated, required List cyclones}): _cyclones = cyclones; @override final int updated; diff --git a/lib/features/typhoon/domain/typhoon_track.freezed.dart b/lib/features/typhoon/domain/typhoon_track.freezed.dart index a5d7ffad8..e0887daa3 100644 --- a/lib/features/typhoon/domain/typhoon_track.freezed.dart +++ b/lib/features/typhoon/domain/typhoon_track.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'typhoon_track.dart'; @@ -9,6 +9,7 @@ part of 'typhoon_track.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -69,7 +70,7 @@ class _$TrackFixCopyWithImpl<$Res> /// Create a copy of TrackFix /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? latitude = null,Object? longitude = null,Object? wind = freezed,Object? gust = freezed,Object? pressure = freezed,}) { - return _then(_self.copyWith( + return _then(TrackFix( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable @@ -355,7 +356,7 @@ class _$TrackNowCopyWithImpl<$Res> /// Create a copy of TrackNow /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? speed = freezed,Object? direction = freezed,Object? move = freezed,Object? c15 = freezed,Object? c25 = freezed,}) { - return _then(_self.copyWith( + return _then(TrackNow( speed: freezed == speed ? _self.speed : speed // ignore: cast_nullable_to_non_nullable as double?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable as String?,move: freezed == move ? _self.move : move // ignore: cast_nullable_to_non_nullable @@ -526,7 +527,7 @@ return $default(_that.speed,_that.direction,_that.move,_that.c15,_that.c25);case @JsonSerializable() class _TrackNow implements TrackNow { - const _TrackNow({this.speed, @JsonKey(name: 'dir') this.direction, final List? move, this.c15, this.c25}): _move = move; + const _TrackNow({this.speed, @JsonKey(name: 'dir') this.direction, List? move, this.c15, this.c25}): _move = move; factory _TrackNow.fromJson(Map json) => _$TrackNowFromJson(json); /// Translation speed (km/hr). @@ -701,7 +702,7 @@ class _$TrackForecastCopyWithImpl<$Res> /// Create a copy of TrackForecast /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? tau = null,Object? time = null,Object? latitude = null,Object? longitude = null,Object? wind = freezed,Object? gust = freezed,Object? pressure = freezed,Object? speed = freezed,Object? direction = freezed,Object? r15 = freezed,Object? r70 = freezed,Object? state = freezed,}) { - return _then(_self.copyWith( + return _then(TrackForecast( tau: null == tau ? _self.tau : tau // ignore: cast_nullable_to_non_nullable as int,time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable @@ -855,7 +856,7 @@ return $default(_that.tau,_that.time,_that.latitude,_that.longitude,_that.wind,_ @JsonSerializable() class _TrackForecast implements TrackForecast { - const _TrackForecast({required this.tau, @JsonKey(name: 't') required this.time, @JsonKey(name: 'lat') required this.latitude, @JsonKey(name: 'lon') required this.longitude, this.wind, this.gust, @JsonKey(name: 'pres') this.pressure, this.speed, @JsonKey(name: 'dir') this.direction, this.r15, this.r70, final List? state}): _state = state; + const _TrackForecast({required this.tau, @JsonKey(name: 't') required this.time, @JsonKey(name: 'lat') required this.latitude, @JsonKey(name: 'lon') required this.longitude, this.wind, this.gust, @JsonKey(name: 'pres') this.pressure, this.speed, @JsonKey(name: 'dir') this.direction, this.r15, this.r70, List? state}): _state = state; factory _TrackForecast.fromJson(Map json) => _$TrackForecastFromJson(json); /// Forecast lead time (hours). @@ -1015,7 +1016,7 @@ class _$TyphoonTrackCopyWithImpl<$Res> /// Create a copy of TyphoonTrack /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? cwaName = freezed,Object? year = null,Object? tdNo = freezed,Object? tyNo = freezed,Object? analysis = null,Object? now = freezed,Object? forecast = null,}) { - return _then(_self.copyWith( + return _then(TyphoonTrack( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,cwaName: freezed == cwaName ? _self.cwaName : cwaName // ignore: cast_nullable_to_non_nullable as String?,year: null == year ? _self.year : year // ignore: cast_nullable_to_non_nullable @@ -1177,7 +1178,7 @@ return $default(_that.name,_that.cwaName,_that.year,_that.tdNo,_that.tyNo,_that. @JsonSerializable() class _TyphoonTrack implements TyphoonTrack { - const _TyphoonTrack({required this.name, this.cwaName, required this.year, this.tdNo, this.tyNo, required final List analysis, this.now, required final List forecast}): _analysis = analysis,_forecast = forecast; + const _TyphoonTrack({required this.name, this.cwaName, required this.year, this.tdNo, this.tyNo, required List analysis, this.now, required List forecast}): _analysis = analysis,_forecast = forecast; factory _TyphoonTrack.fromJson(Map json) => _$TyphoonTrackFromJson(json); @override final String name; @@ -1335,7 +1336,7 @@ class _$TrackPayloadCopyWithImpl<$Res> /// Create a copy of TrackPayload /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? updated = null,Object? cyclones = null,}) { - return _then(_self.copyWith( + return _then(TrackPayload( updated: null == updated ? _self.updated : updated // ignore: cast_nullable_to_non_nullable as int,cyclones: null == cyclones ? _self.cyclones : cyclones // ignore: cast_nullable_to_non_nullable as List, @@ -1479,7 +1480,7 @@ return $default(_that.updated,_that.cyclones);case _: @JsonSerializable() class _TrackPayload implements TrackPayload { - const _TrackPayload({required this.updated, required final List cyclones}): _cyclones = cyclones; + const _TrackPayload({required this.updated, required List cyclones}): _cyclones = cyclones; factory _TrackPayload.fromJson(Map json) => _$TrackPayloadFromJson(json); @override final int updated; diff --git a/lib/features/typhoon/domain/typhoon_warning.freezed.dart b/lib/features/typhoon/domain/typhoon_warning.freezed.dart index 2e2925532..0400f522a 100644 --- a/lib/features/typhoon/domain/typhoon_warning.freezed.dart +++ b/lib/features/typhoon/domain/typhoon_warning.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'typhoon_warning.dart'; @@ -9,6 +9,7 @@ part of 'typhoon_warning.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -71,7 +72,7 @@ class _$WarningFixCopyWithImpl<$Res> /// Create a copy of WarningFix /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? latitude = null,Object? longitude = null,Object? wind = freezed,Object? gust = freezed,Object? pressure = freezed,Object? r15 = freezed,Object? scale = freezed,}) { - return _then(_self.copyWith( + return _then(WarningFix( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable @@ -221,7 +222,7 @@ return $default(_that.time,_that.latitude,_that.longitude,_that.wind,_that.gust, @JsonSerializable() class _WarningFix implements WarningFix { - const _WarningFix({@JsonKey(name: 't') required this.time, @JsonKey(name: 'lat') required this.latitude, @JsonKey(name: 'lon') required this.longitude, this.wind, this.gust, @JsonKey(name: 'pres') this.pressure, this.r15, final List? scale}): _scale = scale; + const _WarningFix({@JsonKey(name: 't') required this.time, @JsonKey(name: 'lat') required this.latitude, @JsonKey(name: 'lon') required this.longitude, this.wind, this.gust, @JsonKey(name: 'pres') this.pressure, this.r15, List? scale}): _scale = scale; factory _WarningFix.fromJson(Map json) => _$WarningFixFromJson(json); @override@JsonKey(name: 't') final int time; @@ -372,7 +373,7 @@ class _$WarningTyphoonCopyWithImpl<$Res> /// Create a copy of WarningTyphoon /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? no = freezed,Object? name = null,Object? cwaName = freezed,Object? reportNo = freezed,Object? category = freezed,Object? analysis = null,Object? prediction = freezed,}) { - return _then(_self.copyWith( + return _then(WarningTyphoon( no: freezed == no ? _self.no : no // ignore: cast_nullable_to_non_nullable as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,cwaName: freezed == cwaName ? _self.cwaName : cwaName // ignore: cast_nullable_to_non_nullable @@ -698,7 +699,7 @@ class _$WarningSectionCopyWithImpl<$Res> /// Create a copy of WarningSection /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? title = null,Object? text = null,}) { - return _then(_self.copyWith( + return _then(WarningSection( title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as String,text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable as String, @@ -964,7 +965,7 @@ class _$WarningAreaCopyWithImpl<$Res> /// Create a copy of WarningArea /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? code = null,}) { - return _then(_self.copyWith( + return _then(WarningArea( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,code: null == code ? _self.code : code // ignore: cast_nullable_to_non_nullable as String, @@ -1237,7 +1238,7 @@ class _$TyphoonWarningCopyWithImpl<$Res> /// Create a copy of TyphoonWarning /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? tdNo = freezed,Object? active = null,Object? id = null,Object? sent = null,Object? status = null,Object? msgType = null,Object? scope = null,Object? event = null,Object? urgency = null,Object? severity = null,Object? certainty = null,Object? effective = null,Object? onset = null,Object? expires = null,Object? headline = null,Object? senderName = null,Object? typhoon = freezed,Object? sections = null,Object? areas = null,}) { - return _then(_self.copyWith( + return _then(TyphoonWarning( tdNo: freezed == tdNo ? _self.tdNo : tdNo // ignore: cast_nullable_to_non_nullable as String?,active: null == active ? _self.active : active // ignore: cast_nullable_to_non_nullable as bool,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable @@ -1410,7 +1411,7 @@ return $default(_that.tdNo,_that.active,_that.id,_that.sent,_that.status,_that.m @JsonSerializable() class _TyphoonWarning implements TyphoonWarning { - const _TyphoonWarning({this.tdNo, required this.active, required this.id, required this.sent, required this.status, required this.msgType, required this.scope, required this.event, required this.urgency, required this.severity, required this.certainty, required this.effective, required this.onset, required this.expires, required this.headline, required this.senderName, this.typhoon, required final List sections, required final List areas}): _sections = sections,_areas = areas; + const _TyphoonWarning({this.tdNo, required this.active, required this.id, required this.sent, required this.status, required this.msgType, required this.scope, required this.event, required this.urgency, required this.severity, required this.certainty, required this.effective, required this.onset, required this.expires, required this.headline, required this.senderName, this.typhoon, required List sections, required List areas}): _sections = sections,_areas = areas; factory _TyphoonWarning.fromJson(Map json) => _$TyphoonWarningFromJson(json); /// Matched CWA `tdNo` from the active cyclone index; blank when the @@ -1596,7 +1597,7 @@ class _$WarningPayloadCopyWithImpl<$Res> /// Create a copy of WarningPayload /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? updated = null,Object? cyclones = null,}) { - return _then(_self.copyWith( + return _then(WarningPayload( updated: null == updated ? _self.updated : updated // ignore: cast_nullable_to_non_nullable as int,cyclones: null == cyclones ? _self.cyclones : cyclones // ignore: cast_nullable_to_non_nullable as List, @@ -1740,7 +1741,7 @@ return $default(_that.updated,_that.cyclones);case _: class _WarningPayload implements WarningPayload { - const _WarningPayload({required this.updated, required final List cyclones}): _cyclones = cyclones; + const _WarningPayload({required this.updated, required List cyclones}): _cyclones = cyclones; @override final int updated; diff --git a/lib/features/weather/data/frame_tile_repository.dart b/lib/features/weather/data/frame_tile_repository.dart index 96964c815..7716534ea 100644 --- a/lib/features/weather/data/frame_tile_repository.dart +++ b/lib/features/weather/data/frame_tile_repository.dart @@ -44,7 +44,7 @@ abstract base class FrameTileRepository implements RasterFrameSource { /// How close to native's mirror cap a fill warm fills to — a little under the /// cap (native trims only beyond it), so the mirror stays full but never /// churns. - static const double _fillTarget = 0.85; + static const double _fillTarget = 0.9; @override Future warmFrameTiles({ diff --git a/lib/features/weather/data/meteor_weather_api.dart b/lib/features/weather/data/meteor_weather_api.dart index 04e6dd7ef..7a050a8bc 100644 --- a/lib/features/weather/data/meteor_weather_api.dart +++ b/lib/features/weather/data/meteor_weather_api.dart @@ -27,12 +27,10 @@ class MeteorWeatherApi { Future> getRealtime( double latitude, double longitude, - ) async => - (await _client.get( - _api, - '/api/v5/meteor/weather/realtime/$latitude,$longitude', - )) - as Map; + ) async => (await _client.get( + _api, + '/api/v5/meteor/weather/realtime/$latitude,$longitude', + )) as Map; /// Township forecast for the 3-digit [code]. Future> getForecast(String code) async => diff --git a/lib/features/weather/domain/lightning_snapshot.freezed.dart b/lib/features/weather/domain/lightning_snapshot.freezed.dart index 2edcf4cb7..02d023c73 100644 --- a/lib/features/weather/domain/lightning_snapshot.freezed.dart +++ b/lib/features/weather/domain/lightning_snapshot.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'lightning_snapshot.dart'; @@ -9,6 +9,7 @@ part of 'lightning_snapshot.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; /// @nodoc @@ -65,7 +66,7 @@ class _$LightningStrikeCopyWithImpl<$Res> /// Create a copy of LightningStrike /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? time = null,Object? latitude = null,Object? longitude = null,}) { - return _then(_self.copyWith( + return _then(LightningStrike( type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable @@ -333,7 +334,7 @@ class _$LightningSnapshotCopyWithImpl<$Res> /// Create a copy of LightningSnapshot /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? strikes = null,}) { - return _then(_self.copyWith( + return _then(LightningSnapshot( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,strikes: null == strikes ? _self.strikes : strikes // ignore: cast_nullable_to_non_nullable as List, @@ -477,7 +478,7 @@ return $default(_that.time,_that.strikes);case _: class _LightningSnapshot implements LightningSnapshot { - const _LightningSnapshot({required this.time, required final List strikes}): _strikes = strikes; + const _LightningSnapshot({required this.time, required List strikes}): _strikes = strikes; @override final int time; diff --git a/lib/features/weather/domain/rain_snapshot.freezed.dart b/lib/features/weather/domain/rain_snapshot.freezed.dart index f68e35a57..ed95e5894 100644 --- a/lib/features/weather/domain/rain_snapshot.freezed.dart +++ b/lib/features/weather/domain/rain_snapshot.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'rain_snapshot.dart'; @@ -9,6 +9,7 @@ part of 'rain_snapshot.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; /// @nodoc @@ -73,7 +74,7 @@ class _$RainObservationCopyWithImpl<$Res> /// Create a copy of RainObservation /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? now = freezed,Object? min10 = freezed,Object? hour1 = freezed,Object? hour3 = freezed,Object? hour6 = freezed,Object? hour12 = freezed,Object? hour24 = freezed,Object? day2 = freezed,Object? day3 = freezed,}) { - return _then(_self.copyWith( + return _then(RainObservation( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,now: freezed == now ? _self.now : now // ignore: cast_nullable_to_non_nullable as double?,min10: freezed == min10 ? _self.min10 : min10 // ignore: cast_nullable_to_non_nullable @@ -367,7 +368,7 @@ class _$RainSnapshotCopyWithImpl<$Res> /// Create a copy of RainSnapshot /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? stations = null,}) { - return _then(_self.copyWith( + return _then(RainSnapshot( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,stations: null == stations ? _self.stations : stations // ignore: cast_nullable_to_non_nullable as List, @@ -511,7 +512,7 @@ return $default(_that.time,_that.stations);case _: class _RainSnapshot implements RainSnapshot { - const _RainSnapshot({required this.time, required final List stations}): _stations = stations; + const _RainSnapshot({required this.time, required List stations}): _stations = stations; @override final int time; diff --git a/lib/features/weather/domain/rain_trend.freezed.dart b/lib/features/weather/domain/rain_trend.freezed.dart index 134e1bbc5..bcf878c50 100644 --- a/lib/features/weather/domain/rain_trend.freezed.dart +++ b/lib/features/weather/domain/rain_trend.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'rain_trend.dart'; @@ -9,6 +9,7 @@ part of 'rain_trend.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; /// @nodoc @@ -67,7 +68,7 @@ class _$RainTrendCopyWithImpl<$Res> /// Create a copy of RainTrend /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? range = null,Object? times = null,Object? rain = null,}) { - return _then(_self.copyWith( + return _then(RainTrend( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,range: null == range ? _self.range : range // ignore: cast_nullable_to_non_nullable as String,times: null == times ? _self.times : times // ignore: cast_nullable_to_non_nullable @@ -213,7 +214,7 @@ return $default(_that.id,_that.range,_that.times,_that.rain);case _: class _RainTrend implements RainTrend { - const _RainTrend({required this.id, required this.range, required final List times, required final List rain}): _times = times,_rain = rain; + const _RainTrend({required this.id, required this.range, required List times, required List rain}): _times = times,_rain = rain; /// 6-char station code (the `/station` directory key). diff --git a/lib/features/weather/domain/weather_forecast.freezed.dart b/lib/features/weather/domain/weather_forecast.freezed.dart index 60e05caa4..c8462bd18 100644 --- a/lib/features/weather/domain/weather_forecast.freezed.dart +++ b/lib/features/weather/domain/weather_forecast.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'weather_forecast.dart'; @@ -9,6 +9,7 @@ part of 'weather_forecast.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -67,7 +68,7 @@ class _$WeatherForecastCopyWithImpl<$Res> /// Create a copy of WeatherForecast /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? updateTime = null,Object? forecast = null,}) { - return _then(_self.copyWith( + return _then(WeatherForecast( updateTime: null == updateTime ? _self.updateTime : updateTime // ignore: cast_nullable_to_non_nullable as int,forecast: null == forecast ? _self.forecast : forecast // ignore: cast_nullable_to_non_nullable as List, @@ -211,7 +212,7 @@ return $default(_that.updateTime,_that.forecast);case _: @JsonSerializable() class _WeatherForecast extends WeatherForecast { - const _WeatherForecast({required this.updateTime, required final List forecast}): _forecast = forecast,super._(); + const _WeatherForecast({required this.updateTime, required List forecast}): _forecast = forecast,super._(); factory _WeatherForecast.fromJson(Map json) => _$WeatherForecastFromJson(json); /// Publish time, Unix **milliseconds** (13-digit) — see [updatedAt]. @@ -347,7 +348,7 @@ class _$WeatherForecastPointCopyWithImpl<$Res> /// Create a copy of WeatherForecastPoint /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? temperature = null,Object? apparentTemp = null,Object? humidity = null,Object? weather = null,Object? weatherCode = null,Object? pop = null,Object? wind = null,}) { - return _then(_self.copyWith( + return _then(WeatherForecastPoint( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as String,temperature: null == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable as double,apparentTemp: null == apparentTemp ? _self.apparentTemp : apparentTemp // ignore: cast_nullable_to_non_nullable @@ -656,7 +657,7 @@ class _$ForecastWindCopyWithImpl<$Res> /// Create a copy of ForecastWind /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? direction = null,Object? speed = null,Object? beaufort = null,}) { - return _then(_self.copyWith( + return _then(ForecastWind( direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable as String,speed: null == speed ? _self.speed : speed // ignore: cast_nullable_to_non_nullable as double,beaufort: null == beaufort ? _self.beaufort : beaufort // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/weather/domain/weather_realtime.freezed.dart b/lib/features/weather/domain/weather_realtime.freezed.dart index e74d0a91b..71dfb98f3 100644 --- a/lib/features/weather/domain/weather_realtime.freezed.dart +++ b/lib/features/weather/domain/weather_realtime.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'weather_realtime.dart'; @@ -9,6 +9,7 @@ part of 'weather_realtime.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -71,7 +72,7 @@ class _$WeatherRealtimeCopyWithImpl<$Res> /// Create a copy of WeatherRealtime /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? station = null,Object? time = null,Object? data = null,}) { - return _then(_self.copyWith( + return _then(WeatherRealtime( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,station: null == station ? _self.station : station // ignore: cast_nullable_to_non_nullable as WeatherRealtimeStation,time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable @@ -386,7 +387,7 @@ class _$WeatherRealtimeStationCopyWithImpl<$Res> /// Create a copy of WeatherRealtimeStation /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? latitude = null,Object? longitude = null,Object? altitude = null,Object? distance = null,}) { - return _then(_self.copyWith( + return _then(WeatherRealtimeStation( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable @@ -667,7 +668,7 @@ class _$WeatherRealtimeDataCopyWithImpl<$Res> /// Create a copy of WeatherRealtimeData /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? weather = null,Object? weatherCode = null,Object? temperature = freezed,Object? humidity = freezed,Object? rain = freezed,Object? wind = null,Object? gust = null,Object? visibility = freezed,Object? visibilityText = freezed,Object? pressure = freezed,Object? sunshine = freezed,}) { - return _then(_self.copyWith( + return _then(WeatherRealtimeData( weather: null == weather ? _self.weather : weather // ignore: cast_nullable_to_non_nullable as String,weatherCode: null == weatherCode ? _self.weatherCode : weatherCode // ignore: cast_nullable_to_non_nullable as int,temperature: freezed == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable @@ -1000,7 +1001,7 @@ class _$WeatherWindCopyWithImpl<$Res> /// Create a copy of WeatherWind /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? direction = freezed,Object? speed = freezed,Object? beaufort = freezed,}) { - return _then(_self.copyWith( + return _then(WeatherWind( direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable as String?,speed: freezed == speed ? _self.speed : speed // ignore: cast_nullable_to_non_nullable as double?,beaufort: freezed == beaufort ? _self.beaufort : beaufort // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/weather/domain/weather_snapshot.freezed.dart b/lib/features/weather/domain/weather_snapshot.freezed.dart index 87e13e77f..921c74b02 100644 --- a/lib/features/weather/domain/weather_snapshot.freezed.dart +++ b/lib/features/weather/domain/weather_snapshot.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'weather_snapshot.dart'; @@ -9,6 +9,7 @@ part of 'weather_snapshot.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; /// @nodoc @@ -71,7 +72,7 @@ class _$WeatherObservationCopyWithImpl<$Res> /// Create a copy of WeatherObservation /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? weatherCode = null,Object? temperature = freezed,Object? humidity = freezed,Object? pressure = freezed,Object? windDirection = freezed,Object? windSpeed = freezed,Object? gustSpeed = freezed,Object? gustDirection = freezed,Object? gustTime = freezed,Object? high = freezed,Object? highTime = freezed,Object? low = freezed,Object? lowTime = freezed,}) { - return _then(_self.copyWith( + return _then(WeatherObservation( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,weatherCode: null == weatherCode ? _self.weatherCode : weatherCode // ignore: cast_nullable_to_non_nullable as int,temperature: freezed == temperature ? _self.temperature : temperature // ignore: cast_nullable_to_non_nullable @@ -375,7 +376,7 @@ class _$WeatherSnapshotCopyWithImpl<$Res> /// Create a copy of WeatherSnapshot /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? time = null,Object? stations = null,}) { - return _then(_self.copyWith( + return _then(WeatherSnapshot( time: null == time ? _self.time : time // ignore: cast_nullable_to_non_nullable as int,stations: null == stations ? _self.stations : stations // ignore: cast_nullable_to_non_nullable as List, @@ -519,7 +520,7 @@ return $default(_that.time,_that.stations);case _: class _WeatherSnapshot implements WeatherSnapshot { - const _WeatherSnapshot({required this.time, required final List stations}): _stations = stations; + const _WeatherSnapshot({required this.time, required List stations}): _stations = stations; @override final int time; diff --git a/lib/features/weather/domain/weather_station.freezed.dart b/lib/features/weather/domain/weather_station.freezed.dart index ceb8931a9..09e64803e 100644 --- a/lib/features/weather/domain/weather_station.freezed.dart +++ b/lib/features/weather/domain/weather_station.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'weather_station.dart'; @@ -9,6 +9,7 @@ part of 'weather_station.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; @@ -66,7 +67,7 @@ class _$WeatherStationCopyWithImpl<$Res> /// Create a copy of WeatherStation /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? county = null,Object? town = null,Object? altitude = null,Object? latitude = null,Object? longitude = null,}) { - return _then(_self.copyWith( + return _then(WeatherStation( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,county: null == county ? _self.county : county // ignore: cast_nullable_to_non_nullable as String,town: null == town ? _self.town : town // ignore: cast_nullable_to_non_nullable diff --git a/lib/features/weather/domain/weather_trend.freezed.dart b/lib/features/weather/domain/weather_trend.freezed.dart index 12a19d0f8..e54b60343 100644 --- a/lib/features/weather/domain/weather_trend.freezed.dart +++ b/lib/features/weather/domain/weather_trend.freezed.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'weather_trend.dart'; @@ -9,6 +9,7 @@ part of 'weather_trend.dart'; // FreezedGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND // dart format off T _$identity(T value) => value; /// @nodoc @@ -71,7 +72,7 @@ class _$WeatherTrendCopyWithImpl<$Res> /// Create a copy of WeatherTrend /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? range = null,Object? times = null,Object? temperature = null,Object? humidity = null,Object? pressure = null,Object? windSpeed = null,Object? windDirection = null,}) { - return _then(_self.copyWith( + return _then(WeatherTrend( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,range: null == range ? _self.range : range // ignore: cast_nullable_to_non_nullable as String,times: null == times ? _self.times : times // ignore: cast_nullable_to_non_nullable @@ -221,7 +222,7 @@ return $default(_that.id,_that.range,_that.times,_that.temperature,_that.humidit class _WeatherTrend implements WeatherTrend { - const _WeatherTrend({required this.id, required this.range, required final List times, required final List temperature, required final List humidity, required final List pressure, required final List windSpeed, required final List windDirection}): _times = times,_temperature = temperature,_humidity = humidity,_pressure = pressure,_windSpeed = windSpeed,_windDirection = windDirection; + const _WeatherTrend({required this.id, required this.range, required List times, required List temperature, required List humidity, required List pressure, required List windSpeed, required List windDirection}): _times = times,_temperature = temperature,_humidity = humidity,_pressure = pressure,_windSpeed = windSpeed,_windDirection = windDirection; /// 6-char station code (the `/station` directory key). diff --git a/lib/features/weather/presentation/pages/weather_ranking_page.dart b/lib/features/weather/presentation/pages/weather_ranking_page.dart index 3d4f1abb9..fe6aff58f 100644 --- a/lib/features/weather/presentation/pages/weather_ranking_page.dart +++ b/lib/features/weather/presentation/pages/weather_ranking_page.dart @@ -314,19 +314,23 @@ class _WeatherRankingPageState extends State } } +final DateFormat _snapshotFormat = DateFormat('yyyy/MM/dd HH:mm'); + String _formatSnapshotTime(int unixSeconds) { final taipei = AppTime.taipei( DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000, isUtc: true), ); - return DateFormat('yyyy/MM/dd HH:mm').format(taipei); + return _snapshotFormat.format(taipei); } +final DateFormat _clockFormat = DateFormat('HH:mm'); + /// Taipei wall-clock `HH:mm` for an occurrence timestamp. String _formatClock(int unixSeconds) { final taipei = AppTime.taipei( DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000, isUtc: true), ); - return DateFormat('HH:mm').format(taipei); + return _clockFormat.format(taipei); } /// Ranking list → map tab: focus camera + open station sheet. @@ -417,9 +421,8 @@ class _RainRankingPanelState extends State<_RainRankingPanel> { padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Text( l10n.weatherRankingMeta(time, ranked.length), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colors.onSurfaceVariant), ), ), Expanded( @@ -544,9 +547,8 @@ class _WeatherMetricPanelState extends State<_WeatherMetricPanel> { child: Center( child: Text( l10n.weatherRankingBy, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: colors.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(color: colors.onSurfaceVariant), ), ), ), @@ -586,9 +588,8 @@ class _WeatherMetricPanelState extends State<_WeatherMetricPanel> { child: Center( child: Text( l10n.weatherRankingMergeTo, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: colors.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(color: colors.onSurfaceVariant), ), ), ), @@ -612,9 +613,8 @@ class _WeatherMetricPanelState extends State<_WeatherMetricPanel> { padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Text( l10n.weatherRankingMeta(time, ranked.length), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colors.onSurfaceVariant), ), ), Expanded( @@ -651,9 +651,9 @@ class _WeatherMetricPanelState extends State<_WeatherMetricPanel> { child: Icon( Icons.navigation, size: AppSpacing.lg, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, ), ); } @@ -793,9 +793,8 @@ class _TempExtremePanelState extends State<_TempExtremePanel> { child: Center( child: Text( l10n.weatherRankingMergeTo, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: colors.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(color: colors.onSurfaceVariant), ), ), ), @@ -819,9 +818,8 @@ class _TempExtremePanelState extends State<_TempExtremePanel> { padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: Text( l10n.weatherRankingMeta(time, ranked.length), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colors.onSurfaceVariant), ), ), Expanded( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6a7d25f24..b79ff1c86 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,1082 +1,1129 @@ { - "@@locale": "en", - "languageName": "English", - "@languageName": { - "description": "This language's own name, shown in the in-app language picker. Each locale's ARB names itself; the picker is built from these, never a hardcoded list." + "typhoonValueLat": "{lat}°N", + "onboardingSkipBody": "Without location and notifications, DPIP can't alert you to earthquakes and disasters near you in real time. You can still grant them later in Settings.", + "@mapLayerSatellite": { + "description": "Name of the Himawari infrared layer in the layer picker" }, - "navHome": "Home", - "@navHome": { - "description": "Bottom-nav label and page title for the Home tab" + "@mapAppCoordinatesCopied": { + "description": "Snackbar confirming the coordinates were copied" }, - "navEvents": "Events", - "@navEvents": { - "description": "Bottom-nav label and page title for the Events tab" + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" }, - "navMap": "Map", - "@navMap": { - "description": "Bottom-nav label and page title for the Map tab" + "rainInterval24h": "24 h", + "@mapLayerRain": { + "description": "Map layer switcher label for the rainfall station layer" }, - "navData": "Data", - "@navData": { - "description": "Bottom-nav label and page title for the Data hub tab" + "homeRainTrendHeavyStopping": "Heavy rain likely to stop in {minutes} minutes", + "mapTimelineObserved": "Observed", + "regionSelectTitle": "Select a region", + "skyTimeNoon": "Noon", + "radarCountyOutlineSubtitle": "Keeps county borders legible under the radar echo.", + "@meshtasticRegionLabel": { + "description": "LoRa region" }, - "navEarthquake": "Earthquake", - "@navEarthquake": { - "description": "Earthquake report catalogue title (entry under the Data hub)" + "dpmFilterSectionRestroomType": "Toilet types", + "mapLayerSatelliteB03": "Himawari Red (B03)", + "@typhoonLegendCurrent": { + "description": "Typhoon map legend: current storm centre" }, - "dataSectionSeismic": "Seismic", - "@dataSectionSeismic": { - "description": "Section header on the Data hub for earthquake-related entries" + "reportFilterIntensity": "Intensity", + "@mapLayerAed": { + "description": "Disaster-map overlay menu toggle for AED (defibrillator) points" }, - "dataEarthquakeSubtitle": "Earthquake reports", - "@dataEarthquakeSubtitle": { - "description": "Subtitle under the Earthquake tile on the Data hub" + "mapLayerLightning": "Lightning", + "restroomTypeMale": "Male", + "@mapLayerSatelliteB14": { + "description": "Himawari longwave-infrared channel (B14, 11.2 µm) layer name" }, - "dataSectionWeather": "Weather", - "@dataSectionWeather": { - "description": "Section header on the Data hub for weather observation rankings" + "@regionCurrentUnavailable": { + "description": "Shown when the current-location area is selected but GPS is off/unavailable" }, - "dataWeatherRankingSubtitle": "Live station rankings", - "@dataWeatherRankingSubtitle": { - "description": "Subtitle under weather ranking tiles on the Data hub" + "@mapLayerCategoryEarthquake": { + "description": "Section title in map overlay lists: the seismic-monitor overlays" }, - "weatherRankingTitle": "Observation rankings", - "@weatherRankingTitle": { - "description": "App bar title for the weather station ranking page" + "meshtasticLastReceived": "Last received", + "@notifyOptTsunamiAll": { + "description": "Notify option label" }, - "weatherRankingMeta": "Data time: {time}\n{count} stations", - "@weatherRankingMeta": { - "description": "Snapshot time and station count above a ranking list", + "@typhoonValueHpa": { "placeholders": { - "time": { + "n": { "type": "String" - }, - "count": { - "type": "int" } } }, - "weatherRankingEmpty": "No observations to rank", - "@weatherRankingEmpty": { - "description": "Empty state when a ranking list has no rows after filters" + "reportDetailSortByCounty": "Sort by county", + "@onboardingPermBatteryDesc": { + "description": "Permission row description: battery" }, - "weatherRankingBy": "Sort by", - "@weatherRankingBy": { - "description": "Label before highest/lowest (or desc/asc) chips on ranking" + "@reportListMagnitude": { + "description": "Emphasized magnitude on a report list row", + "placeholders": { + "magnitude": { + "type": "String" + } + } }, - "weatherRankingHighest": "Highest", - "@weatherRankingHighest": { - "description": "Chip to rank temperature descending" + "@mapLayerSatelliteCloudProbablyClear": { + "description": "Cloud-mask category: probably clear" }, - "weatherRankingLowest": "Lowest", - "@weatherRankingLowest": { - "description": "Chip to rank temperature ascending" + "@dpmAddress": { + "description": "Address row label in the disaster-map restroom / shelter detail sheet" }, - "weatherRankingMergeTo": "Merge to", - "@weatherRankingMergeTo": { - "description": "Label before township/county merge chips on ranking" + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" }, - "weatherRankingMergeTown": "Township", - "@weatherRankingMergeTown": { - "description": "Chip to keep one extreme station per township" + "@moonDays": { + "description": "Day unit for the moon age" }, - "weatherRankingMergeCounty": "County", - "@weatherRankingMergeCounty": { - "description": "Chip to keep one extreme station per county" + "@reportDetailReplay": { + "description": "Button that opens the RTS/EEW replay starting from this report's origin time" }, - "weatherRankingWind": "Wind speed", - "@weatherRankingWind": { - "description": "Ranking tab/tile for sustained wind speed" + "homeRainTrendScattered": "Light showers possible", + "@openSourceLicenses": { + "description": "More-menu entry that opens the bundled open-source license list" }, - "weatherRankingGust": "Gust", - "@weatherRankingGust": { - "description": "Ranking tab/tile for peak gust speed" + "meshtasticUptime": "Uptime", + "@commonError": { + "description": "Generic headline when an async request fails" }, "weatherRankingTempExtremes": "Daily extremes", - "@weatherRankingTempExtremes": { - "description": "Ranking tab for recorded daily high/low/range (not current temp)" + "themeLight": "Light", + "mapTerrainReliefHint": "Show shaded terrain relief on the base map", + "@commonEmpty": { + "description": "Generic message when a loaded list is empty" }, - "weatherRankingExtremeHigh": "Daily high", - "@weatherRankingExtremeHigh": { - "description": "Chip to rank by recorded daily maximum temperature" + "meshtasticEmptyMessage": "(empty message)", + "@themeLight": { + "description": "Theme option: always light" }, - "weatherRankingExtremeLow": "Daily low", - "@weatherRankingExtremeLow": { - "description": "Chip to rank by recorded daily minimum temperature" + "moreSectionRegion": "Region", + "dpmDisasterEarthquake": "Earthquake", + "mapLayerSatellite": "Himawari Infrared (B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" }, - "weatherRankingExtremeRange": "Diurnal range", - "@weatherRankingExtremeRange": { - "description": "Chip to rank by daily high minus low" + "aedHoursSaturday": "Saturday hours", + "dpmDisasterSlope": "Slope hazard", + "moonPhaseNew": "New moon", + "notifySectionEew": "Earthquake early warning", + "mapResetNorth": "Reset north", + "@mapLayerTyphoon": { + "description": "Layer-switcher label for the typhoon map layer" }, - "weatherRankingRecordedAt": "Recorded at {time}", - "@weatherRankingRecordedAt": { - "description": "Occurrence time for a gust or daily extreme", - "placeholders": { - "time": { - "type": "String" - } - } + "rainInterval2d": "2 d", + "mapTownLabelsHint": "Show township names when zoomed in", + "commonCancel": "Cancel", + "@reportFilterDateEndNote": { + "description": "Explains that endTime covers through the end of that calendar day" }, - "weatherRankingAnalysisCurrent": "Now {value}°C", - "@weatherRankingAnalysisCurrent": { - "description": "Current temperature fragment in an extremes analysis line", - "placeholders": { - "value": { - "type": "String" - } - } + "notifyOptTsunamiWarning": "Tsunami warnings only", + "mapLayerSatelliteBtdFog": "Himawari Night Fog", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" }, - "weatherRankingAnalysisHigh": "High {value}", - "@weatherRankingAnalysisHigh": { - "description": "Daily high fragment; value may include clock time", - "placeholders": { - "value": { - "type": "String" - } - } + "moreSectionAdvanced": "Advanced", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" }, - "weatherRankingAnalysisLow": "Low {value}", - "@weatherRankingAnalysisLow": { - "description": "Daily low fragment; value may include clock time", - "placeholders": { - "value": { - "type": "String" - } - } + "@reportFilterDepth": { + "description": "Label for the hypocentral-depth range filter" }, - "weatherRankingAnalysisRange": "Range {value}°C", - "@weatherRankingAnalysisRange": { - "description": "Diurnal range fragment in an extremes analysis line", - "placeholders": { - "value": { - "type": "String" - } - } + "@restroomGradeExcellent": { + "description": "Restroom cleanliness grade: excellent" }, - "reportListEmpty": "No earthquake reports", - "@reportListEmpty": { - "description": "Empty state when the report catalogue has no rows" + "@moreSourceCode": { + "description": "More-menu link to DPIP's source repository on GitHub" }, - "reportListEmptyFiltered": "No earthquake reports match these filters", - "@reportListEmptyFiltered": { - "description": "Empty state when active filters yield no report rows" + "weatherRankingExtremeRange": "Diurnal range", + "@moreSectionRegion": { + "description": "Section header on the More page for saved regions" }, - "reportListMeta": "M{magnitude} · {depth} km", - "@reportListMeta": { - "description": "Magnitude and depth line on a report list row", + "notifySettingsMenu": "Notification settings", + "@weatherRankingMergeCounty": { + "description": "Chip to keep one extreme station per county" + }, + "typhoonHistoryTitle": "Dataset time", + "mapAppDefault": "{app} (default)", + "trendRange24h": "24h", + "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", + "weatherRankingRecordedAt": "Recorded at {time}", + "mapLayerRain": "Rainfall", + "@typhoonPickerNamed": { + "description": "Sheet picker: named typhoon (CWA name + TY tyNo)", "placeholders": { - "magnitude": { + "no": { "type": "String" }, - "depth": { + "name": { "type": "String" } } }, - "reportListMagnitude": "M{magnitude}", - "@reportListMagnitude": { - "description": "Emphasized magnitude on a report list row", - "placeholders": { - "magnitude": { - "type": "String" - } - } + "mapLayerQpesums": "1h Precipitation Forecast", + "@notifyOptWeatherLocal": { + "description": "Notify option label" }, - "reportListDepthUnit": "km", - "@reportListDepthUnit": { - "description": "Depth unit label beside the depth value on a report list row" + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." }, - "reportListLocalFelt": "Local felt", - "@reportListLocalFelt": { - "description": "Label for …000 serial reports (small-area felt quake, no CWA number)" + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" }, - "reportListToday": "Today", - "@reportListToday": { - "description": "Date section header for reports that originated today (Taipei)" + "mapOverlaySectionMap": "Map", + "mapTerrainRelief": "Terrain relief", + "eewMaxIntensity": "Max intensity", + "mapLegendCollapse": "Hide legend", + "@typhoonOverlayMenuTooltip": { + "description": "Tooltip for the typhoon overlay-toggle chip beside the layer switcher" }, - "reportListYesterday": "Yesterday", - "@reportListYesterday": { - "description": "Date section header for reports that originated yesterday (Taipei)" + "changelogTitle": "Changelog", + "reportFilterOrderDesc": "Descending", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", + "reportFilterIntensityInfoTitle": "Intensity scales", + "@moreCwaEew": { + "description": "More-menu link to the CWA earthquake early warning publication log website" }, - "reportListDayCount": "{count}", - "@reportListDayCount": { - "description": "Number of reports in a day section", + "mapLayerTyphoon": "Typhoon", + "@homeRainTrendHeavySustained": { + "description": "Home rain trend subtitle: heavy rain that keeps up through the hour" + }, + "radarOverlayMenuTooltip": "Radar overlay options", + "@navData": { + "description": "Bottom-nav label and page title for the Data hub tab" + }, + "mapMyLocation": "My location", + "@notifySectionEarthquake": { + "description": "Notify page section header" + }, + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "@typhoonOverlayForecastCallouts": { + "description": "Overlay menu: toggle forecast-point Flutter callout cards" + }, + "meshtasticNodes": "Nodes", + "@mapTownLabelsHint": { + "description": "Hint under the township-names setting" + }, + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", + "@mapLayerSatelliteB08": { + "description": "Himawari upper-level water-vapour channel (B08, 6.2 µm) layer name" + }, + "@typhoonLabelWind": { + "description": "Bulletin table row label" + }, + "aedType": "Type", + "@language": { + "description": "Language picker tooltip / label" + }, + "@mapLayerSatelliteBtdCo2": { + "description": "Himawari cirrus / cloud-height brightness-temperature-difference layer name" + }, + "termsOfService": "Terms of Service", + "@reportDetailImage": { + "description": "Section header over the CWA-rendered report image" + }, + "typhoonLegendCircle25": "Storm circle (L10)", + "sponsorTitle": "Support DPIP", + "mapNavSatellite": "Satellite", + "@notifyEvacuation": { + "description": "Notify channel title" + }, + "@changelogTitle": { + "description": "More-menu entry and page title for GitHub release notes" + }, + "homeRainTrendUpdated": "Updated {time}", + "@chartHourLabel": { + "description": "Compact chart X-axis hour tick (e.g. 20h / 20時)", "placeholders": { - "count": { + "hour": { "type": "int" } } }, - "reportListEnd": "End of list", - "@reportListEnd": { - "description": "Footer when the report catalogue has no further pages" + "onboardingNext": "Next", + "weatherRankingMergeTown": "Township", + "@typhoonOverlayWeatherNoneTooltip": { + "description": "Tooltip for clearing the weather underlay" }, - "reportFilterTitle": "Filters", - "@reportFilterTitle": { - "description": "Title of the earthquake report filter sheet" + "mapLayerMonitor": "Seismic Monitor", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "Subscriptions", + "@homeForecastWind": { + "description": "Wind direction string and Beaufort force for the selected hour", + "placeholders": { + "direction": { + "type": "String" + }, + "level": { + "type": "String" + } + } }, - "reportFilterSort": "Sort", - "@reportFilterSort": { - "description": "Section title for report list sort field + order" + "@typhoonLegendCircleAvg": { + "description": "Legend for the purple dashed mean-radius storm circle" }, - "reportFilterSortTime": "Time", - "@reportFilterSortTime": { - "description": "Sort reports by origin time" + "typhoonValueLon": "{lon}°E", + "skyTime": "Sky time", + "weatherModeCloudy": "Cloudy", + "@weatherRankingAnalysisLow": { + "description": "Daily low fragment; value may include clock time", + "placeholders": { + "value": { + "type": "String" + } + } }, - "reportFilterSortIntensity": "Intensity", - "@reportFilterSortIntensity": { - "description": "Sort reports by max intensity" + "skyTimeDusk": "Dusk", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" }, - "reportFilterSortMagnitude": "Magnitude", - "@reportFilterSortMagnitude": { - "description": "Sort reports by magnitude" + "@typhoonIntensityIntense": { + "description": "CWA class: intense typhoon (past-track colour)" }, - "reportFilterSortDepth": "Depth", - "@reportFilterSortDepth": { - "description": "Sort reports by hypocentral depth" + "@onboardingIntroTitle": { + "description": "Onboarding intro page title" }, - "reportFilterOrderDesc": "Descending", - "@reportFilterOrderDesc": { - "description": "Sort order: newest / largest first" + "@moreNotifyLog": { + "description": "More-menu link to the DPIP notification send-record website" }, - "reportFilterOrderAsc": "Ascending", "@reportFilterOrderAsc": { "description": "Sort order: oldest / smallest first" }, - "reportFilterIntensity": "Intensity", - "@reportFilterIntensity": { - "description": "Label for the felt-intensity range filter" + "@notifyUnavailable": { + "description": "Shown on the notify page when there is no push token yet" }, - "reportFilterIntensityInfoTitle": "Intensity scales", - "@reportFilterIntensityInfoTitle": { - "description": "Title of the dialog explaining CWA 新制 vs 舊制 intensity" + "@aedOpenRemark": { + "description": "AED opening-hours remark row label" }, - "reportFilterIntensityInfoIntro": "CWA changed the felt-intensity scale on 1 Jan 2020 (Taipei time).", - "@reportFilterIntensityInfoIntro": { - "description": "Intro paragraph for the intensity-scale info dialog" + "@themeDark": { + "description": "Theme option: always dark" }, - "reportFilterIntensityInfoLegacyTitle": "Legacy (before 2020)", - "reportFilterIntensityInfoLegacyBody": "Only levels 0–7. No 5− / 5+ / 6− / 6+ split.", - "reportFilterIntensityInfoModernTitle": "Current (from 2020)", - "reportFilterIntensityInfoModernBody": "Levels 0–4, 5−, 5+, 6−, 6+, and 7. The filter slider uses this scale; older events still show legacy labels in the list.", - "reportFilterMagnitude": "Magnitude", - "@reportFilterMagnitude": { - "description": "Label for the magnitude range filter" + "@reportDetailImageUnavailable": { + "description": "Shown in place of the report image when it fails to load" }, - "reportFilterDepth": "Depth", - "@reportFilterDepth": { - "description": "Label for the hypocentral-depth range filter" + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" }, - "reportFilterDepthKm": "{depth} km", - "@reportFilterDepthKm": { - "description": "Depth value with unit in the filter sheet", - "placeholders": { - "depth": { - "type": "String" - } - } + "reportFilterDateEndNote": "End day: through 24:00 (Taipei)", + "@notifySectionWeather": { + "description": "Notify page section header" }, - "reportFilterDate": "Date", - "@reportFilterDate": { - "description": "Label for the origin-time date-range filter" + "@sponsorRestore": { + "description": "Footer action that restores previously bought purchases" }, - "reportFilterDatePick": "Pick dates", - "@reportFilterDatePick": { - "description": "Button to open the date-range picker when none selected" + "@weatherRankingEmpty": { + "description": "Empty state when a ranking list has no rows after filters" }, - "reportFilterDateStartNote": "Start day: from 00:00 (Taipei)", - "@reportFilterDateStartNote": { - "description": "Explains that startTime covers from midnight on that calendar day" + "@weatherRankingMergeTown": { + "description": "Chip to keep one extreme station per township" }, - "reportFilterDateEndNote": "End day: through 24:00 (Taipei)", - "@reportFilterDateEndNote": { - "description": "Explains that endTime covers through the end of that calendar day" + "reportFilterSortMagnitude": "Magnitude", + "@mapLayerCategoryLife": { + "description": "Section title in map overlay lists: everyday-life facility overlays" }, - "reportFilterRange": "{start} – {end}", - "@reportFilterRange": { - "description": "Displays a selected filter range (intensity, magnitude, depth, or dates)", + "@typhoonStormRadii": { + "description": "Per-quadrant storm-wind radii (km) for a typhoon circle", "placeholders": { - "start": { + "ne": { "type": "String" }, - "end": { + "se": { + "type": "String" + }, + "sw": { + "type": "String" + }, + "nw": { "type": "String" } } }, - "reportFilterLocation": "Location", + "meshtasticSilent": "Silent", + "mapLayerCategoryEarthquake": "Earthquake", + "mapLayerSatelliteB12": "Himawari Ozone (B12)", + "typhoonLegendPast": "Observed track", "@reportFilterLocation": { "description": "Label for the location keyword filter field" }, - "reportFilterLocationHint": "e.g. Hualien, offshore", - "@reportFilterLocationHint": { - "description": "Hint for the location keyword filter field" - }, - "reportFilterAny": "Any", - "@reportFilterAny": { - "description": "Chip / slider label meaning no filter applied" - }, - "reportFilterApply": "Apply", - "@reportFilterApply": { - "description": "Primary button on the report filter sheet — saves draft and searches" + "restroomCategoryOther": "Other", + "@typhoonLegendWarningAreas": { + "description": "Typhoon UI: typhoonLegendWarningAreas" }, - "reportFilterReset": "Reset", - "@reportFilterReset": { - "description": "Clears all filters in the report filter sheet" + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" }, - "reportListSearch": "Search", - "@reportListSearch": { - "description": "Fetches the report list with the current draft filters" + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." }, - "reportDetailTitle": "Earthquake Report", - "@reportDetailTitle": { - "description": "Header title over the report detail map's back button" + "@aedAddress": { + "description": "AED detail row label" }, - "reportDetailNumbered": "No. {number} Significant Earthquake", - "@reportDetailNumbered": { - "description": "Eyebrow label on the detail header for a numbered CWA report", + "homeForecastHighLow": "H {high}° · L {low}°", + "@regionSelectFull": { + "description": "Snackbar shown when trying to add a region beyond the cap", "placeholders": { - "number": { - "type": "String" + "max": { + "type": "int" } } }, - "reportDetailLocalFelt": "Local Felt Earthquake", - "@reportDetailLocalFelt": { - "description": "Eyebrow label on the detail header for a …000 (unnumbered) report" + "locationBannerFix": "Open settings", + "@typhoonLabelDirection": { + "description": "Bulletin table row label" }, - "reportDetailInfo": "Details", - "@reportDetailInfo": { - "description": "Section header over origin time / epicenter / magnitude / depth" + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" }, - "reportDetailOriginTime": "Origin time", - "@reportDetailOriginTime": { - "description": "Row label for the report's origin date/time" + "mapLegendExpand": "Legend", + "eewNone": "No active earthquake early warning", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "Tsunami advisories and warnings", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." }, - "reportDetailEpicenter": "Epicenter", - "@reportDetailEpicenter": { - "description": "Row label for the epicenter's latitude/longitude" - }, - "reportDetailMagnitude": "Magnitude", - "@reportDetailMagnitude": { - "description": "Row label for the report's magnitude" - }, - "reportDetailDepth": "Depth", - "@reportDetailDepth": { - "description": "Row label for the report's hypocentral depth" + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." }, - "reportDetailAreaIntensity": "Intensity by area", - "@reportDetailAreaIntensity": { - "description": "Section header over the per-area/town felt-intensity breakdown" + "@sponsorPerMonth": { + "description": "Monthly price label for a subscription; price is the store-localized amount", + "placeholders": { + "price": { + "type": "String", + "example": "NT$75" + } + } }, - "reportDetailLocalIntensity": "Intensity at your locations", - "@reportDetailLocalIntensity": { - "description": "Section header over the per-location (GPS + saved townships) felt-intensity readout, shown above the area breakdown" + "@shelterOutdoorLabel": { + "description": "Shelter detail row: whether outdoor shelter is provided" }, - "reportDetailLocalIntensityUnavailable": "No intensity data", - "@reportDetailLocalIntensityUnavailable": { - "description": "Shown in place of an intensity badge when a location's county isn't in this report's felt-area list at all" + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." }, - "reportDetailSortByIntensity": "Sort by intensity", - "@reportDetailSortByIntensity": { - "description": "Tooltip on the area-intensity sort toggle when tapping it switches to grouping by intensity level" + "meshtasticLayerOptions": "Node options", + "@weatherRankingLowest": { + "description": "Chip to rank temperature ascending" }, - "reportDetailSortByCounty": "Sort by county", - "@reportDetailSortByCounty": { - "description": "Tooltip on the area-intensity sort toggle when tapping it switches to an alphabetical county list" + "onboardingAgreeContinue": "Agree and continue", + "commonRetry": "Retry", + "@restroomGradePoor": { + "description": "Restroom cleanliness grade: below standard" }, - "reportDetailImage": "Report image", - "@reportDetailImage": { - "description": "Section header over the CWA-rendered report image" + "meshtasticNodeId": "Node ID", + "reportDetailNumbered": "No. {number} Significant Earthquake", + "@dataWeatherRankingSubtitle": { + "description": "Subtitle under weather ranking tiles on the Data hub" }, - "reportDetailImageUnavailable": "Report image not available", - "@reportDetailImageUnavailable": { - "description": "Shown in place of the report image when it fails to load" + "typhoonOverlayStormBandSubtitle": "With average circle", + "@onboardingNext": { + "description": "Onboarding next-step button" }, - "reportDetailOpenReport": "Report page", - "@reportDetailOpenReport": { - "description": "Button that opens the official CWA report page in a browser" + "@homeForecastUnavailable": { + "description": "Shown when no township code is available for the forecast API" }, - "reportDetailReplay": "Replay", - "@reportDetailReplay": { - "description": "Button that opens the RTS/EEW replay starting from this report's origin time" + "disasterMapOverlayRestroomTooltip": "Show public restrooms", + "weatherRankingTitle": "Observation rankings", + "homeRainTrendHeavySustained": "Heavy rain continuing for the next hour", + "notifySectionTsunami": "Tsunami", + "@mapNavSatellite": { + "description": "Short Map-tab bottom-nav / default-layer picker label for satellite" }, - "navMore": "More", - "@navMore": { - "description": "Bottom-nav label and page title for the More tab" + "restroomCategoryPark": "Park", + "@typhoonLegendCircle25": { + "description": "Typhoon UI: typhoonLegendCircle25" }, - "appLogs": "App logs", - "@appLogs": { - "description": "Title of the in-app log viewer and its entry in the More menu" + "moreLinkOpenFailed": "Couldn't open the link", + "@mapNavWind": { + "description": "Short Map-tab bottom-nav / default-layer picker label for wind" }, - "changelogTitle": "Changelog", - "@changelogTitle": { - "description": "More-menu entry and page title for GitHub release notes" + "@typhoonValueKm": { + "placeholders": { + "n": { + "type": "String" + } + } }, - "changelogEmpty": "No release notes yet", - "@changelogEmpty": { - "description": "Empty state when the releases API returns nothing" + "@reportDetailDepth": { + "description": "Row label for the report's hypocentral depth" }, - "changelogTypePrerelease": "Beta", - "@changelogTypePrerelease": { - "description": "Chip label for a pre-release" + "themeDark": "Dark", + "sponsorRestore": "Restore purchases", + "@notifySettingsMenu": { + "description": "More-menu entry that opens the notification-settings page" }, - "changelogTypeStable": "Stable", - "@changelogTypeStable": { - "description": "Chip label for a stable release" + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" }, - "changelogCurrentVersion": "Current", - "@changelogCurrentVersion": { - "description": "Chip/badge when a release matches the installed app version" + "@reportFilterSortTime": { + "description": "Sort reports by origin time" }, - "changelogVersionDetails": "Release details", - "@changelogVersionDetails": { - "description": "App bar title on a single release's detail page" + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@reportDetailLocalFelt": { + "description": "Eyebrow label on the detail header for a …000 (unnumbered) report" }, - "changelogBodyEmpty": "No notes for this release.", - "@changelogBodyEmpty": { - "description": "Placeholder when a GitHub release has an empty markdown body" + "@typhoonHistoryTitle": { + "description": "Typhoon UI: typhoonHistoryTitle" }, - "mapPlaceholderDisabled": "Map (temporarily disabled)", - "@mapPlaceholderDisabled": { - "description": "Placeholder shown in place of the map while MapLibre is disabled" + "@mapLayerSatelliteCloudmask": { + "description": "Himawari cloud-mask (Level-2 retrieval) layer name" }, - "moreSectionRegion": "Region", - "@moreSectionRegion": { - "description": "Section header on the More page for saved regions" + "@meshtasticLastReceived": { + "description": "Age of the last received packet" }, - "moreSectionNotify": "Notifications", - "@moreSectionNotify": { - "description": "Section header on the More page for notification settings" + "meshtasticTraffic": "Traffic", + "@navEvents": { + "description": "Bottom-nav label and page title for the Events tab" }, - "moreSectionDisplay": "Display", - "@moreSectionDisplay": { - "description": "Section header on the More page for language and theme" + "@typhoonForecastLead": { + "description": "Forecast lead time for a tapped track point", + "placeholders": { + "hours": { + "type": "String" + } + } }, - "regionManageTitle": "Saved regions", - "regionAddButton": "Add a region", - "regionEmpty": "No saved regions yet", - "@regionAddButton": { - "description": "Button to open the region picker to add a saved region" + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" }, - "@regionEmpty": { - "description": "Empty state on the saved-regions manage page" + "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", + "@onboardingTermsBody": { + "description": "Onboarding terms of service body" }, - "@regionManageTitle": { - "description": "More-menu entry that opens the region picker" + "@onboardingGrant": { + "description": "Permission grant button" }, - "regionSelectTitle": "Select a region", - "@regionSelectTitle": { - "description": "Title of the region picker (city list) page" + "disasterMapOverlayAedTooltip": "Show AED locations", + "@moonTitle": { + "description": "Moon page title" }, - "regionSelectCount": "{count}/{max} selected", - "@regionSelectCount": { - "description": "Header showing how many saved-region slots are used", + "mapLayerHumidity": "Humidity", + "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", + "@homeRainTrendMinute": { + "description": "X-axis tick label on the home rain trend chart, minutes from now", "placeholders": { - "count": { - "type": "int" - }, - "max": { + "minute": { "type": "int" } } }, - "regionSelectFull": "You can save up to {max} regions", - "@regionSelectFull": { - "description": "Snackbar shown when trying to add a region beyond the cap", + "meshtasticScanning": "Scanning…", + "@weatherRankingAnalysisHigh": { + "description": "Daily high fragment; value may include clock time", "placeholders": { - "max": { - "type": "int" + "value": { + "type": "String" } } }, - "regionEdit": "Edit", - "@regionEdit": { - "description": "Edit action on a saved-region bottom sheet" - }, - "moreSectionAdvanced": "Advanced", - "@moreSectionAdvanced": { - "description": "Section header on the More page grouping advanced/developer entries" - }, - "moreDeveloper": "Debug info", - "@moreDeveloper": { - "description": "More-menu entry / title for the developer diagnostics page" + "@dpmDisasterFlood": { + "description": "Shelter disaster-type filter chip: flood" }, - "experimentalFeatures": "Experimental features", - "@experimentalFeatures": { - "description": "Title of the experimental-features settings page and its More-menu entry" + "@meshtasticDevice": { + "description": "Section: device identity" }, - "moreSectionLinks": "Links", + "regionSelectFull": "You can save up to {max} regions", + "meshtasticTitle": "Meshtastic", "@moreSectionLinks": { "description": "Section header on the More page grouping external website links" }, - "moreCwaEew": "CWA earthquake early warning", - "@moreCwaEew": { - "description": "More-menu link to the CWA earthquake early warning publication log website" + "navMore": "More", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "Layers", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" }, - "moreTremReport": "TREM detection report", - "@moreTremReport": { - "description": "More-menu link to the TREM detection report website" + "@reportFilterReset": { + "description": "Clears all filters in the report filter sheet" }, - "moreServerStatus": "Server status", - "@moreServerStatus": { - "description": "More-menu link to the ExpTech server status website" + "mapLayerSatelliteB05": "Himawari Near-Infrared (B05)", + "@mapLayerMonitor": { + "description": "Map layer switcher label for the real-time seismic monitor (RTS)" }, - "moreAnnouncements": "Announcements", - "@moreAnnouncements": { - "description": "More-menu link to the ExpTech announcements website" + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" }, - "moreDiscord": "Discord community", - "@moreDiscord": { - "description": "More-menu link to the ExpTech Discord community" + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." }, - "moreNotifyLog": "DPIP notification log", - "@moreNotifyLog": { - "description": "More-menu link to the DPIP notification send-record website" + "@mapTerrainReliefHint": { + "description": "Hint under the terrain-relief setting" }, - "moreLinkOpenFailed": "Couldn't open the link", - "@moreLinkOpenFailed": { - "description": "Snackbar shown when an external link fails to open in the browser" + "@homeRainTrendLightStopping": { + "description": "Home rain trend subtitle: light rain forecast to stop partway through the hour", + "placeholders": { + "minutes": { + "type": "int" + } + } }, - "weatherDynamicState": "Weather animation", - "@weatherDynamicState": { - "description": "Setting that forces the home weather backdrop to a fixed state" + "@notifyOptLocalIntensity4": { + "description": "Notify option label" }, - "weatherDynamicStateSubtitle": "Override the home backdrop weather", - "@weatherDynamicStateSubtitle": { - "description": "Subtitle explaining the weather animation setting" + "typhoonLabelNe": "NE", + "meshtasticCopied": "Message copied", + "@mapLayerSatelliteB05": { + "description": "Himawari near-infrared channel (B05, 1.6 µm) layer name" }, - "weatherModeAuto": "Auto", - "@weatherModeAuto": { - "description": "Weather animation follows real conditions" + "reportListEmpty": "No earthquake reports", + "reportListEnd": "End of list", + "mapLayerSatelliteTruecolor": "Himawari True Color", + "typhoonOverlaySectionExtra": "Overlays", + "@moreSectionAdvanced": { + "description": "Section header on the More page grouping advanced/developer entries" }, - "weatherModeClear": "Clear", - "@weatherModeClear": { - "description": "Weather animation forced to a clear sky" + "eewSWave": "S-wave", + "meshtasticBusyTitle": "Another app is using this radio", + "@restroomCategoryCommercial": { + "description": "Restroom venue category: commercial establishment" }, - "weatherModeRain": "Rain", - "@weatherModeRain": { - "description": "Weather animation forced to rain" + "restroomCategoryCultural": "Cultural", + "typhoonLabelWind": "Max. sustained wind near centre", + "@reportFilterLocationHint": { + "description": "Hint for the location keyword filter field" }, - "weatherModeFog": "Fog", - "@weatherModeFog": { - "description": "Weather animation forced to heavy fog" + "radarGlobalOutlineHint": "Every country's outer frame", + "notifyEvacuation": "Disaster information", + "typhoonLegendCircle15": "Gale circle (L7)", + "@mapLayerSatelliteRgbComposite": { + "description": "Satellite legend note for the RGB-recipe products (True Color, Ash, …) that carry no single numerical scale" }, - "weatherModeThunderstorm": "Thunderstorm", - "@weatherModeThunderstorm": { - "description": "Weather animation forced to a thunderstorm" + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." }, - "commonLoading": "Loading…", - "@commonLoading": { - "description": "Generic loading label for an async view" + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" }, - "commonRetry": "Retry", - "@commonRetry": { - "description": "Button that re-runs a failed request" + "dataSectionAstronomy": "Astronomy", + "@mapLayerStyleGray": { + "description": "Colour-style option: JMA grayscale, the default radar-image convention" }, - "commonError": "Something went wrong", - "commonFetchFailed": "Couldn't load data. Please try again.", - "@commonFetchFailed": { - "description": "Error headline when a data request (AsyncView) fails, with a retry button" + "@mapLayerStyleTooltip": { + "description": "Tooltip of the colour-style chip beside the layer switcher" }, - "@commonError": { - "description": "Generic headline when an async request fails" + "homeRainTrendLightSustained": "Light rain continuing for the next hour", + "commonError": "Something went wrong", + "@notifyTitle": { + "description": "Title of the notification-settings page" }, - "commonEmpty": "Nothing to show", - "@commonEmpty": { - "description": "Generic message when a loaded list is empty" + "@typhoonOverlayWeatherNone": { + "description": "No radar or satellite underlay" }, - "feedConnecting": "Connecting…", - "@feedConnecting": { - "description": "A realtime feed is establishing its first data" + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" }, - "feedStale": "Data may be out of date", - "@feedStale": { - "description": "Banner over a realtime feed whose data has aged past the freshness threshold" + "mapTimelineNow": "Now", + "reportFilterRange": "{start} – {end}", + "@reportListLocalFelt": { + "description": "Label for …000 serial reports (small-area felt quake, no CWA number)" }, - "feedOffline": "Connection lost", - "@feedOffline": { - "description": "Banner/headline when a realtime feed has gone offline" + "reportDetailOpenReport": "Report page", + "@weatherModeAuto": { + "description": "Weather animation follows real conditions" }, - "eewTitle": "Earthquake early warning", - "@eewTitle": { - "description": "Header of the earthquake monitor when one or more alerts are active" + "trendRange7d": "7d", + "@changelogEmpty": { + "description": "Empty state when the releases API returns nothing" }, - "eewNone": "No active earthquake early warning", - "@eewNone": { - "description": "Calm state of the earthquake monitor when the live feed reports no alert" + "@notifyOptTsunamiWarning": { + "description": "Notify option label" }, - "eewSummary": "M{magnitude} · depth {depth} km", - "@eewSummary": { - "description": "One-line summary of an EEW alert's magnitude and depth", + "typhoonWarningAreas": "Areas: {areas}", + "rainIntervalSection": "Time window", + "@reportDetailNumbered": { + "description": "Eyebrow label on the detail header for a numbered CWA report", "placeholders": { - "magnitude": { - "type": "String" - }, - "depth": { + "number": { "type": "String" } } }, - "regionNationwide": "Nationwide", - "@regionNationwide": { - "description": "Region bar label for the whole-country view" - }, - "regionCurrent": "Current location", - "@regionCurrent": { - "description": "Region bar label for the current GPS township" - }, - "regionCurrentUnavailable": "Can't get current location", - "@regionCurrentUnavailable": { - "description": "Shown when the current-location area is selected but GPS is off/unavailable" + "@mapLayerSatelliteCloudtop": { + "description": "Himawari cloud-top-temperature (Level-2 retrieval) layer name" }, - "weatherPrecipitation": "Precipitation", - "@weatherPrecipitation": { - "description": "Label for the precipitation metric in the home weather header" + "notifyTitle": "Notifications", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." }, - "weatherHumidity": "Humidity", - "@weatherHumidity": { - "description": "Label for the humidity metric in the home weather header" + "@notifyReport": { + "description": "Notify channel title" }, - "weatherDataTime": "{station} · Data {time}", - "@weatherDataTime": { - "description": "Nearest-station name and observation time shown as small text under the home weather header name", - "placeholders": { - "station": { - "type": "String" - }, - "time": { - "type": "String" - } - } + "@notifyEew": { + "description": "Notify channel title" }, - "homeViewOnMap": "View on map", - "@homeViewOnMap": { - "description": "Small home-header link that opens the map tab on the temperature layer at the nearest station" + "@navHome": { + "description": "Bottom-nav label and page title for the Home tab" }, - "homeForecastTitle": "24-hour forecast", - "@homeForecastTitle": { - "description": "Section title for the home sheet township hourly forecast" + "@mapLayerWindForecastGfs": { + "description": "Map layer switcher label for the GFS wind-forecast layer" }, - "homeForecastHighLow": "H {high}° · L {low}°", - "@homeForecastHighLow": { - "description": "24h forecast series high and low air temperatures", - "placeholders": { - "high": { - "type": "String" - }, - "low": { - "type": "String" - } - } + "restroomCategoryLabel": "Category", + "sponsorRestoring": "Restoring purchases…", + "sponsorIntro": "DPIP is dedicated to real-time disaster-prevention information, with no ads or other revenue model. Your support helps us keep the servers running and keep developing.", + "@mapLayerStyleGrayTooltip": { + "description": "Explains the JMA grayscale band rendering" }, - "homeForecastPop": "{pop}%", - "@homeForecastPop": { - "description": "Probability of precipitation percent on a forecast hour chip", - "placeholders": { - "pop": { - "type": "String" - } - } + "shelterAddressLabel": "Address", + "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", + "@meshtasticHardware": { + "description": "Board model" }, - "homeForecastFeelsLike": "Feels like {temp}°", - "@homeForecastFeelsLike": { - "description": "Apparent temperature for the selected forecast hour", - "placeholders": { - "temp": { - "type": "String" - } - } + "restroomCategoryCommercial": "Commercial", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" }, - "homeForecastHumidity": "Humidity {value}%", - "@homeForecastHumidity": { - "description": "Relative humidity for the selected forecast hour", - "placeholders": { - "value": { - "type": "String" - } - } + "@restroomTypeFamily": { + "description": "Restroom type: family restroom" }, - "homeForecastWind": "{direction} · Force {level}", - "@homeForecastWind": { - "description": "Wind direction string and Beaufort force for the selected hour", - "placeholders": { - "direction": { - "type": "String" - }, - "level": { - "type": "String" - } - } + "@mapLayerSatelliteBtdSplit": { + "description": "Himawari split-window brightness-temperature-difference layer name" }, - "homeForecastUnavailable": "Select a township to see the forecast", - "@homeForecastUnavailable": { - "description": "Shown when no township code is available for the forecast API" + "aedRegion": "Region", + "@dpmFilterSectionRestroom": { + "description": "Filter section title in the disaster-map sheet: restroom venue categories" }, - "homeForecastEmpty": "No forecast available", - "@homeForecastEmpty": { - "description": "Empty or failed forecast on the home sheet" + "homeRainTrendLightStopping": "Light rain likely to stop in {minutes} minutes", + "@navMap": { + "description": "Bottom-nav label and page title for the Map tab" }, - "homeActiveEventsTitle": "Active events", - "@homeActiveEventsTitle": { - "description": "Section title for currently active disaster notices on the collapsed home sheet" + "reportDetailInfo": "Details", + "mapNavWind": "Wind", + "@meshtasticReceived": { + "description": "Packets received this session" }, - "homeActiveEventsEmpty": "No active events", - "@homeActiveEventsEmpty": { - "description": "Empty state when the realtime event feed has nothing in effect" + "@onboardingTermsAgree": { + "description": "Terms agreement checkbox label" }, - "homeRainTrendTitle": "Next hour precipitation", - "@homeRainTrendTitle": { - "description": "Section title for the home sheet 1-hour per-minute rainfall bar chart" + "windForecastOverlayMenuTooltip": "Wind forecast overlay options", + "@mapLayerCategoryTyphoon": { + "description": "Section title in map overlay lists: typhoon overlays" }, - "homeRainTrendMinute": "{minute} min", - "@homeRainTrendMinute": { - "description": "X-axis tick label on the home rain trend chart, minutes from now", + "@reportListDayCount": { + "description": "Number of reports in a day section", "placeholders": { - "minute": { + "count": { "type": "int" } } }, - "homeRainTrendUpdated": "Updated {time}", - "@homeRainTrendUpdated": { - "description": "Data-update time beside the home rain trend title, Taipei wall clock HH:mm", - "placeholders": { - "time": { - "type": "String" - } - } - }, - "homeRainTrendNoData": "No data", - "@homeRainTrendNoData": { - "description": "Label on the home rain trend chart for minutes beyond the forecast window, and the empty-card hint" + "@sponsorRestoring": { + "description": "Snackbar shown when a purchase restore has been requested" }, - - "homeRainTrendScattered": "Light showers possible", - "@homeRainTrendScattered": { - "description": "Home rain trend subtitle: peak intensity below the light-rain threshold" + "@onboardingStart": { + "description": "Onboarding finish button" }, - "homeRainTrendLightSustained": "Light rain continuing for the next hour", - "@homeRainTrendLightSustained": { - "description": "Home rain trend subtitle: light rain that keeps up through the hour" + "dataWeatherRankingSubtitle": "Live station rankings", + "@mapPlaceholderDisabled": { + "description": "Placeholder shown in place of the map while MapLibre is disabled" }, - "homeRainTrendLightStopping": "Light rain likely to stop in {minutes} minutes", - "@homeRainTrendLightStopping": { - "description": "Home rain trend subtitle: light rain forecast to stop partway through the hour", - "placeholders": { - "minutes": { - "type": "int" - } - } + "homeRainTrendMinute": "{minute} min", + "rainInterval6h": "6 h", + "restroomTypeUnspecified": "Unspecified", + "typhoonOverlayProbabilityHint": "Hides the forecast cone", + "@commonRetry": { + "description": "Button that re-runs a failed request" }, - "homeRainTrendHeavySustained": "Heavy rain continuing for the next hour", - "@homeRainTrendHeavySustained": { - "description": "Home rain trend subtitle: heavy rain that keeps up through the hour" + "mapLayerSatelliteGlobalOutline": "Country border", + "mapNavTemperature": "Temperature", + "typhoonLegendForecastPoint": "Forecast point", + "@disasterMapOverlayAedTooltip": { + "description": "Tooltip for the AED toggle in the disaster-map overlay menu" }, - "homeRainTrendHeavyStopping": "Heavy rain likely to stop in {minutes} minutes", - "@homeRainTrendHeavyStopping": { - "description": "Home rain trend subtitle: heavy rain forecast to stop partway through the hour", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "@lightningLegendCc": { + "description": "Lightning legend: cloud-to-cloud strike within N minutes", "placeholders": { "minutes": { "type": "int" } } }, - "mapLayers": "Layers", - "@mapLayers": { - "description": "Title of the map layer-picker sheet" + "reportListYesterday": "Yesterday", + "@mapLayerCategoryRadar": { + "description": "Section title in map overlay lists: radar and precipitation-forecast overlays" }, - "mapLayerOrderTitle": "Reorder layers", - "@mapLayerOrderTitle": { - "description": "Title of the layer-order editor, also the tooltip of the reorder button in the layer picker" + "moreSectionLinks": "Links", + "feedOffline": "Connection lost", + "mapLayerStyleBd": "Dvorak BD", + "@mapLayerSatelliteB09": { + "description": "Himawari mid-level water-vapour channel (B09, 6.9 µm) layer name" }, - "mapLayerOrderReset": "Reset order", - "@mapLayerOrderReset": { - "description": "Button that restores the layer picker's default order" + "@restroomGradeLabel": { + "description": "Restroom detail row label for the cleanliness grade" }, - "mapLayerRadar": "Composite Radar Reflectivity", - "@mapLayerRadar": { - "description": "Name of the composite radar reflectivity layer in the layer picker" + "@moreServerStatus": { + "description": "More-menu link to the ExpTech server status website" }, - "mapLayerSatellite": "Himawari Infrared (B13)", - "@mapLayerSatellite": { - "description": "Name of the Himawari infrared layer in the layer picker" + "@disasterMapOverlaySectionLayers": { + "description": "Section header for DPM sub-layer toggles in the overlay menu" }, - "mapLayerSatelliteB01": "Himawari Blue (B01)", - "@mapLayerSatelliteB01": { - "description": "Himawari visible-blue channel (B01, 0.47 µm) layer name" + "moreSectionDisplay": "Display", + "rainInterval3d": "3 d", + "defaultMapLayerSubtitle": "The Map tab opens on this overlay. The bottom-navigation icon and label follow this choice.", + "aedDescription": "Notes", + "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", + "onboardingPermLocationDesc": "Target alerts to where you are.", + "@typhoonLabelStormAvg": { + "description": "Bulletin table row label" }, - "mapLayerSatelliteB02": "Himawari Green (B02)", - "@mapLayerSatelliteB02": { - "description": "Himawari visible-green channel (B02, 0.51 µm) layer name" + "@reportFilterDate": { + "description": "Label for the origin-time date-range filter" }, - "mapLayerSatelliteB03": "Himawari Red (B03)", - "@mapLayerSatelliteB03": { - "description": "Himawari visible-red channel (B03, 0.64 µm) layer name" + "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" }, - "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", - "@mapLayerSatelliteB04": { - "description": "Himawari near-infrared channel (B04, 0.86 µm) layer name" + "homeActiveEventsEmpty": "No active events", + "@mapLayerSatelliteBtdWvirw": { + "description": "Himawari overshooting-cloud-top brightness-temperature-difference layer name" }, - "mapLayerSatelliteB05": "Himawari Near-Infrared (B05)", - "@mapLayerSatelliteB05": { - "description": "Himawari near-infrared channel (B05, 1.6 µm) layer name" + "typhoonLabelPosition": "Centre location", + "@eewArrived": { + "description": "S-wave arrival countdown state once the wave has arrived" }, - "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", - "@mapLayerSatelliteB06": { - "description": "Himawari near-infrared channel (B06, 2.3 µm) layer name" + "weatherRankingBy": "Sort by", + "typhoonIntensityMild": "Mild typhoon", + "windForecastGlobalOutlineHint": "Every country's outer frame", + "rainInterval1h": "1 h", + "eewLocalIntensity": "Estimated at my location", + "mapLayerRadar": "Composite Radar Reflectivity", + "@onboardingPermCriticalDesc": { + "description": "Permission row description: critical alerts" }, - "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", - "@mapLayerSatelliteB07": { - "description": "Himawari shortwave-infrared channel (B07, 3.9 µm) layer name" + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." }, - "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", - "@mapLayerSatelliteB08": { - "description": "Himawari upper-level water-vapour channel (B08, 6.2 µm) layer name" + "restroomCategoryReligious": "Religious", + "meshtasticRole": "Role", + "mapLayerSatelliteCloudCloudy": "Cloudy", + "@typhoonTimeChip": { + "description": "Compact typhoon time chip / map label shape (day + hour, no month)", + "placeholders": { + "day": { + "type": "String" + }, + "hour": { + "type": "String" + } + } }, - "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", - "@mapLayerSatelliteB09": { - "description": "Himawari mid-level water-vapour channel (B09, 6.9 µm) layer name" + "skyTimeSunrise": "Sunrise", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" }, - "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", - "@mapLayerSatelliteB10": { - "description": "Himawari lower-level water-vapour channel (B10, 7.3 µm) layer name" + "@stationSheetEmpty": { + "description": "Empty-state hint in the map station-value sheet, shown before any station is selected" }, - "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", - "@mapLayerSatelliteB11": { - "description": "Himawari SO₂ absorption channel (B11, 8.6 µm) layer name" + "meshtasticNoMessages": "No messages yet", + "@locationBannerPermission": { + "description": "Banner when location permission is denied" }, - "mapLayerSatelliteB12": "Himawari Ozone (B12)", - "@mapLayerSatelliteB12": { - "description": "Himawari ozone-band channel (B12, 9.6 µm) layer name" + "onboardingPermNotifyDesc": "Deliver earthquake, weather, and disaster alerts the moment they happen.", + "radarTownOutline": "Township borders", + "mapLayerStyleSection": "Colour style", + "@mapOverlaySectionMap": { + "description": "Section title in map overlay settings menus: base-map settings" }, - "mapLayerSatelliteB13": "Himawari Infrared (B13)", - "@mapLayerSatelliteB13": { - "description": "Himawari clean-infrared window channel (B13, 10.4 µm) layer name" + "@moonPhaseNew": { + "description": "Phase: new moon" }, - "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", - "@mapLayerSatelliteB14": { - "description": "Himawari longwave-infrared channel (B14, 11.2 µm) layer name" + "disasterMapOverlayMenuTooltip": "Disaster map layers", + "@notifyOptAll": { + "description": "Notify option label" }, - "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", - "@mapLayerSatelliteB15": { - "description": "Himawari longwave-infrared channel (B15, 12.4 µm) layer name" + "moreGooglePlay": "Google Play", + "@reportListDepthUnit": { + "description": "Depth unit label beside the depth value on a report list row" }, - "mapLayerSatelliteB16": "Himawari CO₂ (B16)", - "@mapLayerSatelliteB16": { - "description": "Himawari CO₂-band channel (B16, 13.3 µm) layer name" + "@onboardingPermsTitle": { + "description": "Onboarding permissions page title" }, - "mapLayerSatelliteTruecolor": "Himawari True Color", - "@mapLayerSatelliteTruecolor": { - "description": "Himawari True Color RGB composite layer name" + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" }, - "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", - "@mapLayerSatelliteNaturalcolor": { - "description": "Himawari Natural Color RGB composite layer name" + "@onboardingTermsTitle": { + "description": "Onboarding terms page title" }, - "mapLayerSatelliteAsh": "Himawari Ash", - "@mapLayerSatelliteAsh": { - "description": "Himawari Ash RGB composite layer name" + "@restroomCategoryLeisure": { + "description": "Restroom venue category: leisure / entertainment venue" }, - "mapLayerSatelliteDust": "Himawari Dust", - "@mapLayerSatelliteDust": { - "description": "Himawari Dust RGB composite layer name" + "@themeSystem": { + "description": "Theme option: follow the system light/dark setting" }, - "mapLayerSatelliteAirmass": "Himawari Airmass", - "@mapLayerSatelliteAirmass": { - "description": "Himawari Airmass RGB composite layer name" + "@commonLoading": { + "description": "Generic loading label for an async view" }, - "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", - "@mapLayerSatelliteNightmicrophysics": { - "description": "Himawari Night Microphysics RGB composite layer name" + "@mapLayerStyleSection": { + "description": "Section header of the satellite band colour-style menu on the map" }, - "mapLayerSatelliteWatervapor": "Himawari Water Vapour", - "@mapLayerSatelliteWatervapor": { - "description": "Himawari water-vapour layer name" + "typhoonLabelSw": "SW", + "typhoonForecastLead": "Forecast +{hours} h", + "dpmDisasterTsunami": "Tsunami", + "@regionNationwide": { + "description": "Region bar label for the whole-country view" }, - "mapLayerSatelliteBtdSplit": "Himawari Split Window", - "@mapLayerSatelliteBtdSplit": { - "description": "Himawari split-window brightness-temperature-difference layer name" + "@mapAppOpenFailed": { + "placeholders": { + "app": { + "type": "String" + } + }, + "description": "Snackbar when the chosen map app cannot be opened on this device" }, - "mapLayerSatelliteBtdFog": "Himawari Night Fog", - "@mapLayerSatelliteBtdFog": { - "description": "Himawari night fog / low-cloud brightness-temperature-difference layer name" + "@mapLayerCategoryForecast": { + "description": "Section title in map overlay lists: numerical weather prediction (ECMWF/GFS) wind-field overlays" }, - "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", - "@mapLayerSatelliteBtdWvirw": { - "description": "Himawari overshooting-cloud-top brightness-temperature-difference layer name" + "@disasterMapOverlayMenuTooltip": { + "description": "Tooltip on the disaster-map overlay tune button" }, - "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", - "@mapLayerSatelliteBtdSo2": { - "description": "Himawari SO₂ / cloud-phase brightness-temperature-difference layer name" + "@trendRange7d": { + "description": "Trend chart range toggle: last 7 days" }, - "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", - "@mapLayerSatelliteBtdCo2": { - "description": "Himawari cirrus / cloud-height brightness-temperature-difference layer name" + "@mapLayerSatelliteAsh": { + "description": "Himawari Ash RGB composite layer name" }, - "mapLayerSatelliteBtdOzone": "Himawari Tropopause", - "@mapLayerSatelliteBtdOzone": { - "description": "Himawari tropopause brightness-temperature-difference layer name" + "@onboardingSkipStay": { + "description": "Dismiss the skip dialog and return to grant permissions" }, - "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", - "@mapLayerSatelliteCloudtop": { - "description": "Himawari cloud-top-temperature (Level-2 retrieval) layer name" + "changelogTypeStable": "Stable", + "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "@reportListToday": { + "description": "Date section header for reports that originated today (Taipei)" }, - "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", - "@mapLayerSatelliteCloudmask": { - "description": "Himawari cloud-mask (Level-2 retrieval) layer name" + "@typhoonIntensityMild": { + "description": "CWA class: mild typhoon (past-track colour)" }, - "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", - "@mapLayerSatelliteSst": { - "description": "Himawari sea-surface-temperature (ACSPO L3C) layer name" + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." }, - "mapLayerSatelliteNdvi": "Himawari NDVI", - "@mapLayerSatelliteNdvi": { - "description": "Himawari normalised-difference vegetation-index layer name" - }, - "mapLayerSatelliteNdwi": "Himawari NDWI", - "@mapLayerSatelliteNdwi": { - "description": "Himawari normalised-difference water-index layer name" + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" }, - "mapLayerSatelliteMndwi": "Himawari MNDWI", - "@mapLayerSatelliteMndwi": { - "description": "Himawari modified normalised-difference water-index layer name" + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." }, - "mapLayerSatelliteGlobalOutline": "Country border", - "@mapLayerSatelliteGlobalOutline": { - "description": "Satellite legend row: the country/global border, drawn bright yellow over the imagery" + "mapOverlaySectionReference": "Reference layers", + "@reportFilterIntensityInfoTitle": { + "description": "Title of the dialog explaining CWA 新制 vs 舊制 intensity" }, - "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", - "@mapLayerSatelliteRgbComposite": { - "description": "Satellite legend note for the RGB-recipe products (True Color, Ash, …) that carry no single numerical scale" + "@mapLayerRadar": { + "description": "Name of the composite radar reflectivity layer in the layer picker" }, - "mapLayerSatelliteCloudClear": "Clear", - "@mapLayerSatelliteCloudClear": { - "description": "Cloud-mask category: clear sky, transparent on the map" + "mapLayerSatelliteB02": "Himawari Green (B02)", + "reportListLocalFelt": "Local felt", + "weatherRankingEmpty": "No observations to rank", + "@mapLayerSatelliteNightmicrophysics": { + "description": "Himawari Night Microphysics RGB composite layer name" }, - "mapLayerSatelliteCloudProbablyClear": "Probably clear", - "@mapLayerSatelliteCloudProbablyClear": { - "description": "Cloud-mask category: probably clear" + "notifySectionOther": "Other", + "@typhoonOverlaySectionStorm": { + "description": "Section header for L7/L10 storm-band choices in the overlay menu" }, - "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", - "@mapLayerSatelliteCloudProbablyCloudy": { - "description": "Cloud-mask category: probably cloudy" + "weatherRankingMeta": "Data time: {time}\n{count} stations", + "onboardingTermsAgree": "I have read and agree to the Terms of Service", + "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", + "@reportListYesterday": { + "description": "Date section header for reports that originated yesterday (Taipei)" }, - "mapLayerSatelliteCloudCloudy": "Cloudy", - "@mapLayerSatelliteCloudCloudy": { - "description": "Cloud-mask category: cloudy" + "@commonClose": { + "description": "Generic close button / action label" }, - "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", - "@mapLayerSatelliteTransparentWarm": { - "description": "Satellite legend note: on the IR grayscale/enhancements the warm end is clear sky, drawn transparent so the basemap shows" + "notifyOptLocalIntensity4": "Local intensity 4 or above", + "@typhoonLabelPosition": { + "description": "Bulletin table row label" }, - "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", - "@mapLayerSatelliteTransparentReflectance": { - "description": "Satellite legend note: on the reflectance bands a dark or night pixel is transparent so the basemap shows" + "eewArrived": "Arrived", + "meshtasticNoDevices": "No Meshtastic devices found", + "@reportFilterMagnitude": { + "description": "Label for the magnitude range filter" }, - "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", - "@mapLayerSatelliteTransparentZero": { - "description": "Satellite legend note: on the brightness-temperature-difference layers a near-zero difference is transparent — no absorber is present" + "mapLayerCategoryLife": "Daily life", + "reportFilterSortIntensity": "Intensity", + "typhoonMotion": "Moving", + "meshtasticStateDisconnected": "Disconnected", + "@regionEdit": { + "description": "Edit action on a saved-region bottom sheet" }, - "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", - "@mapLayerSatelliteTransparentNight": { - "description": "Satellite legend note: the daytime RGB recipes fade out across the terminator and are transparent at night" + "typhoonIntensityIntense": "Intense typhoon", + "@meshtasticSend": { + "description": "Send message button" }, - "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", - "@mapLayerSatelliteTransparentNoData": { - "description": "Satellite legend note: the SST retrieval has no value over land, drawn transparent" + "@notifyIntensity": { + "description": "Notify channel title" }, - "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", - "@mapLayerSatelliteTransparentNoVegetation": { - "description": "Satellite legend note: NDVI below the bare-soil threshold is transparent" + "mapLayerOrderTitle": "Reorder layers", + "@dpmFilterSectionRestroomType": { + "description": "Filter section title in the disaster-map sheet: restroom toilet-kind categories" }, - "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", - "@mapLayerSatelliteTransparentNoWater": { - "description": "Satellite legend note: NDWI/MNDWI at zero or below is transparent — no water signal" + "@onboardingPermBackgroundDesc": { + "description": "Permission row description: background location" }, - "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", - "@mapLayerSatelliteTransparentClear": { - "description": "Satellite legend note: the cloud-mask clear category is transparent so the basemap shows" + "@reportFilterAny": { + "description": "Chip / slider label meaning no filter applied" }, - "mapLayerStyleSection": "Colour style", - "@mapLayerStyleSection": { - "description": "Section header of the satellite band colour-style menu on the map" + "@onboardingPermCritical": { + "description": "Permission row: critical alerts (iOS)" }, - "mapLayerStyleTooltip": "Colour style", - "@mapLayerStyleTooltip": { - "description": "Tooltip of the colour-style chip beside the layer switcher" + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." }, - "mapLayerStyleGray": "Grayscale (JMA)", - "@mapLayerStyleGray": { - "description": "Colour-style option: JMA grayscale, the default radar-image convention" + "@meshtasticShortName": { + "description": "The radio's short name" }, - "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", - "@mapLayerStyleGrayTooltip": { - "description": "Explains the JMA grayscale band rendering" + "@typhoonHistoryLive": { + "description": "Typhoon UI: typhoonHistoryLive" }, - "mapLayerStyleJma": "Cloud-top enhancement (JMA)", - "@mapLayerStyleJma": { - "description": "Colour-style option: JMA cloud-top enhancement, tinted below −40 °C" + "dpmYes": "Yes", + "meshtasticNoHistory": "Not enough history yet", + "@mapLegendUnit": { + "description": "Unit footer under a map colour legend (e.g. Unit: dBZ)", + "placeholders": { + "unit": { + "type": "String" + } + } }, - "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", - "@mapLayerStyleJmaTooltip": { - "description": "Explains the JMA cloud-top enhancement band rendering" + "@dpmFilterSectionShelter": { + "description": "Filter section title in the disaster-map sheet: shelter disaster types" }, - "mapLayerStyleBd": "Dvorak BD", + "reportDetailLocalIntensityUnavailable": "No intensity data", "@mapLayerStyleBd": { "description": "Colour-style option: Dvorak BD curve stepped grayscale" }, - "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", - "@mapLayerStyleBdTooltip": { - "description": "Explains the Dvorak BD band rendering" + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "Depth", + "@eewLocalIntensity": { + "description": "Label for the estimated felt intensity at the user's location" }, - "mapLayerQpesums": "1h Precipitation Forecast", - "@mapLayerQpesums": { - "description": "Name of the QPESUMS next-1-hour precipitation forecast layer in the layer picker" + "@aedType": { + "description": "AED venue type row label" }, - "mapLayerLightning": "Lightning", - "@mapLayerLightning": { - "description": "Map layer switcher label for the lightning strike timeline" + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" }, - "lightningLegendCg": "Cloud-to-ground · {minutes} min", - "@lightningLegendCg": { - "description": "Lightning legend: cloud-to-ground strike within N minutes", - "placeholders": { - "minutes": { - "type": "int" - } - } + "@mapLayerSatelliteWatervapor": { + "description": "Himawari water-vapour layer name" }, - "lightningLegendCc": "Cloud-to-cloud · {minutes} min", - "@lightningLegendCc": { - "description": "Lightning legend: cloud-to-cloud strike within N minutes", + "onboardingScrollHint": "Scroll down to continue", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "Forecast", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "Map", + "notifyAdvisory": "Weather advisories", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "@mapLayerSatelliteCloudCloudy": { + "description": "Cloud-mask category: cloudy" + }, + "reportFilterReset": "Reset", + "@restroomCategoryLabel": { + "description": "Restroom detail row label for the venue category" + }, + "@mapLayerSatelliteTransparentNoData": { + "description": "Satellite legend note: the SST retrieval has no value over land, drawn transparent" + }, + "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionStorm": "Storm wind", + "moonPhaseFull": "Full moon", + "@mapNavRadar": { + "description": "Short Map-tab bottom-nav / default-layer picker label for radar" + }, + "@typhoonOverlayProbabilityTooltip": { + "description": "Tooltip for the strike-probability toggle; notes mutual exclusion with the cone" + }, + "@aedEmergencyPhone": { + "description": "AED emergency contact phone row label" + }, + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@moreTremReport": { + "description": "More-menu link to the TREM detection report website" + }, + "@mapLayerCategorySatellite": { + "description": "Section title in map overlay lists: satellite-imagery overlays" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "@feedOffline": { + "description": "Banner/headline when a realtime feed has gone offline" + }, + "weatherDynamicStateSubtitle": "Override the home backdrop weather", + "@dpmDisasterTsunami": { + "description": "Shelter disaster-type filter chip: tsunami" + }, + "reportFilterIntensityInfoModernTitle": "Current (from 2020)", + "@mapAppGoogleMaps": { + "description": "External map app choice: Google Maps" + }, + "typhoonDataTime": "Data time\n{time}", + "restroomTypeAccessible": "Accessible", + "moreSectionAbout": "About", + "meshtasticSelectDevice": "Select a radio", + "@dpmYes": { + "description": "Affirmative value in the disaster-map detail sheet" + }, + "@reportFilterIntensityInfoIntro": { + "description": "Intro paragraph for the intensity-scale info dialog" + }, + "onboardingIntroBody": "DPIP is your disaster-prevention companion. It brings together earthquake early warnings, earthquake reports, weather, and hazard information, and alerts you the moment it matters.\n\n• Earthquakes: early warnings, intensity reports, and detailed reports\n• Weather: real-time thunderstorm messages and weather advisories\n• Tsunami and disaster information\n\nNext, we'll ask you to review the Terms of Service and grant a few permissions so DPIP can protect you in real time.", + "@mapLayerWind": { + "description": "Map layer switcher label for the wind-direction layer" + }, + "shelterCapacityLabel": "Capacity", + "reportDetailImage": "Report image", + "@eewCountdown": { + "description": "S-wave arrival countdown in seconds", "placeholders": { - "minutes": { + "seconds": { "type": "int" } } }, - "mapTimelineNow": "Now", - "@mapTimelineNow": { - "description": "Label on the map timeline when the newest (latest) frame is selected" + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" }, - "mapTimelinePast": "Past", - "@mapTimelinePast": { - "description": "Label on the map timeline when the selected frame predates the present" + "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", + "onboardingPermNotify": "Notifications", + "@onboardingSkipLeave": { + "description": "Proceed past onboarding without granting permissions" }, - "mapTimelineFuture": "Future", - "@mapTimelineFuture": { - "description": "Label on the map timeline when the selected frame postdates the present" + "@typhoonOverlayStormL10Tooltip": { + "description": "Tooltip for the L10 storm-band radio row" }, - "mapTimelineObserved": "Observed", - "@mapTimelineObserved": { - "description": "Label above the map timeline's date (the radar observation time), e.g. Observed / 2026/07/14" + "meshtasticClearMessages": "Clear messages", + "@reportListEmptyFiltered": { + "description": "Empty state when active filters yield no report rows" }, - "mapTimelineForecast": "Forecast", - "@mapTimelineForecast": { - "description": "Label above the map timeline's date when the frame times are forecast times, e.g. Forecast / 2026/07/14" + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "Default map layer", + "@regionAddButton": { + "description": "Button to open the region picker to add a saved region" }, - "mapTimelineDataTime": "Data {time}", + "moreSectionNotify": "Notifications", "@mapTimelineDataTime": { "description": "Model-run issue time shown on the map timeline under a forecast layer's caption, e.g. Data 8/11 14:00", "placeholders": { @@ -1085,433 +1132,553 @@ } } }, - "notifySettingsMenu": "Notification settings", - "@notifySettingsMenu": { - "description": "More-menu entry that opens the notification-settings page" - }, - "notifyTitle": "Notifications", - "@notifyTitle": { - "description": "Title of the notification-settings page" + "@moonPhaseFull": { + "description": "Phase: full moon" }, "notifyUnavailable": "Push notifications aren't ready yet — try again shortly.", - "@notifyUnavailable": { - "description": "Shown on the notify page when there is no push token yet" - }, - "notifySetFailed": "Couldn't save the setting. Please try again.", - "@notifySetFailed": { - "description": "Snackbar shown when saving a notification channel fails" - }, - "notifySectionEew": "Earthquake early warning", - "@notifySectionEew": { - "description": "Notify page section header" + "mapLayerOrderReset": "Reset order", + "dpmAddress": "Address", + "weatherRankingMergeCounty": "County", + "@disasterMapOverlayShelterTooltip": { + "description": "Tooltip for the shelter toggle in the disaster-map overlay menu" }, - "notifySectionEarthquake": "Earthquake", - "@notifySectionEarthquake": { - "description": "Notify page section header" + "@appLogs": { + "description": "Title of the in-app log viewer and its entry in the More menu" }, - "notifySectionWeather": "Weather", - "@notifySectionWeather": { - "description": "Notify page section header" + "@homeRainTrendScattered": { + "description": "Home rain trend subtitle: peak intensity below the light-rain threshold" }, - "notifySectionTsunami": "Tsunami", - "@notifySectionTsunami": { - "description": "Notify page section header" + "@reportFilterSortIntensity": { + "description": "Sort reports by max intensity" }, - "notifySectionOther": "Other", - "@notifySectionOther": { - "description": "Notify page section header" + "@mapLayerSatelliteB16": { + "description": "Himawari CO₂-band channel (B16, 13.3 µm) layer name" }, - "notifyEew": "Emergency earthquake alert", - "@notifyEew": { + "moreSectionApp": "Get the app", + "reportFilterIntensityInfoLegacyBody": "Only levels 0–7. No 5− / 5+ / 6− / 6+ split.", + "@notifyTsunami": { "description": "Notify channel title" }, - "notifyMonitor": "Strong-motion monitor", - "@notifyMonitor": { - "description": "Notify channel title" + "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", + "qpesumsOverlayMenuTooltip": "QPESUMS overlay options", + "@restroomCategoryTransport": { + "description": "Restroom venue category: transport facility" }, - "notifyReport": "Earthquake report", - "@notifyReport": { - "description": "Notify channel title" + "@reportFilterIntensity": { + "description": "Label for the felt-intensity range filter" }, - "notifyIntensity": "Intensity report", - "@notifyIntensity": { - "description": "Notify channel title" + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." }, - "notifyThunderstorm": "Thunderstorm alerts", - "@notifyThunderstorm": { - "description": "Notify channel title" + "mapTimelineFuture": "Future", + "typhoonLegendCircleAvg": "Average circle", + "@mapTimelinePast": { + "description": "Label on the map timeline when the selected frame predates the present" }, - "notifyAdvisory": "Weather advisories", - "@notifyAdvisory": { - "description": "Notify channel title" + "@onboardingPermBackground": { + "description": "Permission row: background/Always location" }, - "notifyEvacuation": "Disaster information", - "@notifyEvacuation": { - "description": "Notify channel title" + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "SE", + "radarTownOutlineHint": "The finer mesh", + "eewCountdown": "{seconds} s", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" }, - "notifyTsunami": "Tsunami information", - "@notifyTsunami": { - "description": "Notify channel title" + "typhoonLabelGust": "Peak gust", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "Terms of Use", + "restroomTypeGenderNeutral": "Gender-neutral", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." }, - "notifyAnnouncement": "Announcements", - "@notifyAnnouncement": { - "description": "Notify channel title" + "@weatherRankingRecordedAt": { + "description": "Occurrence time for a gust or daily extreme", + "placeholders": { + "time": { + "type": "String" + } + } }, - "notifyOptOff": "Off", - "@notifyOptOff": { - "description": "Notify option label" + "@reportListMeta": { + "description": "Magnitude and depth line on a report list row", + "placeholders": { + "magnitude": { + "type": "String" + }, + "depth": { + "type": "String" + } + } }, - "notifyOptAll": "Receive all", - "@notifyOptAll": { - "description": "Notify option label" + "@regionEmpty": { + "description": "Empty state on the saved-regions manage page" }, - "notifyOptLocalIntensity4": "Local intensity 4 or above", - "@notifyOptLocalIntensity4": { - "description": "Notify option label" + "@changelogCurrentVersion": { + "description": "Chip/badge when a release matches the installed app version" }, - "notifyOptLocalIntensity1": "Local intensity 1 or above", - "@notifyOptLocalIntensity1": { - "description": "Notify option label" + "@disasterMapOverlayRestroomTooltip": { + "description": "Tooltip for the restroom toggle in the disaster-map overlay menu" }, - "notifyOptWeatherLocal": "Current location only", - "@notifyOptWeatherLocal": { - "description": "Notify option label" + "@mapLegendExpand": { + "description": "Collapsed map-legend chip label / tooltip — tap to expand" }, - "notifyOptTsunamiWarning": "Tsunami warnings only", - "@notifyOptTsunamiWarning": { - "description": "Notify option label" + "@shelterCapacityValue": { + "description": "Shelter detail capacity row value", + "placeholders": { + "n": { + "type": "int" + } + } }, - "notifyOptTsunamiAll": "Tsunami advisories and warnings", - "@notifyOptTsunamiAll": { - "description": "Notify option label" + "@homeRainTrendUpdated": { + "description": "Data-update time beside the home rain trend title, Taipei wall clock HH:mm", + "placeholders": { + "time": { + "type": "String" + } + } }, - "onboardingNext": "Next", - "@onboardingNext": { - "description": "Onboarding next-step button" + "@shelterVulnerableOkLabel": { + "description": "Shelter detail row: whether evacuees needing care can be accommodated" }, - "onboardingBack": "Back", - "@onboardingBack": { - "description": "Onboarding back button" + "@mapNavPressure": { + "description": "Short Map-tab bottom-nav / default-layer picker label for pressure" }, - "onboardingScrollHint": "Scroll down to continue", - "@onboardingScrollHint": { - "description": "Hint shown until the user scrolls to the end" + "@changelogVersionDetails": { + "description": "App bar title on a single release's detail page" }, - "onboardingIntroTitle": "Welcome to DPIP", - "@onboardingIntroTitle": { - "description": "Onboarding intro page title" + "notifyThunderstorm": "Thunderstorm alerts", + "skyTimeGolden": "Golden hour", + "@typhoonTdNo": { + "description": "Secondary badge on the typhoon sheet hero: the CWA tropical-depression serial number, e.g. TD 14", + "placeholders": { + "no": { + "type": "String" + } + } }, - "onboardingIntroBody": "DPIP is your disaster-prevention companion. It brings together earthquake early warnings, earthquake reports, weather, and hazard information, and alerts you the moment it matters.\n\n• Earthquakes: early warnings, intensity reports, and detailed reports\n• Weather: real-time thunderstorm messages and weather advisories\n• Tsunami and disaster information\n\nNext, we'll ask you to review the Terms of Service and grant a few permissions so DPIP can protect you in real time.", - "@onboardingIntroBody": { - "description": "Onboarding intro page body" + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." }, - "onboardingTermsTitle": "Terms of Service", - "@onboardingTermsTitle": { - "description": "Onboarding terms page title" + "meshtasticRadioSettings": "LoRa", + "@changelogBodyEmpty": { + "description": "Placeholder when a GitHub release has an empty markdown body" }, - "onboardingTermsBody": "Please read the following notices before using DPIP:\n\n• All information should defer to the content published by the Central Weather Administration (CWA).\n\n• Depending on network, server, app, and upstream data-source conditions, information may not be received; we make every effort to avoid this but cannot guarantee it never happens.\n\n• Strong shaking may reach your location before the notification does.\n\n• Earthquake early warnings are fast-computed results that may carry significant error — understand this and use them with caution.\n\n• Any behavior not sanctioned by the authorities may carry legal risk; please follow all applicable regulations.\n\nIn addition, to provide localized alerts, this service collects and uploads your approximate location and push identifier — in the foreground and background — solely to decide which alerts to send you.\n\nBy tapping \"Agree and continue\" you confirm that you have read, understood, and agree to the above.", - "@onboardingTermsBody": { - "description": "Onboarding terms of service body" + "weatherRankingAnalysisCurrent": "Now {value}°C", + "@mapLayerSatelliteDust": { + "description": "Himawari Dust RGB composite layer name" }, - "onboardingTermsAgree": "I have read and agree to the Terms of Service", - "@onboardingTermsAgree": { - "description": "Terms agreement checkbox label" + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" }, - "onboardingAgreeContinue": "Agree and continue", - "@onboardingAgreeContinue": { - "description": "Terms page continue button" + "moreGithub": "ExpTech GitHub", + "@restroomTypeMixed": { + "description": "Restroom type: mixed/unisex restroom" }, - "onboardingPermsTitle": "Permissions", - "@onboardingPermsTitle": { - "description": "Onboarding permissions page title" + "@weatherRankingMeta": { + "description": "Snapshot time and station count above a ranking list", + "placeholders": { + "time": { + "type": "String" + }, + "count": { + "type": "int" + } + } }, - "onboardingPermsBody": "So DPIP can alert you the moment disaster strikes, please grant the following. You can change these anytime in system settings.", - "@onboardingPermsBody": { - "description": "Onboarding permissions page intro" + "@dataSectionWeather": { + "description": "Section header on the Data hub for weather observation rankings" }, - "onboardingPermNotify": "Notifications", - "@onboardingPermNotify": { - "description": "Permission row: notifications" + "@mapLayerSatelliteNdwi": { + "description": "Himawari normalised-difference water-index layer name" }, - "onboardingPermNotifyDesc": "Deliver earthquake, weather, and disaster alerts the moment they happen.", - "@onboardingPermNotifyDesc": { - "description": "Permission row description: notifications" + "@notifyAnnouncement": { + "description": "Notify channel title" }, - "onboardingPermCritical": "Critical alerts", - "@onboardingPermCritical": { - "description": "Permission row: critical alerts (iOS)" + "homeForecastUnavailable": "Select a township to see the forecast", + "mapLayers": "Layers", + "@homeRainTrendHeavyStopping": { + "description": "Home rain trend subtitle: heavy rain forecast to stop partway through the hour", + "placeholders": { + "minutes": { + "type": "int" + } + } }, - "onboardingPermCriticalDesc": "Let life-threatening earthquake warnings sound even in silent mode or Do Not Disturb.", - "@onboardingPermCriticalDesc": { - "description": "Permission row description: critical alerts" + "@mapTownLabels": { + "description": "Map setting: show township-name labels when the map is zoomed in" }, - "onboardingPermLocation": "Location", - "@onboardingPermLocation": { - "description": "Permission row: location" + "@weatherModeRain": { + "description": "Weather animation forced to rain" }, - "onboardingPermLocationDesc": "Target alerts to where you are.", - "@onboardingPermLocationDesc": { - "description": "Permission row description: location" + "meshtasticHardware": "Hardware", + "languageSettings": "Language", + "dpmDisasterNuclear": "Nuclear accident", + "@typhoonWarningAreas": { + "description": "List of counties under a typhoon warning", + "placeholders": { + "areas": { + "type": "String" + } + } }, - "onboardingPermBackground": "Background location", - "@onboardingPermBackground": { - "description": "Permission row: background/Always location" + "@moonNextFullMoon": { + "description": "Next full moon date label" }, - "onboardingPermBackgroundDesc": "Allow \"Always\" so alerts still target you when the app is closed.", - "@onboardingPermBackgroundDesc": { - "description": "Permission row description: background location" + "language": "Language", + "homeForecastFeelsLike": "Feels like {temp}°", + "typhoonOverlayWeatherHint": "Aligned to bulletin time", + "@mapLayerSatelliteTransparentZero": { + "description": "Satellite legend note: on the brightness-temperature-difference layers a near-zero difference is transparent — no absorber is present" }, - "onboardingPermBattery": "Battery exemption", - "@onboardingPermBattery": { - "description": "Permission row: battery optimization (Android)" + "@termsOfService": { + "description": "More-menu link title for the Terms of Service" }, - "onboardingPermBatteryDesc": "Allow DPIP to keep running in the background so alerts aren't delayed or missed.", - "@onboardingPermBatteryDesc": { - "description": "Permission row description: battery" + "@typhoonLegendCone": { + "description": "Typhoon map legend: uncertainty cone" }, - "onboardingGrant": "Grant", - "@onboardingGrant": { - "description": "Permission grant button" + "@moreSectionApp": { + "description": "More-page section header for the app-store download links" }, - "onboardingGranted": "Granted", - "@onboardingGranted": { - "description": "Permission granted label" + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" }, - "onboardingStart": "Get started", - "@onboardingStart": { - "description": "Onboarding finish button" + "skyTimeDawn": "Dawn", + "skyTimeAfternoon": "Afternoon", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "Typhoon warning", + "@mapLayerSatelliteB02": { + "description": "Himawari visible-green channel (B02, 0.51 µm) layer name" }, - "language": "Language", - "@language": { - "description": "Language picker tooltip / label" + "@eewSerial": { + "description": "The serial (report number) of an EEW alert", + "placeholders": { + "serial": { + "type": "int" + } + } }, - "languageSettings": "Language", - "@languageSettings": { - "description": "Label next to the language picker on the welcome screen" + "@restroomGradeGood": { + "description": "Restroom cleanliness grade: good" }, - "languageSystem": "System default", - "@languageSystem": { - "description": "Language picker option: follow the system language" + "@faq": { + "description": "More-menu link title for the FAQ / help page" }, - "locationBannerServiceOff": "Location services are off — local alerts can't target your area.", - "@locationBannerServiceOff": { - "description": "Banner when the OS location toggle is off" + "moreSourceCode": "Source code", + "mapLayerCategoryWeather": "Weather observations", + "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", + "@sponsorIntro": { + "description": "Support page intro paragraph explaining why donations help" }, - "locationBannerPermission": "Location permission is off — local alerts can't target your area.", - "@locationBannerPermission": { - "description": "Banner when location permission is denied" + "windForecastTownOutlineHint": "The finer mesh", + "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", + "mapAppCopyCoordinates": "Copy coordinates", + "reportFilterIntensityInfoIntro": "CWA changed the felt-intensity scale on 1 Jan 2020 (Taipei time).", + "@mapLayerSatelliteGlobalOutline": { + "description": "Satellite legend row: the country/global border, drawn bright yellow over the imagery" }, - "locationBannerFix": "Open settings", - "@locationBannerFix": { - "description": "Action on the location banner to open system settings" + "@weatherRankingExtremeRange": { + "description": "Chip to rank by daily high minus low" }, - "notifyBannerDisabled": "Notifications are off — you won't receive disaster alerts.", - "@notifyBannerDisabled": { - "description": "App-wide banner shown when notification permission is disabled" + "@weatherModeFog": { + "description": "Weather animation forced to heavy fog" }, - "onboardingSkipTitle": "Permissions not granted", - "@onboardingSkipTitle": { - "description": "Title of the confirm dialog shown when finishing onboarding without key permissions" + "mapNavEarthquake": "Earthquake", + "@aedHoursSunday": { + "description": "AED Sunday opening hours row label" }, - "onboardingSkipBody": "Without location and notifications, DPIP can't alert you to earthquakes and disasters near you in real time. You can still grant them later in Settings.", - "@onboardingSkipBody": { - "description": "Body of the skip-permissions confirmation dialog" + "typhoonGust": "Gust", + "restroomGradeAverage": "Average", + "@meshtasticNodes": { + "description": "Mesh nodes section header" }, - "onboardingSkipStay": "Go back", - "@onboardingSkipStay": { - "description": "Dismiss the skip dialog and return to grant permissions" + "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", + "onboardingPermBackgroundDesc": "Allow \"Always\" so alerts still target you when the app is closed.", + "mapTimelineForecast": "Forecast", + "restroomTypeLabel": "Type", + "navEarthquake": "Earthquake", + "@shelterAddressLabel": { + "description": "Shelter detail address row label" }, - "onboardingSkipLeave": "Skip anyway", - "@onboardingSkipLeave": { - "description": "Proceed past onboarding without granting permissions" + "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "Earthquake Report", + "@weatherHumidity": { + "description": "Label for the humidity metric in the home weather header" }, - "moreYoutube": "YouTube", - "@moreYoutube": { - "description": "More-menu link to the ExpTech YouTube channel" + "moreTremReport": "TREM detection report", + "weatherDataTime": "{station} · Data {time}", + "@typhoonPickerTd": { + "description": "Sheet picker: unnamed tropical depression (CWA tdNo)", + "placeholders": { + "no": { + "type": "String" + } + } }, - "moreGithub": "ExpTech GitHub", - "@moreGithub": { - "description": "More-menu link to the ExpTech GitHub organisation" + "@restroomTypeMale": { + "description": "Restroom type: male restroom" }, - "moreSourceCode": "Source code", - "moreSectionApp": "Get the app", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "@moreSectionApp": { - "description": "More-page section header for the app-store download links" + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "@weatherRankingExtremeLow": { + "description": "Chip to rank by recorded daily minimum temperature" }, - "@moreGooglePlay": { - "description": "Google Play store link title (brand name)" + "radarCountyOutline": "County borders", + "onboardingGranted": "Granted", + "@restroomCategoryPark": { + "description": "Restroom venue category: park" }, - "@moreAppStore": { - "description": "Apple App Store link title (brand name)" + "@mapLayerSatelliteMndwi": { + "description": "Himawari modified normalised-difference water-index layer name" }, - "@moreSourceCode": { - "description": "More-menu link to DPIP's source repository on GitHub" + "@mapTimelineNow": { + "description": "Label on the map timeline when the newest (latest) frame is selected" }, - "displaySettings": "Display", - "@displaySettings": { - "description": "Display-settings menu entry and page title (theme mode)" + "@mapLayerCategoryWeather": { + "description": "Section title in map overlay lists: the weather-observation overlays" }, - "defaultMapLayerSettings": "Default map layer", - "@defaultMapLayerSettings": { - "description": "More-menu entry and page title for choosing the Map tab's default overlay" + "@mapAppCopyCoordinates": { + "description": "Choice-sheet action: copy the point's coordinates" }, - "defaultMapLayerSubtitle": "The Map tab opens on this overlay. The bottom-navigation icon and label follow this choice.", - "@defaultMapLayerSubtitle": { - "description": "Explanatory subtitle on the default-map-layer settings page" + "commonClose": "Close", + "restroomGradeLabel": "Grade", + "rainIntervalNow": "Today", + "changelogCurrentVersion": "Current", + "typhoonLabelPressure": "Central pressure", + "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", + "aedOpenRemark": "Hours note", + "@homeActiveEventsEmpty": { + "description": "Empty state when the realtime event feed has nothing in effect" }, - "mapNavRadar": "Radar", - "@mapNavRadar": { - "description": "Short Map-tab bottom-nav / default-layer picker label for radar" + "onboardingPermsBody": "So DPIP can alert you the moment disaster strikes, please grant the following. You can change these anytime in system settings.", + "typhoonOverlaySectionWeather": "Weather underlay", + "@homeForecastHighLow": { + "description": "24h forecast series high and low air temperatures", + "placeholders": { + "high": { + "type": "String" + }, + "low": { + "type": "String" + } + } }, - "mapNavQpesums": "Forecast", - "@mapNavQpesums": { - "description": "Short Map-tab bottom-nav / default-layer picker label for the 1h QPESUMS precipitation forecast" + "@meshtasticStateConnected": { + "description": "Connection state label" }, - "mapNavSatellite": "Satellite", - "@mapNavSatellite": { - "description": "Short Map-tab bottom-nav / default-layer picker label for satellite" + "@notifySectionTsunami": { + "description": "Notify page section header" }, - "mapNavLightning": "Lightning", - "@mapNavLightning": { - "description": "Short Map-tab bottom-nav / default-layer picker label for lightning" + "@weatherRankingGust": { + "description": "Ranking tab/tile for peak gust speed" }, - "mapNavTyphoon": "Typhoon", - "@mapNavTyphoon": { - "description": "Short Map-tab bottom-nav / default-layer picker label for typhoon" + "@mapLayerSatelliteTransparentReflectance": { + "description": "Satellite legend note: on the reflectance bands a dark or night pixel is transparent so the basemap shows" }, - "mapNavEarthquake": "Earthquake", - "@mapNavEarthquake": { - "description": "Short Map-tab bottom-nav / default-layer picker label for RTS seismic monitor" + "@weatherRankingAnalysisRange": { + "description": "Diurnal range fragment in an extremes analysis line", + "placeholders": { + "value": { + "type": "String" + } + } }, - "mapNavTemperature": "Temperature", - "@mapNavTemperature": { - "description": "Short Map-tab bottom-nav / default-layer picker label for temperature" + "notifyOptWeatherLocal": "Current location only", + "@reportDetailTitle": { + "description": "Header title over the report detail map's back button" }, - "mapNavHumidity": "Humidity", - "@mapNavHumidity": { - "description": "Short Map-tab bottom-nav / default-layer picker label for humidity" + "mapNavRain": "Rain", + "moonDays": "days", + "@aedDescription": { + "description": "AED free-text description row label" }, - "mapNavPressure": "Pressure", - "@mapNavPressure": { - "description": "Short Map-tab bottom-nav / default-layer picker label for pressure" + "@reportDetailLocalIntensity": { + "description": "Section header over the per-location (GPS + saved townships) felt-intensity readout, shown above the area breakdown" }, - "mapNavWind": "Wind", - "@mapNavWind": { - "description": "Short Map-tab bottom-nav / default-layer picker label for wind" + "@onboardingIntroBody": { + "description": "Onboarding intro page body" }, - "mapNavRain": "Rain", - "@mapNavRain": { - "description": "Short Map-tab bottom-nav / default-layer picker label for rain" + "@onboardingScrollHint": { + "description": "Hint shown until the user scrolls to the end" }, - "mapNavDisaster": "Disaster", - "@mapNavDisaster": { - "description": "Short Map-tab bottom-nav / default-layer picker label for disaster-prevention map" + "@restroomGradeAverage": { + "description": "Restroom cleanliness grade: average" }, - "displayTheme": "Theme", - "@displayTheme": { - "description": "Section header for the theme-mode chooser on the Display settings page" + "@mapLegendCollapse": { + "description": "Tooltip on the control that collapses the map legend" }, - "themeSystem": "System", - "@themeSystem": { - "description": "Theme option: follow the system light/dark setting" + "@aedHoursSaturday": { + "description": "AED Saturday opening hours row label" }, - "themeLight": "Light", - "@themeLight": { - "description": "Theme option: always light" + "mapLegendUnit": "Unit: {unit}", + "@trendNoData": { + "description": "Shown in the station trend chart when there is no data to plot" }, - "themeDark": "Dark", - "@themeDark": { - "description": "Theme option: always dark" + "weatherModeClear": "Clear", + "meshtasticRadio": "Radio", + "@mapTerrainRelief": { + "description": "Map setting: show the base map's hillshade relief" }, - "moreSectionAbout": "About", - "@moreSectionAbout": { - "description": "More-menu section header for about / legal links" + "commonEmpty": "Nothing to show", + "mapLayerSatelliteB01": "Himawari Blue (B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" }, - "termsOfService": "Terms of Service", - "@termsOfService": { - "description": "More-menu link title for the Terms of Service" + "reportFilterOrderAsc": "Ascending", + "reportFilterApply": "Apply", + "@rainIntervalNow": { + "description": "Rainfall accumulation since local midnight (API now)" }, - "faq": "FAQ", - "@faq": { - "description": "More-menu link title for the FAQ / help page" + "reportDetailImageUnavailable": "Report image not available", + "@weatherRankingMergeTo": { + "description": "Label before township/county merge chips on ranking" }, - "openSourceLicenses": "Open-source licenses", - "@openSourceLicenses": { - "description": "More-menu entry that opens the bundled open-source license list" + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." }, - "sponsorTitle": "Support DPIP", - "@sponsorTitle": { - "description": "Support page title and the More-menu entry that opens it" + "@typhoonOverlaySectionWeather": { + "description": "Overlay-menu section for radar / IR under the typhoon vectors" }, - "sponsorIntro": "DPIP is dedicated to real-time disaster-prevention information, with no ads or other revenue model. Your support helps us keep the servers running and keep developing.", - "@sponsorIntro": { - "description": "Support page intro paragraph explaining why donations help" + "weatherRankingHighest": "Highest", + "@aedCategory": { + "description": "AED venue category row label" }, - "sponsorSubscriptions": "Subscriptions", - "@sponsorSubscriptions": { - "description": "Support page section header for recurring subscription tiers" + "@homeViewOnMap": { + "description": "Small home-header link that opens the map tab on the temperature layer at the nearest station" }, - "sponsorRecommended": "Recommended", - "@sponsorRecommended": { - "description": "Badge on the recommended (subscription) support section" + "reportDetailReplay": "Replay", + "mapLayerRestroom": "Restrooms", + "restroomCategoryWelfare": "Welfare", + "@notifyAdvisory": { + "description": "Notify channel title" }, - "sponsorOneTime": "One-time", - "@sponsorOneTime": { - "description": "Support page section header for one-time tips" + "restroomGradeExcellent": "Excellent", + "@mapLayerSatelliteCloudProbablyCloudy": { + "description": "Cloud-mask category: probably cloudy" }, - "sponsorPerMonth": "{price} / month", - "@sponsorPerMonth": { - "description": "Monthly price label for a subscription; price is the store-localized amount", + "@reportFilterDateStartNote": { + "description": "Explains that startTime covers from midnight on that calendar day" + }, + "@dpmDisasterLandslide": { + "description": "Shelter disaster-type filter chip: landslide" + }, + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@homeForecastEmpty": { + "description": "Empty or failed forecast on the home sheet" + }, + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "Numerical forecast", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", + "@dpmSheetEmpty": { + "description": "Hint in the disaster-map detail sheet when nothing is selected" + }, + "themeSystem": "System", + "mapLayerSatelliteNdvi": "Himawari NDVI", + "typhoonLegendForecast": "Forecast track", + "@feedConnecting": { + "description": "A realtime feed is establishing its first data" + }, + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "Precipitation", + "@typhoonSatelliteTitle": { + "description": "Typhoon UI: typhoonSatelliteTitle" + }, + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "Tap a marker on the map for details", + "@mapLayerOrderReset": { + "description": "Button that restores the layer picker's default order" + }, + "@mapResetNorth": { + "description": "Map compass tooltip: re-points the camera to north-up" + }, + "@regionSelectCount": { + "description": "Header showing how many saved-region slots are used", "placeholders": { - "price": { - "type": "String", - "example": "NT$75" + "count": { + "type": "int" + }, + "max": { + "type": "int" } } }, - "sponsorRestore": "Restore purchases", - "@sponsorRestore": { - "description": "Footer action that restores previously bought purchases" + "@moreAppStore": { + "description": "Apple App Store link title (brand name)" }, - "sponsorTerms": "Terms of Use", - "@sponsorTerms": { - "description": "Footer link to the Terms of Use" + "@typhoonOverlayWeatherHint": { + "description": "Subtitle: weather tile matches typhoon report time" + }, + "onboardingSkipLeave": "Skip anyway", + "onboardingBack": "Back", + "aedPlaceDesc": "Placement", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "@reportListEmpty": { + "description": "Empty state when the report catalogue has no rows" + }, + "@mapLayerWindForecastEcmwf": { + "description": "Map layer switcher label for the ECMWF wind-forecast layer" + }, + "@mapNavQpesums": { + "description": "Short Map-tab bottom-nav / default-layer picker label for the 1h QPESUMS precipitation forecast" + }, + "onboardingSkipTitle": "Permissions not granted", + "@mapLayerSatelliteNaturalcolor": { + "description": "Himawari Natural Color RGB composite layer name" + }, + "restroomTypeFamily": "Family", + "@restroomCategoryGovernment": { + "description": "Restroom venue category: public service office" + }, + "@mapLayers": { + "description": "Title of the map layer-picker sheet" + }, + "@onboardingPermNotify": { + "description": "Permission row: notifications" }, - "sponsorPrivacy": "Privacy Policy", "@sponsorPrivacy": { "description": "Footer link to the Privacy Policy" }, - "sponsorRestoring": "Restoring purchases…", - "@sponsorRestoring": { - "description": "Snackbar shown when a purchase restore has been requested" + "@eewSWave": { + "description": "Label for the S-wave arrival countdown tile" }, - "sponsorRestoreUnavailable": "Can't reach the store. Please try again later.", - "@sponsorRestoreUnavailable": { - "description": "Snackbar shown when the store can't be reached to restore" + "typhoonValueKm": "{n} km", + "@typhoonLegendForecastPoint": { + "description": "Typhoon map legend: forecast waypoint" }, - "commonClose": "Close", - "@commonClose": { - "description": "Generic close button / action label" + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." }, - "mapLayerTemperature": "Temperature", - "@mapLayerTemperature": { - "description": "Map layer switcher label for the air-temperature layer" + "typhoonPressure": "Pressure", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" }, - "trendRange24h": "24h", - "@trendRange24h": { - "description": "Trend chart range toggle: last 24 hours" + "onboardingPermBattery": "Battery exemption", + "typhoonLabelNw": "NW", + "dpmDisasterFlood": "Flood", + "moonPhaseWaxingCrescent": "Waxing crescent", + "@locationBannerFix": { + "description": "Action on the location banner to open system settings" }, - "trendRange7d": "7d", - "@trendRange7d": { - "description": "Trend chart range toggle: last 7 days" + "@homeActiveEventsTitle": { + "description": "Section title for currently active disaster notices on the collapsed home sheet" }, - "trendNoData": "No trend data", - "@trendNoData": { - "description": "Shown in the station trend chart when there is no data to plot" + "@mapLayerShelter": { + "description": "Disaster-map overlay menu toggle for evacuation shelters" }, - "trendCumulativeTotal": "Cumulative {total} mm", + "restroomCategoryLeisure": "Leisure", + "mapLayerTemperature": "Temperature", + "aedCategory": "Category", "@trendCumulativeTotal": { "description": "Running total label above the cumulative station rain trend chart", "placeholders": { @@ -1520,143 +1687,1033 @@ } } }, - "chartHourLabel": "{hour}h", - "@chartHourLabel": { - "description": "Compact chart X-axis hour tick (e.g. 20h / 20時)", - "placeholders": { - "hour": { - "type": "int" - } - } + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" }, - "mapLayerHumidity": "Humidity", - "@mapLayerHumidity": { - "description": "Map layer switcher label for the humidity layer" + "@onboardingPermsBody": { + "description": "Onboarding permissions page intro" }, - "mapLayerPressure": "Pressure", - "@mapLayerPressure": { - "description": "Map layer switcher label for the air-pressure layer" + "@onboardingGranted": { + "description": "Permission granted label" }, - "mapLayerWind": "Wind direction", - "mapLayerRain": "Rainfall", - "@mapLayerRain": { - "description": "Map layer switcher label for the rainfall station layer" + "meshtasticChannels": "Channels", + "@notifySectionEew": { + "description": "Notify page section header" }, - "rainIntervalMenu": "Accumulation window", - "@rainIntervalMenu": { - "description": "Tooltip for the rainfall accumulation-interval menu" + "monitorWaiting": "Waiting for data…", + "typhoonOverlayForecastCallouts": "Forecast tooltips", + "@weatherRankingWind": { + "description": "Ranking tab/tile for sustained wind speed" }, - "rainIntervalNow": "Today", - "@rainIntervalNow": { - "description": "Rainfall accumulation since local midnight (API now)" + "@mapLayerSatelliteB01": { + "description": "Himawari visible-blue channel (B01, 0.47 µm) layer name" }, - "rainInterval10m": "10 min", - "rainInterval1h": "1 h", - "rainInterval3h": "3 h", - "rainInterval6h": "6 h", - "rainInterval12h": "12 h", - "rainInterval24h": "24 h", - "rainInterval2d": "2 d", - "rainInterval3d": "3 d", - "mapLayerTyphoon": "Typhoon", - "@mapLayerTyphoon": { - "description": "Layer-switcher label for the typhoon map layer" + "@reportFilterSortMagnitude": { + "description": "Sort reports by magnitude" }, - "typhoonNoActive": "No active typhoon", + "@onboardingSkipTitle": { + "description": "Title of the confirm dialog shown when finishing onboarding without key permissions" + }, + "@feedStale": { + "description": "Banner over a realtime feed whose data has aged past the freshness threshold" + }, + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "Epicenter", + "meshtasticVoltage": "Voltage", + "@monitorDelay": { + "description": "RTS monitor latency: how far behind the latest snapshot is (calibrated now minus the snapshot timestamp), in seconds — pre-formatted to one decimal, e.g. \"0.3\"", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@restroomTypeLabel": { + "description": "Restroom detail row label for the toilet type" + }, + "@meshtasticSent": { + "description": "Packets sent this session" + }, + "mapLayerWind": "Wind direction", + "reportDetailMagnitude": "Magnitude", + "@moreSectionDisplay": { + "description": "Section header on the More page for language and theme" + }, + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "@typhoonOverlayWeatherSatelliteTooltip": { + "description": "Tooltip for Himawari IR underlay (mutex with radar)" + }, + "reportDetailAreaIntensity": "Intensity by area", + "rainInterval12h": "12 h", + "reportListMagnitude": "M{magnitude}", + "@weatherRankingTempExtremes": { + "description": "Ranking tab for recorded daily high/low/range (not current temp)" + }, + "dpmDisasterLandslide": "Landslide", + "notifyMonitor": "Strong-motion monitor", + "onboardingStart": "Get started", + "@trendRange24h": { + "description": "Trend chart range toggle: last 24 hours" + }, + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / month", + "mapLayerPressure": "Pressure", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "@moreGooglePlay": { + "description": "Google Play store link title (brand name)" + }, + "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "@typhoonTyNo": { + "description": "Secondary badge on the typhoon sheet hero: the CWA typhoon serial number, e.g. TY 4", + "placeholders": { + "no": { + "type": "String" + } + } + }, + "@mapLayerSatelliteAirmass": { + "description": "Himawari Airmass RGB composite layer name" + }, + "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", + "shelterIndoorLabel": "Indoor shelter", + "@changelogTypePrerelease": { + "description": "Chip label for a pre-release" + }, + "@sponsorRestoreUnavailable": { + "description": "Snackbar shown when the store can't be reached to restore" + }, + "notifyOptOff": "Off", + "@reportListSearch": { + "description": "Fetches the report list with the current draft filters" + }, + "reportFilterSortTime": "Time", + "mapLayerSatelliteCloudProbablyClear": "Probably clear", + "weatherModeThunderstorm": "Thunderstorm", + "homeViewOnMap": "View on map", + "reportFilterIntensityInfoLegacyTitle": "Legacy (before 2020)", + "typhoonLabelSpeed": "Past movement speed", + "@mapNavDisaster": { + "description": "Short Map-tab bottom-nav / default-layer picker label for disaster-prevention map" + }, + "@reportFilterTitle": { + "description": "Title of the earthquake report filter sheet" + }, + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "@reportDetailSortByIntensity": { + "description": "Tooltip on the area-intensity sort toggle when tapping it switches to grouping by intensity level" + }, + "mapAppOpenFailed": "Could not open {app}", + "@weatherRankingExtremeHigh": { + "description": "Chip to rank by recorded daily maximum temperature" + }, + "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "Daily low", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "@mapLayerSatelliteBtdFog": { + "description": "Himawari night fog / low-cloud brightness-temperature-difference layer name" + }, + "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", + "@homeForecastPop": { + "description": "Probability of precipitation percent on a forecast hour chip", + "placeholders": { + "pop": { + "type": "String" + } + } + }, + "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", + "shelterCategoryLabel": "Disaster types", + "meshtasticStateConnecting": "Connecting…", + "@dpmNo": { + "description": "Negative value in the disaster-map detail sheet" + }, + "@mapLayerSatelliteBtdOzone": { + "description": "Himawari tropopause brightness-temperature-difference layer name" + }, + "moonTitle": "Moon", + "weatherRankingGust": "Gust", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "Shelter disaster types", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "Server status", + "notifySectionWeather": "Weather", + "meshtasticPreset": "Modem preset", + "@restroomTypeUnspecified": { + "description": "Restroom type: not specified" + }, + "@mapLayerSatelliteTruecolor": { + "description": "Himawari True Color RGB composite layer name" + }, + "dataSectionSeismic": "Seismic", + "@weatherModeThunderstorm": { + "description": "Weather animation forced to a thunderstorm" + }, + "@sponsorTerms": { + "description": "Footer link to the Terms of Use" + }, + "@eewSummary": { + "description": "One-line summary of an EEW alert's magnitude and depth", + "placeholders": { + "magnitude": { + "type": "String" + }, + "depth": { + "type": "String" + } + } + }, + "@mapTimelineFuture": { + "description": "Label on the map timeline when the selected frame postdates the present" + }, + "@mapLayerSatelliteSst": { + "description": "Himawari sea-surface-temperature (ACSPO L3C) layer name" + }, + "changelogBodyEmpty": "No notes for this release.", + "radarGlobalOutline": "National borders", + "@mapMyLocation": { + "description": "Map control that centers the camera on the device GPS fix" + }, + "@weatherDataTime": { + "description": "Nearest-station name and observation time shown as small text under the home weather header name", + "placeholders": { + "station": { + "type": "String" + }, + "time": { + "type": "String" + } + } + }, + "notifyEew": "Emergency earthquake alert", + "@mapLayerSatelliteB15": { + "description": "Himawari longwave-infrared channel (B15, 12.4 µm) layer name" + }, + "regionNationwide": "Nationwide", + "moreNotifyLog": "DPIP notification log", + "@mapLayerStyleBdTooltip": { + "description": "Explains the Dvorak BD band rendering" + }, + "regionCurrent": "Current location", + "@mapLayerHumidity": { + "description": "Map layer switcher label for the humidity layer" + }, + "dpmFilterSectionRestroom": "Venue types", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "Snow", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "Debug info", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "@mapLayerSatelliteTransparentClear": { + "description": "Satellite legend note: the cloud-mask clear category is transparent so the basemap shows" + }, + "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "Lightning", + "@eewNone": { + "description": "Calm state of the earthquake monitor when the live feed reports no alert" + }, + "@onboardingSkipBody": { + "description": "Body of the skip-permissions confirmation dialog" + }, + "homeForecastEmpty": "No forecast available", + "@reportDetailLocalIntensityUnavailable": { + "description": "Shown in place of an intensity badge when a location's county isn't in this report's felt-area list at all" + }, + "@reportFilterDepthKm": { + "description": "Depth value with unit in the filter sheet", + "placeholders": { + "depth": { + "type": "String" + } + } + }, + "@mapLayerTemperature": { + "description": "Map layer switcher label for the air-temperature layer" + }, + "sponsorOneTime": "One-time", + "mapLayerSatelliteBtdSplit": "Himawari Split Window", + "onboardingPermBackground": "Background location", + "@homeForecastHumidity": { + "description": "Relative humidity for the selected forecast hour", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "aedEmergencyPhone": "Emergency phone", + "dpmOpenInMaps": "Open in maps", + "meshtasticNotifyNodes": "Notify on new nodes", + "@restroomTypeGenderNeutral": { + "description": "Restroom type: gender-neutral restroom" + }, + "onboardingPermCriticalDesc": "Let life-threatening earthquake warnings sound even in silent mode or Do Not Disturb.", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + }, + "description": "Choice-sheet label suffix marking the platform home map app, with the app name" + }, + "@notifyOptOff": { + "description": "Notify option label" + }, + "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", + "meshtasticSent": "Sent", + "homeForecastTitle": "24-hour forecast", + "@typhoonValueMs": { + "placeholders": { + "n": { + "type": "String" + } + } + }, + "typhoonLegendWarningAreas": "Warning areas", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "Local intensity 1 or above", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@reportDetailInfo": { + "description": "Section header over origin time / epicenter / magnitude / depth" + }, + "@onboardingPermNotifyDesc": { + "description": "Permission row description: notifications" + }, + "@sponsorRecommended": { + "description": "Badge on the recommended (subscription) support section" + }, + "@typhoonWarningTitle": { + "description": "Typhoon UI: typhoonWarningTitle" + }, + "mapTimelinePast": "Past", + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "@lightningLegendCg": { + "description": "Lightning legend: cloud-to-ground strike within N minutes", + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "restroomTypeFemale": "Female", + "reportListToday": "Today", + "@reportFilterDatePick": { + "description": "Button to open the date-range picker when none selected" + }, + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "Loading…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "Moderate typhoon", "typhoonWind": "Wind", - "typhoonGust": "Gust", - "typhoonPressure": "Pressure", - "typhoonMotion": "Moving", - "typhoonLabelPosition": "Centre location", - "@typhoonLabelPosition": { + "@weatherRankingTitle": { + "description": "App bar title for the weather station ranking page" + }, + "mapLayerSatelliteAsh": "Himawari Ash", + "@restroomCategoryOther": { + "description": "Restroom venue category: other" + }, + "rainInterval3h": "3 h", + "reportListSearch": "Search", + "@onboardingPermLocationDesc": { + "description": "Permission row description: location" + }, + "@rainIntervalMenu": { + "description": "Tooltip for the rainfall accumulation-interval menu" + }, + "mapLayerCategorySatellite": "Satellite", + "@typhoonOverlayStormL7Tooltip": { + "description": "Tooltip for the L7 storm-band radio option" + }, + "@moreLinkOpenFailed": { + "description": "Snackbar shown when an external link fails to open in the browser" + }, + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "reportFilterLocation": "Location", + "@mapNavLightning": { + "description": "Short Map-tab bottom-nav / default-layer picker label for lightning" + }, + "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", + "@mapTimelineForecast": { + "description": "Label above the map timeline's date when the frame times are forecast times, e.g. Forecast / 2026/07/14" + }, + "typhoonIntensityTd": "Tropical depression", + "@mapLayerSatelliteTransparentNoWater": { + "description": "Satellite legend note: NDWI/MNDWI at zero or below is transparent — no water signal" + }, + "reportFilterDate": "Date", + "sponsorRestoreUnavailable": "Can't reach the store. Please try again later.", + "homeForecastPop": "{pop}%", + "regionEmpty": "No saved regions yet", + "@weatherRankingBy": { + "description": "Label before highest/lowest (or desc/asc) chips on ranking" + }, + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "@homeRainTrendLightSustained": { + "description": "Home rain trend subtitle: light rain that keeps up through the hour" + }, + "onboardingPermBatteryDesc": "Allow DPIP to keep running in the background so alerts aren't delayed or missed.", + "mapNavDisaster": "Disaster", + "@dpmDisasterNuclear": { + "description": "Shelter disaster-type filter chip: nuclear accident" + }, + "radarScanRangeSubtitle": "Outlines the area the four radars actually observe.", + "aedHoursSunday": "Sunday hours", + "reportDetailOriginTime": "Origin time", + "trendNoData": "No trend data", + "onboardingPermLocation": "Location", + "moreDiscord": "Discord community", + "@typhoonOverlayStormBandSubtitle": { + "description": "Subtitle under each storm-band option (fill + dashed avg)" + }, + "mapNavPressure": "Pressure", + "mapLayerSatelliteB13": "Himawari Infrared (B13)", + "@onboardingAgreeContinue": { + "description": "Terms page continue button" + }, + "typhoonTdNo": "TD {no}", + "@mapLayerOrderTitle": { + "description": "Title of the layer-order editor, also the tooltip of the reorder button in the layer picker" + }, + "@reportDetailSortByCounty": { + "description": "Tooltip on the area-intensity sort toggle when tapping it switches to an alphabetical county list" + }, + "@notifyMonitor": { + "description": "Notify channel title" + }, + "@mapLayerStyleJmaTooltip": { + "description": "Explains the JMA cloud-top enhancement band rendering" + }, + "changelogEmpty": "No release notes yet", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "Start day: from 00:00 (Taipei)", + "eewTitle": "Earthquake early warning", + "@mapLayerRestroom": { + "description": "Disaster-map overlay menu toggle for public restrooms" + }, + "mapLayerWindForecastEcmwf": "ECMWF", + "@mapLayerSatelliteB13": { + "description": "Himawari clean-infrared window channel (B13, 10.4 µm) layer name" + }, + "@dpmDisasterSlope": { + "description": "Shelter disaster-type filter chip: slope hazard" + }, + "@@locale": "en", + "regionSelectCount": "{count}/{max} selected", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", + "@restroomCategoryCultural": { + "description": "Restroom venue category: cultural / leisure activity venue" + }, + "@weatherRankingAnalysisCurrent": { + "description": "Current temperature fragment in an extremes analysis line", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "meshtasticStateError": "Error", + "weatherModeOvercast": "Overcast", + "@homeForecastFeelsLike": { + "description": "Apparent temperature for the selected forecast hour", + "placeholders": { + "temp": { + "type": "String" + } + } + }, + "@typhoonLabelProbCircle": { + "description": "Forecast point: radius of the 70% track probability circle" + }, + "@reportFilterSortDepth": { + "description": "Sort reports by hypocentral depth" + }, + "@mapLayerSatelliteNdvi": { + "description": "Himawari normalised-difference vegetation-index layer name" + }, + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "@shelterCategoryLabel": { + "description": "Shelter detail applicable-disaster categories row label" + }, + "reportDetailDepth": "Depth", + "@typhoonTrackDetail": { + "description": "Typhoon UI: typhoonTrackDetail" + }, + "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", + "reportFilterDatePick": "Pick dates", + "onboardingSkipStay": "Go back", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "@mapLayerSatelliteTransparentWarm": { + "description": "Satellite legend note: on the IR grayscale/enhancements the warm end is clear sky, drawn transparent so the basemap shows" + }, + "@reportDetailOriginTime": { + "description": "Row label for the report's origin date/time" + }, + "commonFetchFailed": "Couldn't load data. Please try again.", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "Outdoor shelter", + "@moreSectionAbout": { + "description": "More-menu section header for about / legal links" + }, + "meshtasticStateConnected": "Connected", + "mapNavRadar": "Radar", + "mapLayerSatelliteCloudClear": "Clear", + "eewSummary": "M{magnitude} · depth {depth} km", + "locationBannerPermission": "Location permission is off — local alerts can't target your area.", + "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", + "radarCountyOutlineHint": "Drawn over the echo", + "@defaultMapLayerSettings": { + "description": "More-menu entry and page title for choosing the Map tab's default overlay" + }, + "windForecastCountyOutlineHint": "Drawn over the wind field", + "homeRainTrendTitle": "Next hour precipitation", + "@experimentalFeatures": { + "description": "Title of the experimental-features settings page and its More-menu entry" + }, + "moonPhaseFirstQuarter": "First quarter", + "@onboardingPermBattery": { + "description": "Permission row: battery optimization (Android)" + }, + "mapLayerCategoryTyphoon": "Typhoon", + "@displayTheme": { + "description": "Section header for the theme-mode chooser on the Display settings page" + }, + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "@mapLayerSatelliteB04": { + "description": "Himawari near-infrared channel (B04, 0.86 µm) layer name" + }, + "@commonFetchFailed": { + "description": "Error headline when a data request (AsyncView) fails, with a retry button" + }, + "@mapNavTyphoon": { + "description": "Short Map-tab bottom-nav / default-layer picker label for typhoon" + }, + "meshtasticUtilization": "Airtime (24h)", + "@weatherRankingHighest": { + "description": "Chip to rank temperature descending" + }, + "restroomTypeMixed": "Mixed", + "restroomGradeGood": "Good", + "notifyTsunami": "Tsunami information", + "navData": "Data", + "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "@languageSettings": { + "description": "Label next to the language picker on the welcome screen" + }, + "mapAppCallFailed": "This device cannot make phone calls", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "Any", + "@moreGithub": { + "description": "More-menu link to the ExpTech GitHub organisation" + }, + "@mapLayerSatelliteTransparentNoVegetation": { + "description": "Satellite legend note: NDVI below the bare-soil threshold is transparent" + }, + "weatherRankingMergeTo": "Merge to", + "@mapLayerSatelliteB10": { + "description": "Himawari lower-level water-vapour channel (B10, 7.3 µm) layer name" + }, + "notifyIntensity": "Intensity report", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "Accumulation window", + "@mapLayerSatelliteB06": { + "description": "Himawari near-infrared channel (B06, 2.3 µm) layer name" + }, + "reportDetailLocalFelt": "Local Felt Earthquake", + "@moreYoutube": { + "description": "More-menu link to the ExpTech YouTube channel" + }, + "meshtasticDevice": "Device", + "onboardingGrant": "Grant", + "@typhoonLabelSpeed": { "description": "Bulletin table row label" }, - "typhoonLabelDirection": "Past movement direction", - "@typhoonLabelDirection": { - "description": "Bulletin table row label" + "@sponsorSubscriptions": { + "description": "Support page section header for recurring subscription tiers" + }, + "@mapLayerPressure": { + "description": "Map layer switcher label for the air-pressure layer" + }, + "@typhoonLegendProbability": { + "description": "Typhoon UI: typhoonLegendProbability" + }, + "weatherModeRain": "Rain", + "shelterVulnerableOkLabel": "Vulnerable-people friendly", + "stationSheetEmpty": "Tap a station to see its reading", + "@typhoonIntensityTd": { + "description": "CWA class: tropical depression (past-track colour)" + }, + "typhoonLegendProbability": "Strike probability", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "Magnitude", + "@moreDeveloper": { + "description": "More-menu entry / title for the developer diagnostics page" + }, + "@languageName": { + "description": "This language's own name, shown in the in-app language picker. Each locale's ARB names itself; the picker is built from these, never a hardcoded list." + }, + "@regionManageTitle": { + "description": "More-menu entry that opens the region picker" + }, + "skyTimeMorning": "Morning", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "Experimental features", + "onboardingTermsBody": "Please read the following notices before using DPIP:\n\n• All information should defer to the content published by the Central Weather Administration (CWA).\n\n• Depending on network, server, app, and upstream data-source conditions, information may not be received; we make every effort to avoid this but cannot guarantee it never happens.\n\n• Strong shaking may reach your location before the notification does.\n\n• Earthquake early warnings are fast-computed results that may carry significant error — understand this and use them with caution.\n\n• Any behavior not sanctioned by the authorities may carry legal risk; please follow all applicable regulations.\n\nIn addition, to provide localized alerts, this service collects and uploads your approximate location and push identifier — in the foreground and background — solely to decide which alerts to send you.\n\nBy tapping \"Agree and continue\" you confirm that you have read, understood, and agree to the above.", + "@mapTimelineObserved": { + "description": "Label above the map timeline's date (the radar observation time), e.g. Observed / 2026/07/14" + }, + "reportFilterTitle": "Filters", + "onboardingPermCritical": "Critical alerts", + "trendCumulativeTotal": "Cumulative {total} mm", + "languageName": "English", + "@mapLayerStyleJma": { + "description": "Colour-style option: JMA cloud-top enhancement, tinted below −40 °C" + }, + "@monitorWaiting": { + "description": "Shown in the monitor panel before the first RTS snapshot arrives" + }, + "reportListEmptyFiltered": "No earthquake reports match these filters", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "Typhoon", + "weatherModeSand": "Dust", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "Satellite", + "@typhoonValueLat": { + "placeholders": { + "lat": { + "type": "String" + } + } + }, + "@dpmOpenInMaps": { + "description": "Action in the disaster-map detail sheet: open the point in an external map app" + }, + "notifyReport": "Earthquake report", + "mapAppCoordinatesCopied": "Coordinates copied", + "@dataSectionSeismic": { + "description": "Section header on the Data hub for earthquake-related entries" + }, + "skyTimeNight": "Night", + "@languageSystem": { + "description": "Language picker option: follow the system language" + }, + "sponsorRecommended": "Recommended", + "@eewMaxIntensity": { + "description": "Label for an EEW alert's maximum felt intensity badge" + }, + "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", + "weatherRankingWind": "Wind speed", + "feedStale": "Data may be out of date", + "@homeRainTrendTitle": { + "description": "Section title for the home sheet 1-hour per-minute rainfall bar chart" + }, + "@defaultMapLayerSubtitle": { + "description": "Explanatory subtitle on the default-map-layer settings page" + }, + "homeForecastWind": "{direction} · Force {level}", + "@mapLayerSatelliteB03": { + "description": "Himawari visible-red channel (B03, 0.64 µm) layer name" + }, + "navHome": "Home", + "meshtasticRegionLabel": "Region", + "@weatherDynamicState": { + "description": "Setting that forces the home weather backdrop to a fixed state" + }, + "@mapNavEarthquake": { + "description": "Short Map-tab bottom-nav / default-layer picker label for RTS seismic monitor" + }, + "@reportFilterRange": { + "description": "Displays a selected filter range (intensity, magnitude, depth, or dates)", + "placeholders": { + "start": { + "type": "String" + }, + "end": { + "type": "String" + } + } + }, + "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@reportDetailEpicenter": { + "description": "Row label for the epicenter's latitude/longitude" }, - "typhoonLabelSpeed": "Past movement speed", - "@typhoonLabelSpeed": { - "description": "Bulletin table row label" + "@notifySetFailed": { + "description": "Snackbar shown when saving a notification channel fails" }, - "typhoonLabelPressure": "Central pressure", - "@typhoonLabelPressure": { - "description": "Bulletin table row label" + "@typhoonIntensityModerate": { + "description": "CWA class: moderate typhoon (past-track colour)" }, - "typhoonLabelWind": "Max. sustained wind near centre", - "@typhoonLabelWind": { - "description": "Bulletin table row label" + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" }, - "typhoonLabelGust": "Peak gust", + "@mapLayerLightning": { + "description": "Map layer switcher label for the lightning strike timeline" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "Open-source licenses", "@typhoonLabelGust": { "description": "Bulletin table row label" }, - "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", + "@weatherPrecipitation": { + "description": "Label for the precipitation metric in the home weather header" + }, + "weatherRankingLowest": "Lowest", + "@onboardingPermLocation": { + "description": "Permission row: location" + }, + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "@regionCurrent": { + "description": "Region bar label for the current GPS township" + }, + "reportFilterSortDepth": "Depth", + "@mapLayerDisasterMap": { + "description": "Map layer switcher label for the disaster-prevention map (DPM)" + }, + "@reportDetailAreaIntensity": { + "description": "Section header over the per-area/town felt-intensity breakdown" + }, + "@moreAnnouncements": { + "description": "More-menu link to the ExpTech announcements website" + }, + "mapTimelineDataTime": "Data {time}", + "radarScanRange": "Show scan range", + "@mapLayerQpesums": { + "description": "Name of the QPESUMS next-1-hour precipitation forecast layer in the layer picker" + }, + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "@mapLayerSatelliteBtdSo2": { + "description": "Himawari SO₂ / cloud-phase brightness-temperature-difference layer name" + }, + "weatherRankingAnalysisRange": "Range {value}°C", + "weatherRankingExtremeHigh": "Daily high", + "@aedPlaceDesc": { + "description": "AED placement description row label" + }, + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "@rainIntervalSection": { + "description": "Section title in the rainfall menu: the accumulation-interval choices" + }, + "changelogVersionDetails": "Release details", + "@typhoonOverlayProbabilityHint": { + "description": "Short hint under the strike-probability toggle" + }, "@typhoonLabelGaleAvg": { "description": "Bulletin table row label" }, - "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", - "@typhoonLabelStormAvg": { - "description": "Bulletin table row label" + "sponsorPrivacy": "Privacy Policy", + "reportDetailLocalIntensity": "Intensity at your locations", + "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", + "meshtasticAirtime": "Air time (TX)", + "@eewTitle": { + "description": "Header of the earthquake monitor when one or more alerts are active" }, - "typhoonLabelProbCircle": "70% probability circle", - "@typhoonLabelProbCircle": { - "description": "Forecast point: radius of the 70% track probability circle" + "shelterCapacityValue": "{n} people", + "@locationBannerServiceOff": { + "description": "Banner when the OS location toggle is off" }, - "typhoonForecastLead": "Forecast +{hours} h", - "@typhoonForecastLead": { - "description": "Forecast lead time for a tapped track point", + "lightningLegendCc": "Cloud-to-cloud · {minutes} min", + "meshtasticSendHint": "Message to broadcast", + "@restroomTypeAccessible": { + "description": "Restroom type: accessible restroom" + }, + "@dataEarthquakeSubtitle": { + "description": "Subtitle under the Earthquake tile on the Data hub" + }, + "monitorDelay": "Delay {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "@changelogTypeStable": { + "description": "Chip label for a stable release" + }, + "dpmNo": "No", + "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": { + "description": "External map app choice: Apple Maps" + }, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "Keeps township borders legible under the radar echo.", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", + "radarScanRangeHint": "Blank outside means unobserved", + "typhoonPickerTd": "Tropical depression TD {no}", + "@mapAppCallFailed": { + "description": "Snackbar when tapping the emergency phone and the device has no phone handler" + }, + "mapLayerSatelliteWatervapor": "Himawari Water Vapour", + "regionAddButton": "Add a region", + "displaySettings": "Display", + "restroomGradePoor": "Below standard", + "@moreSectionNotify": { + "description": "Section header on the More page for notification settings" + }, + "@mapOverlaySectionReference": { + "description": "Section title in map overlay settings menus: the reference overlays" + }, + "restroomCategoryTourist": "Tourist", + "locationBannerServiceOff": "Location services are off — local alerts can't target your area.", + "mapLayerStyleTooltip": "Colour style", + "@mapLayerSatelliteB12": { + "description": "Himawari ozone-band channel (B12, 9.6 µm) layer name" + }, + "lightningLegendCg": "Cloud-to-ground · {minutes} min", + "skyTimeAuto": "Auto", + "appLogs": "App logs", + "feedConnecting": "Connecting…", + "notifyBannerDisabled": "Notifications are off — you won't receive disaster alerts.", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "@typhoonValueLon": { "placeholders": { - "hours": { + "lon": { "type": "String" } } }, - "typhoonLabelNw": "NW", - "typhoonLabelNe": "NE", - "typhoonLabelSw": "SW", - "typhoonLabelSe": "SE", - "typhoonValueLat": "{lat}°N", - "@typhoonValueLat": { - "placeholders": { - "lat": { - "type": "String" - } - } + "@restroomCategoryWelfare": { + "description": "Restroom venue category: social welfare institution / gathering place" + }, + "@reportDetailMagnitude": { + "description": "Row label for the report's magnitude" + }, + "@typhoonOverlaySectionExtra": { + "description": "Section header for optional typhoon overlays (probability, warning)" + }, + "@reportListEnd": { + "description": "Footer when the report catalogue has no further pages" + }, + "@moreDiscord": { + "description": "More-menu link to the ExpTech Discord community" + }, + "weatherHumidity": "Humidity", + "typhoonValueMs": "{n} m/s", + "homeForecastHumidity": "Humidity {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "Transport", + "reportFilterLocationHint": "e.g. Hualien, offshore", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "Distance", + "meshtasticSnrTrend": "Signal trend (SNR)", + "meshtasticBatteryTrend": "Battery trend", + "typhoonOverlayMenuTooltip": "Typhoon overlay options", + "mapLayerSatelliteBtdOzone": "Himawari Tropopause", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "Earthquake", + "@displaySettings": { + "description": "Display-settings menu entry and page title (theme mode)" + }, + "mapLayerDisasterMap": "Disaster Map", + "weatherModeFog": "Fog", + "@typhoonLegendCircle15": { + "description": "Typhoon UI: typhoonLegendCircle15" + }, + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", + "@aedRegion": { + "description": "AED city / district row label" + }, + "moreAnnouncements": "Announcements", + "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "@restroomTypeFemale": { + "description": "Restroom type: female restroom" + }, + "restroomCategoryGovernment": "Government", + "typhoonLegendCurrent": "Current centre", + "aedAddress": "Address", + "mapLayerAed": "AED", + "changelogTypePrerelease": "Beta", + "reportFilterIntensityInfoModernBody": "Levels 0–4, 5−, 5+, 6−, 6+, and 7. The filter slider uses this scale; older events still show legacy labels in the list.", + "@typhoonLegendPast": { + "description": "Typhoon map legend: past/observed path" + }, + "@aedHoursWeekday": { + "description": "AED weekday opening hours row label" + }, + "typhoonOverlayWeatherNone": "None", + "@mapNavHumidity": { + "description": "Short Map-tab bottom-nav / default-layer picker label for humidity" + }, + "mapLayerStyleGray": "Grayscale (JMA)", + "weatherModeAuto": "Auto", + "typhoonLabelProbCircle": "70% probability circle", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "@homeForecastTitle": { + "description": "Section title for the home sheet township hourly forecast" + }, + "notifyOptAll": "Receive all", + "displayTheme": "Theme", + "@reportFilterOrderDesc": { + "description": "Sort order: newest / largest first" + }, + "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", + "@mapLayerSatelliteCloudClear": { + "description": "Cloud-mask category: clear sky, transparent on the map" + }, + "@typhoonOverlayWeatherRadarTooltip": { + "description": "Tooltip for radar underlay (mutex with IR)" + }, + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "@sponsorOneTime": { + "description": "Support page section header for one-time tips" + }, + "@reportDetailOpenReport": { + "description": "Button that opens the official CWA report page in a browser" + }, + "typhoonLabelDirection": "Past movement direction", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "Saved regions", + "typhoonLegendCone": "Forecast cone", + "moreCwaEew": "CWA earthquake early warning", + "onboardingPermsTitle": "Permissions", + "mapLayerStyleJma": "Cloud-top enhancement (JMA)", + "rainInterval10m": "10 min", + "weatherRankingAnalysisLow": "Low {value}", + "@mapNavTemperature": { + "description": "Short Map-tab bottom-nav / default-layer picker label for temperature" + }, + "meshtasticConnectAnyway": "Connect anyway", + "@typhoonLabelPressure": { + "description": "Bulletin table row label" + }, + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", + "@onboardingBack": { + "description": "Onboarding back button" + }, + "@restroomCategoryReligious": { + "description": "Restroom venue category: religious / ceremonial venue" + }, + "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", + "chartHourLabel": "{hour}h", + "@sponsorTitle": { + "description": "Support page title and the More-menu entry that opens it" }, - "typhoonValueLon": "{lon}°E", - "@typhoonValueLon": { - "placeholders": { - "lon": { - "type": "String" - } - } + "@reportFilterSort": { + "description": "Section title for report list sort field + order" }, - "typhoonValueKm": "{n} km", - "@typhoonValueKm": { - "placeholders": { - "n": { - "type": "String" - } - } + "@weatherDynamicStateSubtitle": { + "description": "Subtitle explaining the weather animation setting" }, - "typhoonValueHpa": "{n} hPa", - "@typhoonValueHpa": { - "placeholders": { - "n": { - "type": "String" - } - } + "mapLayerShelter": "Shelters", + "@weatherModeClear": { + "description": "Weather animation forced to a clear sky" }, - "typhoonValueMs": "{n} m/s", - "@typhoonValueMs": { - "placeholders": { - "n": { - "type": "String" - } - } + "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", + "@navEarthquake": { + "description": "Earthquake report catalogue title (entry under the Data hub)" }, - "typhoonDataTime": "Data time\n{time}", + "mapLayerSatelliteNdwi": "Himawari NDWI", "@typhoonDataTime": { "description": "Bulletin data time under the intensity chip (Taipei wall clock)", "placeholders": { @@ -1665,801 +2722,815 @@ } } }, - "@mapLayerWind": { - "description": "Map layer switcher label for the wind-direction layer" + "disasterMapOverlayShelterTooltip": "Show evacuation shelters", + "mapNavHumidity": "Humidity", + "@meshtasticTraffic": { + "description": "Section: packet counters" }, - "mapLayerWindForecastEcmwf": "ECMWF", - "@mapLayerWindForecastEcmwf": { - "description": "Map layer switcher label for the ECMWF wind-forecast layer" + "reportDetailSortByIntensity": "Sort by intensity", + "homeRainTrendNoData": "No data", + "mapLayerCategoryRadar": "Radar", + "@mapNavRain": { + "description": "Short Map-tab bottom-nav / default-layer picker label for rain" }, - "mapLayerWindForecastGfs": "GFS", - "@mapLayerWindForecastGfs": { - "description": "Map layer switcher label for the GFS wind-forecast layer" + "@regionSelectTitle": { + "description": "Title of the region picker (city list) page" }, - "mapLayerMonitor": "Seismic Monitor", - "@mapLayerMonitor": { - "description": "Map layer switcher label for the real-time seismic monitor (RTS)" + "@meshtasticStateConfiguring": { + "description": "Connection state label" }, - "mapLayerDisasterMap": "Disaster Map", - "@mapLayerDisasterMap": { - "description": "Map layer switcher label for the disaster-prevention map (DPM)" + "meshtasticShortName": "Short name", + "mapLayerSatelliteAirmass": "Himawari Airmass", + "@notifySectionOther": { + "description": "Notify page section header" }, - "mapLayerAed": "AED", - "@mapLayerAed": { - "description": "Disaster-map overlay menu toggle for AED (defibrillator) points" + "@mapLayerSatelliteB07": { + "description": "Himawari shortwave-infrared channel (B07, 3.9 µm) layer name" }, - "disasterMapOverlayMenuTooltip": "Disaster map layers", - "@disasterMapOverlayMenuTooltip": { - "description": "Tooltip on the disaster-map overlay tune button" + "@dpmDisasterEarthquake": { + "description": "Shelter disaster-type filter chip: earthquake" }, - "disasterMapOverlaySectionLayers": "Layers", - "@disasterMapOverlaySectionLayers": { - "description": "Section header for DPM sub-layer toggles in the overlay menu" + "@mapLayerSatelliteTransparentNight": { + "description": "Satellite legend note: the daytime RGB recipes fade out across the terminator and are transparent at night" }, - "disasterMapOverlayAedTooltip": "Show AED locations", - "@disasterMapOverlayAedTooltip": { - "description": "Tooltip for the AED toggle in the disaster-map overlay menu" + "@meshtasticPreset": { + "description": "LoRa modem preset" }, - "aedAddress": "Address", - "@aedAddress": { - "description": "AED detail row label" + "typhoonTrackDetail": "Track detail", + "dataSectionWeather": "Weather", + "aedHoursWeekday": "Weekday hours", + "homeActiveEventsTitle": "Active events", + "weatherRankingAnalysisHigh": "High {value}", + "@homeRainTrendNoData": { + "description": "Label on the home rain trend chart for minutes beyond the forecast window, and the empty-card hint" }, - "aedRegion": "Region", - "@aedRegion": { - "description": "AED city / district row label" + "@notifyThunderstorm": { + "description": "Notify channel title" }, - "aedCategory": "Category", - "@aedCategory": { - "description": "AED venue category row label" + "faq": "FAQ", + "typhoonHistoryLive": "Live", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." }, - "aedType": "Type", - "@aedType": { - "description": "AED venue type row label" + "eewSerial": "Report {serial}", + "reportFilterSort": "Sort", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." }, - "aedPlaceDesc": "Placement", - "@aedPlaceDesc": { - "description": "AED placement description row label" + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "Earthquake reports", + "typhoonNoActive": "No active typhoon", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } }, - "aedDescription": "Notes", - "@aedDescription": { - "description": "AED free-text description row label" + "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", + "@typhoonLegendForecast": { + "description": "Typhoon map legend: forecast path" }, - "aedHoursWeekday": "Weekday hours", - "@aedHoursWeekday": { - "description": "AED weekday opening hours row label" + "navEvents": "Events", + "onboardingTermsTitle": "Terms of Service", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" }, - "aedHoursSaturday": "Saturday hours", - "@aedHoursSaturday": { - "description": "AED Saturday opening hours row label" + "mapTownLabels": "Township names", + "@typhoonOverlayWarningTooltip": { + "description": "Tooltip for the warning-areas overlay toggle" }, - "aedHoursSunday": "Sunday hours", - "@aedHoursSunday": { - "description": "AED Sunday opening hours row label" + "@mapLayerSatelliteB11": { + "description": "Himawari SO₂ absorption channel (B11, 8.6 µm) layer name" }, - "aedOpenRemark": "Hours note", - "@aedOpenRemark": { - "description": "AED opening-hours remark row label" + "notifySetFailed": "Couldn't save the setting. Please try again.", + "@notifyBannerDisabled": { + "description": "App-wide banner shown when notification permission is disabled" }, - "aedEmergencyPhone": "Emergency phone", - "@aedEmergencyPhone": { - "description": "AED emergency contact phone row label" + "@shelterCapacityLabel": { + "description": "Shelter detail capacity row label" }, - "mapLayerRestroom": "Restrooms", - "@mapLayerRestroom": { - "description": "Disaster-map overlay menu toggle for public restrooms" + "@typhoonOverlayForecastCalloutsTooltip": { + "description": "Tooltip for the forecast callouts overlay toggle" }, - "mapLayerShelter": "Shelters", - "@mapLayerShelter": { - "description": "Disaster-map overlay menu toggle for evacuation shelters" + "@restroomCategoryTourist": { + "description": "Restroom venue category: tourist area / scenic spot" }, - "disasterMapOverlayRestroomTooltip": "Show public restrooms", - "@disasterMapOverlayRestroomTooltip": { - "description": "Tooltip for the restroom toggle in the disaster-map overlay menu" + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "@navMore": { + "description": "Bottom-nav label and page title for the More tab" }, - "disasterMapOverlayShelterTooltip": "Show evacuation shelters", - "@disasterMapOverlayShelterTooltip": { - "description": "Tooltip for the shelter toggle in the disaster-map overlay menu" + "@shelterIndoorLabel": { + "description": "Shelter detail row: whether indoor shelter is provided" }, - "dpmOpenInMaps": "Open in maps", - "@dpmOpenInMaps": { - "description": "Action in the disaster-map detail sheet: open the point in an external map app" + "notifyAnnouncement": "Announcements", + "onboardingIntroTitle": "Welcome to DPIP", + "regionCurrentUnavailable": "Can't get current location", + "languageSystem": "System default", + "skyTimeSunset": "Sunset", + "mapLayerSatelliteDust": "Himawari Dust", + "mapAppAppleMaps": "Apple Maps", + "@reportFilterApply": { + "description": "Primary button on the report filter sheet — saves draft and searches" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { - "description": "External map app choice: Google Maps" + "regionEdit": "Edit", + "weatherDynamicState": "Weather animation", + "@notifyOptLocalIntensity1": { + "description": "Notify option label" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { - "description": "External map app choice: Apple Maps" + "mapPlaceholderDisabled": "Map (temporarily disabled)", + "moonNow": "Now", + "@moonNow": { + "description": "Returns the moon page to the present moment" }, - "mapAppDefault": "{app} (default)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - }, - "description": "Choice-sheet label suffix marking the platform home map app, with the app name" + "moonSectionAppearance": "Appearance", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" }, - "mapAppCopyCoordinates": "Copy coordinates", - "@mapAppCopyCoordinates": { - "description": "Choice-sheet action: copy the point's coordinates" + "moonSectionRiseSet": "Rise and set", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" }, - "mapAppCoordinatesCopied": "Coordinates copied", - "@mapAppCoordinatesCopied": { - "description": "Snackbar confirming the coordinates were copied" + "moonSectionUpcoming": "Upcoming", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" }, - "mapAppOpenFailed": "Could not open {app}", - "@mapAppOpenFailed": { - "placeholders": { - "app": {"type": "String"} - }, - "description": "Snackbar when the chosen map app cannot be opened on this device" + "moonSectionCalendar": "Calendar", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" }, - "mapAppCallFailed": "This device cannot make phone calls", - "@mapAppCallFailed": { - "description": "Snackbar when tapping the emergency phone and the device has no phone handler" + "moonDistance": "Distance", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" }, - - "mapOverlaySectionReference": "Reference layers", - "@mapOverlaySectionReference": { - "description": "Section title in map overlay settings menus: the reference overlays" + "moonKilometres": "km", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" }, - "mapLayerCategoryEarthquake": "Earthquake", - "@mapLayerCategoryEarthquake": { - "description": "Section title in map overlay lists: the seismic-monitor overlays" + "moonApparentSize": "Apparent size", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" }, - - "mapLayerCategoryTyphoon": "Typhoon", - "@mapLayerCategoryTyphoon": { - "description": "Section title in map overlay lists: typhoon overlays" + "moonRise": "Moonrise", + "@moonRise": { + "description": "Time the Moon rises" }, - - "mapLayerCategoryWeather": "Weather observations", - "@mapLayerCategoryWeather": { - "description": "Section title in map overlay lists: the weather-observation overlays" + "moonSet": "Moonset", + "@moonSet": { + "description": "Time the Moon sets" }, - - "mapLayerCategorySatellite": "Satellite", - "@mapLayerCategorySatellite": { - "description": "Section title in map overlay lists: satellite-imagery overlays" + "moonNextNewMoon": "Next new moon", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "Up all day", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "None today", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "Sun", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "Sunrise, twilight and the solar terms", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "Daylight", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "Twilight", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "Light", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "Sundial", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" }, - - "mapLayerCategoryRadar": "Radar", - "@mapLayerCategoryRadar": { - "description": "Section title in map overlay lists: radar and precipitation-forecast overlays" + "sunSectionTerms": "Solar terms", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" }, - - "mapLayerCategoryLife": "Daily life", - "@mapLayerCategoryLife": { - "description": "Section title in map overlay lists: everyday-life facility overlays" + "sunRise": "Sunrise", + "@sunRise": { + "description": "Time the Sun rises" }, - - "mapLayerCategoryForecast": "Numerical forecast", - "@mapLayerCategoryForecast": { - "description": "Section title in map overlay lists: numerical weather prediction (ECMWF/GFS) wind-field overlays" - }, "mapOverlaySectionMap": "Map", - "@mapOverlaySectionMap": { - "description": "Section title in map overlay settings menus: base-map settings" + "sunSet": "Sunset", + "@sunSet": { + "description": "Time the Sun sets" }, - "rainIntervalSection": "Time window", - "@rainIntervalSection": { - "description": "Section title in the rainfall menu: the accumulation-interval choices" + "sunNoon": "Solar noon", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" }, - - "mapTownLabels": "Township names", - "@mapTownLabels": { - "description": "Map setting: show township-name labels when the map is zoomed in" + "sunDayLength": "Day length", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" }, - "mapTownLabelsHint": "Show township names when zoomed in", - "@mapTownLabelsHint": { - "description": "Hint under the township-names setting" + "sunTwilightCivil": "Civil", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" }, - - "mapTerrainRelief": "Terrain relief", - "@mapTerrainRelief": { - "description": "Map setting: show the base map's hillshade relief" + "sunTwilightNautical": "Nautical", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" }, - "mapTerrainReliefHint": "Show shaded terrain relief on the base map", - "@mapTerrainReliefHint": { - "description": "Hint under the terrain-relief setting" + "sunTwilightAstronomical": "Astronomical", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" }, - - "dpmSheetEmpty": "Tap a marker on the map for details", - "@dpmSheetEmpty": { - "description": "Hint in the disaster-map detail sheet when nothing is selected" + "sunGoldenHourMorning": "Morning golden hour", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" }, - "dpmAddress": "Address", - "@dpmAddress": { - "description": "Address row label in the disaster-map restroom / shelter detail sheet" + "sunGoldenHourEvening": "Evening golden hour", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" }, - "restroomTypeLabel": "Type", - "@restroomTypeLabel": { - "description": "Restroom detail row label for the toilet type" + "sunBlueHour": "Blue hour", + "@sunBlueHour": { + "description": "Blue hour span after sunset" }, - "restroomCategoryLabel": "Category", - "@restroomCategoryLabel": { - "description": "Restroom detail row label for the venue category" + "sunEquationOfTime": "Equation of time", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" }, - "restroomGradeLabel": "Grade", - "@restroomGradeLabel": { - "description": "Restroom detail row label for the cleanliness grade" + "sunMinutes": "min", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" }, - "restroomTypeFemale": "Female", - "@restroomTypeFemale": { - "description": "Restroom type: female restroom" + "solarTermNext": "Next term", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" }, - "restroomTypeMale": "Male", - "@restroomTypeMale": { - "description": "Restroom type: male restroom" + "planetsTitle": "Planets", + "@planetsTitle": { + "description": "Planets page title" }, - "restroomTypeMixed": "Mixed", - "@restroomTypeMixed": { - "description": "Restroom type: mixed/unisex restroom" + "planetsSubtitle": "Where they are tonight, and how bright", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" }, - "restroomTypeAccessible": "Accessible", - "@restroomTypeAccessible": { - "description": "Restroom type: accessible restroom" + "planetsSectionTonight": "Right now", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" }, - "restroomTypeGenderNeutral": "Gender-neutral", - "@restroomTypeGenderNeutral": { - "description": "Restroom type: gender-neutral restroom" + "planetUp": "Up", + "@planetUp": { + "description": "Badge: the planet is above the horizon" }, - "restroomTypeFamily": "Family", - "@restroomTypeFamily": { - "description": "Restroom type: family restroom" + "planetDown": "Below", + "@planetDown": { + "description": "Badge: the planet is below the horizon" }, - "restroomTypeUnspecified": "Unspecified", - "@restroomTypeUnspecified": { - "description": "Restroom type: not specified" + "planetInGlare": "In glare", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" }, - "restroomCategoryTransport": "Transport", - "@restroomCategoryTransport": { - "description": "Restroom venue category: transport facility" + "planetMagnitude": "Magnitude", + "@planetMagnitude": { + "description": "Apparent visual magnitude" }, - "restroomCategoryPark": "Park", - "@restroomCategoryPark": { - "description": "Restroom venue category: park" + "planetElongation": "Elongation", + "@planetElongation": { + "description": "Angular distance from the Sun" }, - "restroomCategoryCommercial": "Commercial", - "@restroomCategoryCommercial": { - "description": "Restroom venue category: commercial establishment" + "planetSky": "Sky", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" }, - "restroomCategoryReligious": "Religious", - "@restroomCategoryReligious": { - "description": "Restroom venue category: religious / ceremonial venue" + "planetEvening": "Evening", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" }, - "restroomCategoryCultural": "Cultural", - "@restroomCategoryCultural": { - "description": "Restroom venue category: cultural / leisure activity venue" + "planetMorning": "Morning", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" }, - "restroomCategoryGovernment": "Government", - "@restroomCategoryGovernment": { - "description": "Restroom venue category: public service office" + "planetDistance": "Distance", + "@planetDistance": { + "description": "Distance from the Earth" }, - "restroomCategoryWelfare": "Welfare", - "@restroomCategoryWelfare": { - "description": "Restroom venue category: social welfare institution / gathering place" + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" }, - "restroomCategoryTourist": "Tourist", - "@restroomCategoryTourist": { - "description": "Restroom venue category: tourist area / scenic spot" + "planetAltitude": "Altitude", + "@planetAltitude": { + "description": "Height above the horizon right now" }, - "restroomCategoryLeisure": "Leisure", - "@restroomCategoryLeisure": { - "description": "Restroom venue category: leisure / entertainment venue" + "planetMercury": "Mercury", + "@planetMercury": { + "description": "Planet name" }, - "restroomCategoryOther": "Other", - "@restroomCategoryOther": { - "description": "Restroom venue category: other" + "planetVenus": "Venus", + "@planetVenus": { + "description": "Planet name" }, - "restroomGradeExcellent": "Excellent", - "@restroomGradeExcellent": { - "description": "Restroom cleanliness grade: excellent" + "planetMars": "Mars", + "@planetMars": { + "description": "Planet name" }, - "restroomGradeGood": "Good", - "@restroomGradeGood": { - "description": "Restroom cleanliness grade: good" + "planetJupiter": "Jupiter", + "@planetJupiter": { + "description": "Planet name" }, - "restroomGradeAverage": "Average", - "@restroomGradeAverage": { - "description": "Restroom cleanliness grade: average" + "planetSaturn": "Saturn", + "@planetSaturn": { + "description": "Planet name" }, - "restroomGradePoor": "Below standard", - "@restroomGradePoor": { - "description": "Restroom cleanliness grade: below standard" + "planetUranus": "Uranus", + "@planetUranus": { + "description": "Planet name" }, - "shelterAddressLabel": "Address", - "@shelterAddressLabel": { - "description": "Shelter detail address row label" + "planetNeptune": "Neptune", + "@planetNeptune": { + "description": "Planet name" }, - "shelterCapacityLabel": "Capacity", - "@shelterCapacityLabel": { - "description": "Shelter detail capacity row label" + "solarTermVernalEquinox": "Vernal Equinox", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" }, - "shelterCapacityValue": "{n} people", - "@shelterCapacityValue": { - "description": "Shelter detail capacity row value", - "placeholders": { - "n": { - "type": "int" - } - } + "solarTermPureBrightness": "Pure Brightness", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" }, - "shelterCategoryLabel": "Disaster types", - "@shelterCategoryLabel": { - "description": "Shelter detail applicable-disaster categories row label" + "solarTermGrainRain": "Grain Rain", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" }, - "shelterIndoorLabel": "Indoor shelter", - "@shelterIndoorLabel": { - "description": "Shelter detail row: whether indoor shelter is provided" + "solarTermStartOfSummer": "Start of Summer", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" }, - "shelterOutdoorLabel": "Outdoor shelter", - "@shelterOutdoorLabel": { - "description": "Shelter detail row: whether outdoor shelter is provided" + "solarTermGrainFull": "Grain Full", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" }, - "shelterVulnerableOkLabel": "Vulnerable-people friendly", - "@shelterVulnerableOkLabel": { - "description": "Shelter detail row: whether evacuees needing care can be accommodated" + "solarTermGrainInEar": "Grain in Ear", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" }, - "dpmYes": "Yes", - "@dpmYes": { - "description": "Affirmative value in the disaster-map detail sheet" + "solarTermSummerSolstice": "Summer Solstice", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" }, - "dpmNo": "No", - "@dpmNo": { - "description": "Negative value in the disaster-map detail sheet" + "solarTermMinorHeat": "Minor Heat", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" }, - "stationSheetEmpty": "Tap a station to see its reading", - "@stationSheetEmpty": { - "description": "Empty-state hint in the map station-value sheet, shown before any station is selected" + "solarTermMajorHeat": "Major Heat", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" }, - "monitorDelay": "Delay {value} s", - "@monitorDelay": { - "description": "RTS monitor latency: how far behind the latest snapshot is (calibrated now minus the snapshot timestamp), in seconds — pre-formatted to one decimal, e.g. \"0.3\"", - "placeholders": { - "value": { - "type": "String" - } - } + "solarTermStartOfAutumn": "Start of Autumn", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" }, - "monitorWaiting": "Waiting for data…", - "@monitorWaiting": { - "description": "Shown in the monitor panel before the first RTS snapshot arrives" + "solarTermEndOfHeat": "End of Heat", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" }, - "mapLegendUnit": "Unit: {unit}", - "@mapLegendUnit": { - "description": "Unit footer under a map colour legend (e.g. Unit: dBZ)", - "placeholders": { - "unit": { - "type": "String" - } - } + "solarTermWhiteDew": "White Dew", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" }, - "typhoonLegendPast": "Observed track", - "@typhoonLegendPast": { - "description": "Typhoon map legend: past/observed path" + "solarTermAutumnalEquinox": "Autumnal Equinox", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" }, - "typhoonIntensityTd": "Tropical depression", - "@typhoonIntensityTd": { - "description": "CWA class: tropical depression (past-track colour)" + "solarTermColdDew": "Cold Dew", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" }, - "typhoonPickerNamed": "{name} TY {no}", - "@typhoonPickerNamed": { - "description": "Sheet picker: named typhoon (CWA name + TY tyNo)", - "placeholders": { - "no": { - "type": "String" - }, - "name": { - "type": "String" - } - } + "solarTermFrostDescent": "Frost Descent", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" }, - "typhoonPickerTd": "Tropical depression TD {no}", - "@typhoonPickerTd": { - "description": "Sheet picker: unnamed tropical depression (CWA tdNo)", - "placeholders": { - "no": { - "type": "String" - } - } + "solarTermStartOfWinter": "Start of Winter", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" }, - "typhoonTyNo": "TY {no}", - "@typhoonTyNo": { - "description": "Secondary badge on the typhoon sheet hero: the CWA typhoon serial number, e.g. TY 4", - "placeholders": { - "no": { - "type": "String" - } - } + "solarTermMinorSnow": "Minor Snow", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" }, - "typhoonTdNo": "TD {no}", - "@typhoonTdNo": { - "description": "Secondary badge on the typhoon sheet hero: the CWA tropical-depression serial number, e.g. TD 14", - "placeholders": { - "no": { - "type": "String" - } - } + "solarTermMajorSnow": "Major Snow", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" }, - "typhoonIntensityMild": "Mild typhoon", - "@typhoonIntensityMild": { - "description": "CWA class: mild typhoon (past-track colour)" + "solarTermWinterSolstice": "Winter Solstice", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" }, - "typhoonIntensityModerate": "Moderate typhoon", - "@typhoonIntensityModerate": { - "description": "CWA class: moderate typhoon (past-track colour)" + "solarTermMinorCold": "Minor Cold", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" }, - "typhoonIntensityIntense": "Intense typhoon", - "@typhoonIntensityIntense": { - "description": "CWA class: intense typhoon (past-track colour)" + "solarTermMajorCold": "Major Cold", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" }, - "typhoonLegendForecast": "Forecast track", - "@typhoonLegendForecast": { - "description": "Typhoon map legend: forecast path" + "solarTermStartOfSpring": "Start of Spring", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" }, - "typhoonLegendForecastPoint": "Forecast point", - "@typhoonLegendForecastPoint": { - "description": "Typhoon map legend: forecast waypoint" + "solarTermRainWater": "Rain Water", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" }, - "typhoonLegendCurrent": "Current centre", - "@typhoonLegendCurrent": { - "description": "Typhoon map legend: current storm centre" + "solarTermAwakeningOfInsects": "Awakening of Insects", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" }, - "typhoonLegendCone": "Forecast cone", - "@typhoonLegendCone": { - "description": "Typhoon map legend: uncertainty cone" + "tonightTitle": "Tonight", + "@tonightTitle": { + "description": "Tonight page title" }, - "mapLegendExpand": "Legend", - "@mapLegendExpand": { - "description": "Collapsed map-legend chip label / tooltip — tap to expand" + "tonightSubtitle": "What is observable, and when", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" }, - "mapLegendCollapse": "Hide legend", - "@mapLegendCollapse": { - "description": "Tooltip on the control that collapses the map legend" + "tonightSectionDark": "Observing window", + "@tonightSectionDark": { + "description": "Section header: the observing window" }, - "mapMyLocation": "My location", - "@mapMyLocation": { - "description": "Map control that centers the camera on the device GPS fix" + "tonightAstronomicalNight": "Astronomical night", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" }, - "mapResetNorth": "Reset north", - "@mapResetNorth": { - "description": "Map compass tooltip: re-points the camera to north-up" + "tonightNeverDark": "Never fully dark", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" }, - "typhoonLegendCircle15": "Gale circle (L7)", - "typhoonLegendCircleAvg": "Average circle", - "@typhoonLegendCircleAvg": { - "description": "Legend for the purple dashed mean-radius storm circle" + "tonightDarkWindow": "Dark window", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" }, - "typhoonLegendCircle25": "Storm circle (L10)", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "@typhoonStormRadii": { - "description": "Per-quadrant storm-wind radii (km) for a typhoon circle", - "placeholders": { - "ne": { - "type": "String" - }, - "se": { - "type": "String" - }, - "sw": { - "type": "String" - }, - "nw": { - "type": "String" - } - } + "tonightMoonAllNight": "Moon up all night", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "Total dark", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "Moonlight", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" }, - "typhoonTimeChip": "{day}日{hour}時", - "@typhoonTimeChip": { - "description": "Compact typhoon time chip / map label shape (day + hour, no month)", - "placeholders": { - "day": { - "type": "String" - }, - "hour": { - "type": "String" - } - } + "tonightSectionShowers": "Meteor showers", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" }, - "typhoonLegendProbability": "Strike probability", - "typhoonLegendWarningAreas": "Warning areas", - "typhoonOverlayMenuTooltip": "Typhoon overlay options", - "@typhoonOverlayMenuTooltip": { - "description": "Tooltip for the typhoon overlay-toggle chip beside the layer switcher" + "tonightRadiantDown": "Radiant never rises", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" }, - "typhoonOverlaySectionStorm": "Storm wind", - "@typhoonOverlaySectionStorm": { - "description": "Section header for L7/L10 storm-band choices in the overlay menu" + "tonightPerHour": "/h", + "@tonightPerHour": { + "description": "Unit: meteors per hour" }, - "typhoonOverlaySectionExtra": "Overlays", - "@typhoonOverlaySectionExtra": { - "description": "Section header for optional typhoon overlays (probability, warning)" + "tonightSectionSatellites": "Satellite passes", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" }, - "typhoonOverlayStormBandSubtitle": "With average circle", - "@typhoonOverlayStormBandSubtitle": { - "description": "Subtitle under each storm-band option (fill + dashed avg)" + "tonightSectionTargets": "Targets up now", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" }, - "typhoonOverlayProbabilityHint": "Hides the forecast cone", - "@typhoonOverlayProbabilityHint": { - "description": "Short hint under the strike-probability toggle" + "showerQuadrantids": "Quadrantids", + "@showerQuadrantids": { + "description": "Meteor shower name" }, - "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", - "@typhoonOverlayProbabilityTooltip": { - "description": "Tooltip for the strike-probability toggle; notes mutual exclusion with the cone" + "showerLyrids": "Lyrids", + "@showerLyrids": { + "description": "Meteor shower name" }, - "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", - "@typhoonOverlayWarningTooltip": { - "description": "Tooltip for the warning-areas overlay toggle" + "showerEtaAquariids": "Eta Aquariids", + "@showerEtaAquariids": { + "description": "Meteor shower name" }, - "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", - "@typhoonOverlayStormL7Tooltip": { - "description": "Tooltip for the L7 storm-band radio option" + "showerDeltaAquariids": "Delta Aquariids", + "@showerDeltaAquariids": { + "description": "Meteor shower name" }, - "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", - "@typhoonOverlayStormL10Tooltip": { - "description": "Tooltip for the L10 storm-band radio row" + "showerPerseids": "Perseids", + "@showerPerseids": { + "description": "Meteor shower name" }, - "typhoonOverlaySectionWeather": "Weather underlay", - "@typhoonOverlaySectionWeather": { - "description": "Overlay-menu section for radar / IR under the typhoon vectors" + "showerOrionids": "Orionids", + "@showerOrionids": { + "description": "Meteor shower name" }, - "typhoonOverlayWeatherNone": "None", - "@typhoonOverlayWeatherNone": { - "description": "No radar or satellite underlay" + "showerSouthernTaurids": "Southern Taurids", + "@showerSouthernTaurids": { + "description": "Meteor shower name" }, - "typhoonOverlayWeatherHint": "Aligned to bulletin time", - "@typhoonOverlayWeatherHint": { - "description": "Subtitle: weather tile matches typhoon report time" + "showerLeonids": "Leonids", + "@showerLeonids": { + "description": "Meteor shower name" }, - "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", - "@typhoonOverlayWeatherNoneTooltip": { - "description": "Tooltip for clearing the weather underlay" + "showerGeminids": "Geminids", + "@showerGeminids": { + "description": "Meteor shower name" }, - "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", - "@typhoonOverlayWeatherRadarTooltip": { - "description": "Tooltip for radar underlay (mutex with IR)" + "showerUrsids": "Ursids", + "@showerUrsids": { + "description": "Meteor shower name" }, - "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", - "@typhoonOverlayWeatherSatelliteTooltip": { - "description": "Tooltip for Himawari IR underlay (mutex with radar)" + "deepSkyOpenCluster": "Open cluster", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" }, - "typhoonWarningTitle": "Typhoon warning", - "typhoonWarningAreas": "Areas: {areas}", - "typhoonTrackDetail": "Track detail", - "typhoonHistoryTitle": "Dataset time", - "typhoonHistoryLive": "Live", - "typhoonSatelliteTitle": "Satellite", - "@typhoonWarningAreas": { - "description": "List of counties under a typhoon warning", - "placeholders": { - "areas": { - "type": "String" - } - } + "deepSkyGlobularCluster": "Globular cluster", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" }, - "@typhoonLegendCircle15": { - "description": "Typhoon UI: typhoonLegendCircle15" + "deepSkySpiralGalaxy": "Spiral galaxy", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" }, - "@typhoonLegendCircle25": { - "description": "Typhoon UI: typhoonLegendCircle25" + "deepSkyEllipticalGalaxy": "Elliptical galaxy", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" }, - "@typhoonLegendProbability": { - "description": "Typhoon UI: typhoonLegendProbability" + "deepSkyIrregularGalaxy": "Irregular galaxy", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" }, - "@typhoonLegendWarningAreas": { - "description": "Typhoon UI: typhoonLegendWarningAreas" + "deepSkyPlanetaryNebula": "Planetary nebula", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" }, - "@typhoonWarningTitle": { - "description": "Typhoon UI: typhoonWarningTitle" + "deepSkySupernovaRemnant": "Supernova remnant", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" }, - "@typhoonTrackDetail": { - "description": "Typhoon UI: typhoonTrackDetail" + "deepSkyEmissionNebula": "Emission nebula", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" }, - "@typhoonHistoryTitle": { - "description": "Typhoon UI: typhoonHistoryTitle" + "deepSkyReflectionNebula": "Reflection nebula", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" }, - "@typhoonHistoryLive": { - "description": "Typhoon UI: typhoonHistoryLive" + "deepSkyAsterism": "Asterism", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" }, - "@typhoonSatelliteTitle": { - "description": "Typhoon UI: typhoonSatelliteTitle" + "almanacTitle": "Almanac", + "@almanacTitle": { + "description": "Almanac page title" }, - "typhoonOverlayForecastCallouts": "Forecast tooltips", - "@typhoonOverlayForecastCallouts": { - "description": "Overlay menu: toggle forecast-point Flutter callout cards" + "almanacSubtitle": "The lunisolar date and the eclipses ahead", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" }, - "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", - "@typhoonOverlayForecastCalloutsTooltip": { - "description": "Tooltip for the forecast callouts overlay toggle" + "almanacSectionToday": "Today", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" }, - "dpmFilterSectionRestroom": "Venue types", - "@dpmFilterSectionRestroom": { - "description": "Filter section title in the disaster-map sheet: restroom venue categories" + "almanacGregorian": "Gregorian", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "dpmFilterSectionRestroomType": "Toilet types", - "@dpmFilterSectionRestroomType": { - "description": "Filter section title in the disaster-map sheet: restroom toilet-kind categories" + "almanacLunar": "Lunisolar", + "@almanacLunar": { + "description": "The lunisolar date" }, - "dpmFilterSectionShelter": "Shelter disaster types", - "@dpmFilterSectionShelter": { - "description": "Filter section title in the disaster-map sheet: shelter disaster types" + "almanacYear": "Year", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "dpmDisasterFlood": "Flood", - "@dpmDisasterFlood": { - "description": "Shelter disaster-type filter chip: flood" + "almanacMonthLength": "Month length", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "dpmDisasterEarthquake": "Earthquake", - "@dpmDisasterEarthquake": { - "description": "Shelter disaster-type filter chip: earthquake" + "almanacLongMonth": "30 days", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "dpmDisasterLandslide": "Landslide", - "@dpmDisasterLandslide": { - "description": "Shelter disaster-type filter chip: landslide" + "almanacShortMonth": "29 days", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "dpmDisasterTsunami": "Tsunami", - "@dpmDisasterTsunami": { - "description": "Shelter disaster-type filter chip: tsunami" + "almanacLeapPrefix": "Leap ", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - "dpmDisasterSlope": "Slope hazard", - "@dpmDisasterSlope": { - "description": "Shelter disaster-type filter chip: slope hazard" + "almanacSectionLunarEclipses": "Lunar eclipses", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "dpmDisasterNuclear": "Nuclear accident", - "@dpmDisasterNuclear": { - "description": "Shelter disaster-type filter chip: nuclear accident" + "almanacSectionSolarEclipses": "Solar eclipses", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTime": "Sky time", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacNoSolarEclipse": "None in range", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeAuto": "Auto", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "eclipseTotal": "Total", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeDawn": "Dawn", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "eclipsePartial": "Partial", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeSunrise": "Sunrise", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseAnnular": "Annular", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeMorning": "Morning", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePenumbral": "Penumbral", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeNoon": "Noon", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "zodiacRat": "Rat", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeAfternoon": "Afternoon", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "zodiacOx": "Ox", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeGolden": "Golden hour", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacTiger": "Tiger", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "Sunset", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacRabbit": "Rabbit", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "Dusk", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacDragon": "Dragon", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "Night", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacSnake": "Snake", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "Cloudy", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacHorse": "Horse", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "Overcast", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacGoat": "Goat", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "Snow", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacMonkey": "Monkey", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "Dust", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacRooster": "Rooster", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "Show scan range", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacDog": "Dog", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "Outlines the area the four radars actually observe.", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacPig": "Pig", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "Blank outside means unobserved", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "tideTitle": "Tide", + "@tideTitle": { + "description": "Tide page title" }, - "radarOverlayMenuTooltip": "Radar overlay options", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "tideSubtitle": "Spring, neap and the pull of the Moon", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarCountyOutline": "County borders", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideDisclaimer": "Astronomical forcing only — not a harbour tide table. For water levels use the CWA's published tables.", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarGlobalOutline": "National borders", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSectionNow": "Right now", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarGlobalOutlineHint": "Every country's outer frame", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tidePhase": "Cycle", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarCountyOutlineHint": "Drawn over the echo", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSpring": "Spring", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarCountyOutlineSubtitle": "Keeps county borders legible under the radar echo.", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideNeap": "Neap", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutline": "Township borders", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "Middling", + "@tideMiddling": { + "description": "Between spring and neap" }, - "radarTownOutlineHint": "The finer mesh", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideLunarDistanceFactor": "Lunar pull", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "radarTownOutlineSubtitle": "Keeps township borders legible under the radar echo.", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideEquilibrium": "Equilibrium tide", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "qpesumsOverlayMenuTooltip": "QPESUMS overlay options", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideMetres": "m", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastOverlayMenuTooltip": "Wind forecast overlay options", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tidePerigeanSpring": "Next perigean spring", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastCountyOutlineHint": "Drawn over the wind field", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "Turning points", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "windForecastGlobalOutlineHint": "Every country's outer frame", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tideHigh": "High", + "@tideHigh": { + "description": "A high point of the tidal forcing" }, - "windForecastTownOutlineHint": "The finer mesh", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideLow": "Low", + "@tideLow": { + "description": "A low point of the tidal forcing" }, - "eewSerial": "Report {serial}", - "@eewSerial": { - "description": "The serial (report number) of an EEW alert", - "placeholders": {"serial": {"type": "int"}} + "skyChartTitle": "Sky chart", + "@skyChartTitle": { + "description": "Sky chart page title" }, - "eewMaxIntensity": "Max intensity", - "@eewMaxIntensity": { - "description": "Label for an EEW alert's maximum felt intensity badge" + "skyChartSubtitle": "The naked-eye sky above you", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" }, - "eewLocalIntensity": "Estimated at my location", - "@eewLocalIntensity": { - "description": "Label for the estimated felt intensity at the user's location" + "skyChartNorth": "N", + "@skyChartNorth": { + "description": "Compass point on the sky chart" }, - "eewSWave": "S-wave", - "@eewSWave": { - "description": "Label for the S-wave arrival countdown tile" + "skyChartEast": "E", + "@skyChartEast": { + "description": "Compass point on the sky chart" }, - "eewArrived": "Arrived", - "@eewArrived": { - "description": "S-wave arrival countdown state once the wave has arrived" + "skyChartSouth": "S", + "@skyChartSouth": { + "description": "Compass point on the sky chart" }, - "eewCountdown": "{seconds} s", - "@eewCountdown": { - "description": "S-wave arrival countdown in seconds", - "placeholders": {"seconds": {"type": "int"}} + "skyChartWest": "W", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "elements {days} d old", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}month {month}, day {day}", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "No shower running", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "No visible pass in 48 h", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "Orbit data unavailable", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "Nothing high enough", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "Star catalogue unavailable", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" } } diff --git a/lib/l10n/app_fil.arb b/lib/l10n/app_fil.arb index ecdb128a7..1d6fa72b2 100644 --- a/lib/l10n/app_fil.arb +++ b/lib/l10n/app_fil.arb @@ -1,679 +1,1739 @@ { - "@@locale": "fil", - "languageName": "Filipino", - "navHome": "Tahanan", - "navEvents": "Mga Kaganapan", - "navMap": "Mapa", - "navData": "Datos", - "navEarthquake": "Lindol", - "dataSectionSeismic": "Seismic", - "dataEarthquakeSubtitle": "Mga ulat ng lindol", - "dataSectionWeather": "Panahon", - "dataWeatherRankingSubtitle": "Live na ranggo ng istasyon", - "weatherRankingTitle": "Mga ranggo ng obserbasyon", - "weatherRankingMeta": "Oras ng datos: {time}\n{count} istasyon", - "weatherRankingEmpty": "Walang obserbasyon na iraranggo", - "weatherRankingBy": "Ayon sa", - "weatherRankingHighest": "Pinakamataas", - "weatherRankingLowest": "Pinakamababa", - "weatherRankingMergeTo": "Pagsamahin", - "weatherRankingMergeTown": "Bayan", - "weatherRankingMergeCounty": "Lalawigan", - "weatherRankingWind": "Bilis ng hangin", - "weatherRankingGust": "Bugso", + "typhoonValueLat": "{lat}°N", + "onboardingSkipBody": "Kung walang lokasyon at mga notification, hindi ka maaalertuhan ng DPIP nang real time sa mga lindol at sakuna malapit sa iyo. Maaari mo pa ring ibigay ang mga ito sa ibang pagkakataon sa Settings.", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 oras", + "homeRainTrendHeavyStopping": "Baka huminto ang malakas na ulan sa loob ng {minutes} minuto", + "mapTimelineObserved": "Naobserbahan", + "regionSelectTitle": "Pumili ng rehiyon", + "skyTimeNoon": "Tanghali", + "radarCountyOutlineSubtitle": "Nananatiling mababasa ang mga hangganan sa ilalim ng radar echo.", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "Mga uri ng banyo", + "mapLayerSatelliteB03": "Himawari Red (B03)", + "reportFilterIntensity": "Intensity", + "mapLayerLightning": "Kidlat", + "restroomTypeMale": "Palikuran ng lalaki", + "meshtasticLastReceived": "Last received", + "reportDetailSortByCounty": "Ayusin ayon sa lalawigan", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "Posibleng mahinang ulan", + "meshtasticUptime": "Uptime", "weatherRankingTempExtremes": "Mga sukdulan ng temperatura", - "weatherRankingExtremeHigh": "Pinakamataas ngayong araw", - "weatherRankingExtremeLow": "Pinakamababa ngayong araw", + "themeLight": "Maliwanag", + "mapTerrainReliefHint": "Ipakita ang anino ng terrain sa base map", + "meshtasticEmptyMessage": "(empty message)", + "moreSectionRegion": "Rehiyon", + "dpmDisasterEarthquake": "Lindol", + "mapLayerSatellite": "Himawari Infrared (B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "Oras sa Sabado", + "dpmDisasterSlope": "Panganib sa dalisdis", + "moonPhaseNew": "New moon", + "notifySectionEew": "Maagang babala sa lindol", + "mapResetNorth": "Bumalik sa hilaga", + "rainInterval2d": "2 araw", + "mapTownLabelsHint": "Ipakita ang mga pangalan ng bayan kapag naka-zoom", + "commonCancel": "Cancel", + "notifyOptTsunamiWarning": "Mga babala sa tsunami lamang", + "mapLayerSatelliteBtdFog": "Himawari Night Fog", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "Advanced", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "Saklaw sa araw", + "notifySettingsMenu": "Mga setting ng notipikasyon", + "typhoonHistoryTitle": "Dataset time", + "mapAppDefault": "{app} (default)", + "trendRange24h": "24 oras", + "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", "weatherRankingRecordedAt": "Naitala noong {time}", - "weatherRankingAnalysisCurrent": "Ngayon {value}°C", - "weatherRankingAnalysisHigh": "Mataas {value}", - "weatherRankingAnalysisLow": "Mababa {value}", - "weatherRankingAnalysisRange": "Saklaw {value}°C", - "reportListEmpty": "Walang ulat ng lindol", - "reportListEmptyFiltered": "Walang ulat na tumutugma sa mga filter", - "reportListMeta": "M{magnitude} · {depth} km", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "km", - "reportListLocalFelt": "Lokal na naramdaman", - "reportListToday": "Ngayon", - "reportListYesterday": "Kahapon", - "reportListDayCount": "{count}", - "reportListEnd": "Dulo ng listahan", - "reportFilterTitle": "Mga filter", - "reportFilterSort": "Pagkakasunud-sunod", - "reportFilterSortTime": "Oras", - "reportFilterSortIntensity": "Intensity", - "reportFilterSortMagnitude": "Magnitude", - "reportFilterSortDepth": "Lalim", + "mapLayerRain": "Ulan", + "mapLayerQpesums": "Pagtaya ng ulan sa susunod na 1 oras", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "Mapa", + "mapTerrainRelief": "Rehiyebo ng terrain", + "eewMaxIntensity": "Pinakamataas na intensidad", + "mapLegendCollapse": "Itago ang alamat", + "changelogTitle": "Changelog", "reportFilterOrderDesc": "Pababa", - "reportFilterOrderAsc": "Pataas", - "reportFilterIntensity": "Intensity", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", "reportFilterIntensityInfoTitle": "Bagong at lumang intensity scale", - "reportFilterIntensityInfoIntro": "Pinalitan ng CWA ang intensity scale noong 1 Ene 2020 (oras ng Taipei).", - "reportFilterIntensityInfoLegacyTitle": "Luma (bago ang 2020)", - "reportFilterIntensityInfoLegacyBody": "Antas 0–7 lang; walang 5−/5+/6−/6+.", - "reportFilterIntensityInfoModernTitle": "Bago (mula 2020)", - "reportFilterIntensityInfoModernBody": "Antas 0–4, 5−, 5+, 6−, 6+, 7. Gamit ng filter ang bagong scale; ang mga lumang event ay may legacy label sa listahan.", - "reportFilterMagnitude": "Magnitude", - "reportFilterDepth": "Depth", - "reportFilterDepthKm": "{depth} km", - "reportFilterDate": "Petsa", - "reportFilterDatePick": "Pumili ng petsa", - "reportFilterDateStartNote": "Start day: from 00:00(Taipei)", + "mapLayerTyphoon": "Bagyo", + "radarOverlayMenuTooltip": "Mga opsyon sa layer ng radar", + "mapMyLocation": "Aking lokasyon", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "Nodes", + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", + "aedType": "Uri", + "termsOfService": "Mga Tuntunin ng Serbisyo", + "typhoonLegendCircle25": "Storm circle (L10)", + "sponsorTitle": "Suportahan ang DPIP", + "mapNavSatellite": "Satellite", + "homeRainTrendUpdated": "Na-update {time}", + "onboardingNext": "Susunod", + "weatherRankingMergeTown": "Bayan", + "mapLayerMonitor": "Seismic Monitor", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "Mga subscription", + "typhoonValueLon": "{lon}°E", + "skyTime": "Oras ng langit", + "weatherModeCloudy": "Maulap", + "skyTimeDusk": "Takipsilim", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "End day: through 24:00(Taipei)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "Lokasyon", - "reportFilterLocationHint": "hal. Hualien, offshore", - "reportFilterAny": "Lahat", - "reportFilterApply": "I-apply", - "reportFilterReset": "I-reset", - "reportListSearch": "Maghanap", - "reportDetailTitle": "Ulat ng Lindol", - "reportDetailNumbered": "Blg. {number} Makabuluhang Naramdamang Lindol", - "reportDetailLocalFelt": "Lokal na Naramdamang Lindol", - "reportDetailInfo": "Mga Detalye", - "reportDetailOriginTime": "Oras ng pangyayari", - "reportDetailEpicenter": "Coordinates ng Epicenter", - "reportDetailMagnitude": "Magnitude", - "reportDetailDepth": "Lalim ng Hypocenter", - "reportDetailAreaIntensity": "Intensity ayon sa lugar", - "reportDetailLocalIntensity": "Intensity sa iyong lokasyon", - "reportDetailLocalIntensityUnavailable": "Walang datos ng intensity", - "reportDetailSortByIntensity": "Ayusin ayon sa intensity", - "reportDetailSortByCounty": "Ayusin ayon sa lalawigan", - "reportDetailImage": "Larawan ng Ulat", - "reportDetailImageUnavailable": "Wala pang available na larawan ng ulat", - "reportDetailOpenReport": "Pahina ng Ulat", - "reportDetailReplay": "I-replay", - "navMore": "Higit Pa", - "appLogs": "Mga log ng app", - "changelogTitle": "Changelog", - "changelogEmpty": "Wala pang release notes", - "changelogTypePrerelease": "Beta", - "changelogTypeStable": "Stable", - "changelogCurrentVersion": "Kasalukuyan", - "changelogVersionDetails": "Detalye ng release", - "changelogBodyEmpty": "Walang tala para sa release na ito.", - "mapPlaceholderDisabled": "Mapa (pansamantalang naka-disable)", - "moreSectionRegion": "Rehiyon", - "moreSectionNotify": "Mga Abiso", - "moreSectionDisplay": "Display", - "regionManageTitle": "Mga naka-save na rehiyon", - "regionAddButton": "Magdagdag ng rehiyon", - "regionEmpty": "Wala pang naka-save na rehiyon", - "regionSelectTitle": "Pumili ng rehiyon", - "regionSelectCount": "{count}/{max} ang napili", - "regionSelectFull": "Maaari kang mag-save ng hanggang {max} na rehiyon", - "regionEdit": "I-edit", - "moreSectionAdvanced": "Advanced", - "moreDeveloper": "Impormasyon sa debug", - "experimentalFeatures": "Mga experimental na feature", - "moreSectionLinks": "Mga Link", - "moreCwaEew": "Maagang babala sa lindol ng CWA", - "moreTremReport": "Ulat ng pagtukoy ng TREM", - "moreServerStatus": "Katayuan ng server", - "moreAnnouncements": "Mga Anunsyo", - "moreDiscord": "Komunidad sa Discord", - "moreNotifyLog": "Log ng notipikasyon ng DPIP", - "moreLinkOpenFailed": "Hindi mabuksan ang link", - "weatherDynamicState": "Animation ng panahon", - "weatherDynamicStateSubtitle": "I-override ang panahon sa background ng home", - "weatherModeAuto": "Awtomatiko", - "weatherModeClear": "Maaliwalas", - "weatherModeRain": "Ulan", - "weatherModeFog": "Makapal na Hamog", - "weatherModeThunderstorm": "Kulog at Kidlat", - "commonLoading": "Naglo-load…", - "commonRetry": "Subukan Muli", - "commonError": "May Nangyaring Mali", - "commonFetchFailed": "Hindi ma-load ang data. Pakisubukan muli.", - "commonEmpty": "Walang Maipakita", - "feedConnecting": "Kumokonekta…", - "feedStale": "Maaaring luma na ang datos", - "feedOffline": "Nawala ang koneksyon", - "eewTitle": "Maagang babala sa lindol", - "eewNone": "Walang aktibong maagang babala sa lindol", - "eewSummary": "M{magnitude} · lalim {depth} km", - "regionNationwide": "Buong bansa", - "regionCurrent": "Kasalukuyang lokasyon", - "regionCurrentUnavailable": "Hindi makuha ang kasalukuyang lokasyon", - "weatherPrecipitation": "Pag-ulan", - "weatherHumidity": "Halumigmig", - "weatherDataTime": "{station} · Oras ng datos {time}", - "homeViewOnMap": "Tingnan sa mapa", - "homeForecastTitle": "24-oras na forecast", + "meshtasticSilent": "Silent", + "reportFilterSortMagnitude": "Magnitude", + "mapLayerCategoryEarthquake": "Lindol", + "mapLayerSatelliteB12": "Himawari Ozone (B12)", + "typhoonLegendPast": "Aktwal na landas", + "restroomCategoryOther": "Iba pa", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "T {high}° · B {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "Pakiramdam {temp}°", - "homeForecastHumidity": "Halumigmig {value}%", - "homeForecastWind": "{direction} · Force {level}", - "homeForecastUnavailable": "Pumili ng bayan para makita ang forecast", - "homeForecastEmpty": "Walang forecast", - "homeActiveEventsTitle": "Mga aktibong event", - "homeActiveEventsEmpty": "Walang aktibong event", - "homeRainTrendTitle": "Ulan sa susunod na oras", - "homeRainTrendMinute": "{minute} min", - "homeRainTrendUpdated": "Na-update {time}", - "homeRainTrendNoData": "Walang data", - - "homeRainTrendScattered": "Posibleng mahinang ulan", - "homeRainTrendLightSustained": "Tuloy-tuloy na mahinang ulan sa susunod na oras", - "homeRainTrendLightStopping": "Baka huminto ang mahinang ulan sa loob ng {minutes} minuto", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "Buksan ang mga setting", + "mapLegendExpand": "Alamat", + "eewNone": "Walang aktibong maagang babala sa lindol", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "Mga abiso at babala sa tsunami", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "Node options", + "onboardingAgreeContinue": "Sumang-ayon at magpatuloy", + "meshtasticNodeId": "Node ID", + "commonRetry": "Subukan Muli", + "reportDetailNumbered": "Blg. {number} Makabuluhang Naramdamang Lindol", + "typhoonOverlayStormBandSubtitle": "With average circle", + "disasterMapOverlayRestroomTooltip": "Ipakita ang mga pampublikong palikuran", + "weatherRankingTitle": "Mga ranggo ng obserbasyon", "homeRainTrendHeavySustained": "Tuloy-tuloy na malakas na ulan sa susunod na oras", - "homeRainTrendHeavyStopping": "Baka huminto ang malakas na ulan sa loob ng {minutes} minuto", - "mapLayers": "Mga Layer", - "mapLayerOrderTitle": "Ayusin ang ayos ng layer", - "mapLayerOrderReset": "I-reset ang ayos", - "mapLayerRadar": "Composite Radar Reflectivity", - "mapLayerSatellite": "Himawari Infrared (B13)", - "mapLayerSatelliteB01": "Himawari Blue (B01)", - "mapLayerSatelliteB02": "Himawari Green (B02)", - "mapLayerSatelliteB03": "Himawari Red (B03)", - "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "notifySectionTsunami": "Tsunami", + "restroomCategoryPark": "Parke", + "moreLinkOpenFailed": "Hindi mabuksan ang link", + "themeDark": "Madilim", + "sponsorRestore": "Ibalik ang mga pagbili", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "Traffic", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", + "disasterMapOverlayAedTooltip": "Show AED locations", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "Halumigmig", + "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", + "meshtasticScanning": "Scanning…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "Maaari kang mag-save ng hanggang {max} na rehiyon", + "meshtasticTitle": "Meshtastic", + "navMore": "Higit Pa", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "Layers", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "Himawari Near-Infrared (B05)", - "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", - "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", - "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", - "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", - "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", - "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", - "mapLayerSatelliteB12": "Himawari Ozone (B12)", - "mapLayerSatelliteB13": "Himawari Infrared (B13)", - "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", - "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", - "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "NE", + "meshtasticCopied": "Message copied", + "reportListEmpty": "Walang ulat ng lindol", + "reportListEnd": "Dulo ng listahan", "mapLayerSatelliteTruecolor": "Himawari True Color", - "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", - "mapLayerSatelliteAsh": "Himawari Ash", - "mapLayerSatelliteDust": "Himawari Dust", - "mapLayerSatelliteAirmass": "Himawari Airmass", - "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", - "mapLayerSatelliteWatervapor": "Himawari Water Vapour", - "mapLayerSatelliteBtdSplit": "Himawari Split Window", - "mapLayerSatelliteBtdFog": "Himawari Night Fog", - "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", - "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", - "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", - "mapLayerSatelliteBtdOzone": "Himawari Tropopause", - "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", - "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", - "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", - "mapLayerSatelliteNdvi": "Himawari NDVI", - "mapLayerSatelliteNdwi": "Himawari NDWI", - "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionExtra": "Overlays", + "eewSWave": "S wave", + "meshtasticBusyTitle": "Another app is using this radio", + "restroomCategoryCultural": "Pook na pangkultura", + "typhoonLabelWind": "Max. sustained wind near centre", + "radarGlobalOutlineHint": "Panlabas na balangkas ng bawat bansa", + "notifyEvacuation": "Impormasyon sa sakuna", + "typhoonLegendCircle15": "Gale circle (L7)", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "Astronomy", + "homeRainTrendLightSustained": "Tuloy-tuloy na mahinang ulan sa susunod na oras", + "commonError": "May Nangyaring Mali", + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "Ngayon", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "Pahina ng Ulat", + "trendRange7d": "7 araw", + "typhoonWarningAreas": "Areas: {areas}", + "rainIntervalSection": "Window ng oras", + "notifyTitle": "Mga Notipikasyon", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "Kategorya", + "sponsorRestoring": "Ibinabalik ang mga pagbili…", + "sponsorIntro": "Nakatuon ang DPIP sa pagbibigay ng real-time na impormasyon sa pag-iwas sa sakuna, nang walang ad o iba pang modelo ng kita. Tumutulong ang inyong suporta na mapanatili ang mga server at magpatuloy sa pagbuo.", + "shelterAddressLabel": "Address", + "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "Komersyal na establisyimento", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "Rehiyon", + "homeRainTrendLightStopping": "Baka huminto ang mahinang ulan sa loob ng {minutes} minuto", + "reportDetailInfo": "Mga Detalye", + "mapNavWind": "Hangin", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "Mga opsyon sa layer ng pagtataya ng hangin", + "dataWeatherRankingSubtitle": "Live na ranggo ng istasyon", + "rainInterval6h": "6 oras", + "homeRainTrendMinute": "{minute} min", + "restroomTypeUnspecified": "Hindi natukoy", + "typhoonOverlayProbabilityHint": "Hides the forecast cone", "mapLayerSatelliteGlobalOutline": "Country border", - "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", - "mapLayerSatelliteCloudClear": "Clear", - "mapLayerSatelliteCloudProbablyClear": "Probably clear", - "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "mapNavTemperature": "Temperatura", + "typhoonLegendForecastPoint": "Punto ng forecast", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "Kahapon", + "moreSectionLinks": "Mga Link", + "feedOffline": "Nawala ang koneksyon", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "Display", + "rainInterval3d": "3 araw", + "defaultMapLayerSubtitle": "Bubukas ang tab ng Mapa sa layer na ito. Susunod ang icon at label ng bottom navigation.", + "aedDescription": "Tala", + "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", + "onboardingPermLocationDesc": "Itutok ang mga alerto sa kinaroroonan mo.", + "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "Walang aktibong event", + "typhoonLabelPosition": "Centre location", + "weatherRankingBy": "Ayon sa", + "typhoonIntensityMild": "Mild typhoon", + "windForecastGlobalOutlineHint": "Panlabas na balangkas ng bawat bansa", + "rainInterval1h": "1 oras", + "eewLocalIntensity": "Tantiya sa lokasyon", + "mapLayerRadar": "Composite Radar Reflectivity", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "Relihiyosong lugar", + "meshtasticRole": "Role", "mapLayerSatelliteCloudCloudy": "Cloudy", - "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", - "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", - "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", - "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", - "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", - "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", - "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "skyTimeSunrise": "Pagsikat ng araw", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "No messages yet", + "onboardingPermNotifyDesc": "Ihatid ang mga alerto sa lindol, panahon, at sakuna sa sandaling maganap ang mga ito.", + "radarTownOutline": "Mga hangganan ng bayan", "mapLayerStyleSection": "Colour style", - "mapLayerStyleTooltip": "Colour style", - "mapLayerStyleGray": "Grayscale (JMA)", - "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", - "mapLayerStyleJma": "Cloud-top enhancement (JMA)", - "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", - "mapLayerQpesums": "Pagtaya ng ulan sa susunod na 1 oras", - "mapLayerLightning": "Kidlat", - "lightningLegendCg": "Ulap–lupa · {minutes} min", - "lightningLegendCc": "Ulap–ulap · {minutes} min", - "mapTimelineNow": "Ngayon", - "mapTimelinePast": "Nakaraan", - "mapTimelineFuture": "Hinaharap", - "mapTimelineObserved": "Naobserbahan", - "mapTimelineForecast": "Pagtaya", - "mapTimelineDataTime": "Oras ng data {time}", - "notifySettingsMenu": "Mga setting ng notipikasyon", - "notifyTitle": "Mga Notipikasyon", - "notifyUnavailable": "Hindi pa handa ang push notifications — subukan muli mamaya.", - "notifySetFailed": "Hindi ma-save ang setting. Pakisubukan muli.", - "notifySectionEew": "Maagang babala sa lindol", - "notifySectionEarthquake": "Lindol", - "notifySectionWeather": "Panahon", - "notifySectionTsunami": "Tsunami", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "Disaster map layers", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "SW", + "typhoonForecastLead": "Forecast +{hours} h", + "dpmDisasterTsunami": "Tsunami", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "Stable", + "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "Layer ng sanggunian", + "mapLayerSatelliteB02": "Himawari Green (B02)", + "reportListLocalFelt": "Lokal na naramdaman", + "weatherRankingEmpty": "Walang obserbasyon na iraranggo", "notifySectionOther": "Iba pa", - "notifyEew": "Emergency na alerto sa lindol", - "notifyMonitor": "Monitor ng malakas na paggalaw", - "notifyReport": "Ulat ng lindol", - "notifyIntensity": "Ulat ng intensidad", - "notifyThunderstorm": "Mga alerto sa kulog at kidlat", - "notifyAdvisory": "Mga advisory sa panahon", - "notifyEvacuation": "Impormasyon sa sakuna", - "notifyTsunami": "Impormasyon sa tsunami", - "notifyAnnouncement": "Mga Anunsyo", - "notifyOptOff": "Naka-off", - "notifyOptAll": "Tumanggap ng lahat", + "weatherRankingMeta": "Oras ng datos: {time}\n{count} istasyon", + "onboardingTermsAgree": "Nabasa ko na at sumasang-ayon ako sa Mga Tuntunin ng Serbisyo", + "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", "notifyOptLocalIntensity4": "Lokal na intensidad 4 pataas", - "notifyOptLocalIntensity1": "Lokal na intensidad 1 pataas", - "notifyOptWeatherLocal": "Kasalukuyang lokasyon lamang", - "notifyOptTsunamiWarning": "Mga babala sa tsunami lamang", - "notifyOptTsunamiAll": "Mga abiso at babala sa tsunami", - "onboardingNext": "Susunod", - "onboardingBack": "Bumalik", + "eewArrived": "Dumating", + "meshtasticNoDevices": "No Meshtastic devices found", + "mapLayerCategoryLife": "Pang-araw-araw na buhay", + "reportFilterSortIntensity": "Intensity", + "typhoonMotion": "Gumagalaw", + "meshtasticStateDisconnected": "Disconnected", + "typhoonIntensityIntense": "Intense typhoon", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "Ayusin ang ayos ng layer", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "Oo", + "meshtasticNoHistory": "Not enough history yet", + "reportDetailLocalIntensityUnavailable": "Walang datos ng intensity", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "Depth", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "Mag-scroll pababa para magpatuloy", - "onboardingIntroTitle": "Maligayang pagdating sa DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "Pagtaya", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "Mapa", + "notifyAdvisory": "Mga advisory sa panahon", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "I-reset", + "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionStorm": "Storm wind", + "moonPhaseFull": "Full moon", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "weatherDynamicStateSubtitle": "I-override ang panahon sa background ng home", + "reportFilterIntensityInfoModernTitle": "Bago (mula 2020)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "Data time\n{time}", + "restroomTypeAccessible": "Palikurang may accessibility", + "moreSectionAbout": "Tungkol", + "meshtasticSelectDevice": "Select a radio", "onboardingIntroBody": "Ang DPIP ang iyong kasama sa pag-iwas sa sakuna. Pinagsasama-sama nito ang mga maagang babala sa lindol, ulat ng lindol, panahon, at impormasyon sa panganib, at inaalertuhan ka sa sandaling mahalaga ito.\n\n• Mga lindol: mga maagang babala, ulat ng intensidad, at detalyadong ulat\n• Panahon: real-time na mensahe ng kulog at kidlat at mga advisory sa panahon\n• Impormasyon sa tsunami at sakuna\n\nSusunod, hihilingin naming basahin mo ang Mga Tuntunin ng Serbisyo at magbigay ng ilang pahintulot para maprotektahan ka ng DPIP nang real time.", - "onboardingTermsTitle": "Mga Tuntunin ng Serbisyo", - "onboardingTermsBody": "Mangyaring basahin ang mga sumusunod na paunawa bago gamitin ang DPIP:\n\n• Ang lahat ng impormasyon ay dapat sumunod sa nilalamang inilathala ng Central Weather Administration (CWA).\n\n• Depende sa kalagayan ng network, server, app, at pinagmumulan ng datos, may posibilidad na hindi matanggap ang impormasyon; ginagawa namin ang lahat ng aming makakaya upang maiwasan ito ngunit hindi namin magagarantiya na hindi ito mangyayari.\n\n• Maaaring maunang makarating sa iyong lokasyon ang malakas na pagyanig bago pa dumating ang notipikasyon.\n\n• Ang mga maagang babala sa lindol ay mabilis na kinakalkulang resulta na maaaring magtaglay ng malaking pagkakamali — unawain ito at gamitin nang may pag-iingat.\n\n• Anumang gawaing hindi pinahihintulutan ng mga awtoridad ay maaaring magdala ng panganib sa batas; mangyaring sundin ang lahat ng naaangkop na regulasyon.\n\nBukod dito, upang magbigay ng lokal na mga alerto, kinokolekta at ini-upload ng serbisyong ito ang iyong tinatayang lokasyon at push identifier — sa foreground at background — para lamang matukoy kung aling mga alerto ang ipapadala sa iyo.\n\nSa pamamagitan ng pag-tap sa \"Sumang-ayon at magpatuloy\" kinukumpirma mo na nabasa, naunawaan, at sinasang-ayunan mo ang nasa itaas.", - "onboardingTermsAgree": "Nabasa ko na at sumasang-ayon ako sa Mga Tuntunin ng Serbisyo", - "onboardingAgreeContinue": "Sumang-ayon at magpatuloy", - "onboardingPermsTitle": "Mga Pahintulot", - "onboardingPermsBody": "Para maalertuhan ka ng DPIP sa sandaling maganap ang sakuna, mangyaring ibigay ang mga sumusunod. Maaari mo itong baguhin anumang oras sa mga setting ng system.", + "shelterCapacityLabel": "Kapasidad", + "reportDetailImage": "Larawan ng Ulat", + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", "onboardingPermNotify": "Mga Notipikasyon", - "onboardingPermNotifyDesc": "Ihatid ang mga alerto sa lindol, panahon, at sakuna sa sandaling maganap ang mga ito.", - "onboardingPermCritical": "Mga kritikal na alerto", - "onboardingPermCriticalDesc": "Hayaang tumunog ang mga nakamamatay na babala sa lindol kahit sa silent mode o Do Not Disturb.", - "onboardingPermLocation": "Lokasyon", - "onboardingPermLocationDesc": "Itutok ang mga alerto sa kinaroroonan mo.", - "onboardingPermBackground": "Lokasyon sa background", - "onboardingPermBackgroundDesc": "Payagan ang \"Always\" para patuloy kang matukoy ng mga alerto kahit sarado ang app.", - "onboardingPermBattery": "Exemption sa baterya", - "onboardingPermBatteryDesc": "Payagan ang DPIP na patuloy na tumakbo sa background para hindi maantala o mapalampas ang mga alerto.", - "onboardingGrant": "Ibigay", - "onboardingGranted": "Naibigay na", - "onboardingStart": "Magsimula", - "language": "Wika", - "languageSettings": "Wika", - "languageSystem": "Default ng system", - "locationBannerServiceOff": "Naka-off ang mga serbisyo ng lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.", - "locationBannerPermission": "Naka-off ang pahintulot sa lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.", - "locationBannerFix": "Buksan ang mga setting", - "notifyBannerDisabled": "Naka-off ang mga notification — hindi ka makakatanggap ng mga alerto sa sakuna.", - "onboardingSkipTitle": "Hindi pa naibibigay ang mga pahintulot", - "onboardingSkipBody": "Kung walang lokasyon at mga notification, hindi ka maaalertuhan ng DPIP nang real time sa mga lindol at sakuna malapit sa iyo. Maaari mo pa ring ibigay ang mga ito sa ibang pagkakataon sa Settings.", - "onboardingSkipStay": "Bumalik", - "onboardingSkipLeave": "Laktawan pa rin", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "Clear messages", + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "Default na layer ng mapa", + "moreSectionNotify": "Mga Abiso", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "Hindi pa handa ang push notifications — subukan muli mamaya.", + "mapLayerOrderReset": "I-reset ang ayos", + "dpmAddress": "Address", + "weatherRankingMergeCounty": "Lalawigan", + "moreSectionApp": "Kunin ang app", + "reportFilterIntensityInfoLegacyBody": "Antas 0–7 lang; walang 5−/5+/6−/6+.", + "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", + "qpesumsOverlayMenuTooltip": "Mga opsyon sa layer ng pagtataya ng pag-ulan", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "Hinaharap", + "typhoonLegendCircleAvg": "Average circle", + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "SE", + "radarTownOutlineHint": "Mas pinong hati", + "eewCountdown": "{seconds} segundo", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "Peak gust", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "Mga Tuntunin ng Paggamit", + "restroomTypeGenderNeutral": "Palikurang neutral sa kasarian", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "Mga alerto sa kulog at kidlat", + "skyTimeGolden": "Gintong oras", + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "Ngayon {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "Pumili ng bayan para makita ang forecast", + "mapLayers": "Mga Layer", + "meshtasticHardware": "Hardware", + "languageSettings": "Wika", + "dpmDisasterNuclear": "Aksidente sa nukleyar", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "Wika", + "homeForecastFeelsLike": "Pakiramdam {temp}°", + "typhoonOverlayWeatherHint": "Aligned to bulletin time", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "Bukang-liwayway", + "skyTimeAfternoon": "Hapon", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "Typhoon warning", "moreSourceCode": "Source code", - "moreSectionApp": "Kunin ang app", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "Pagpapakita", - "defaultMapLayerSettings": "Default na layer ng mapa", - "defaultMapLayerSubtitle": "Bubukas ang tab ng Mapa sa layer na ito. Susunod ang icon at label ng bottom navigation.", - "mapNavRadar": "Radar", - "mapNavQpesums": "Pagtaya", - "mapNavSatellite": "Satellite", - "mapNavLightning": "Kidlat", - "mapNavTyphoon": "Bagyo", + "mapLayerCategoryWeather": "Obserbasyon sa panahon", + "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", + "windForecastTownOutlineHint": "Ang mas pinong mesh", + "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", + "mapAppCopyCoordinates": "Kopyahin ang coordinates", + "reportFilterIntensityInfoIntro": "Pinalitan ng CWA ang intensity scale noong 1 Ene 2020 (oras ng Taipei).", "mapNavEarthquake": "Lindol", - "mapNavTemperature": "Temperatura", - "mapNavHumidity": "Halumigmig", - "mapNavPressure": "Presyon", - "mapNavWind": "Hangin", + "typhoonGust": "Ugong", + "restroomGradeAverage": "Katamtaman", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", + "onboardingPermBackgroundDesc": "Payagan ang \"Always\" para patuloy kang matukoy ng mga alerto kahit sarado ang app.", + "mapTimelineForecast": "Pagtaya", + "restroomTypeLabel": "Uri", + "navEarthquake": "Lindol", + "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "Ulat ng Lindol", + "moreTremReport": "Ulat ng pagtukoy ng TREM", + "weatherDataTime": "{station} · Oras ng datos {time}", + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "radarCountyOutline": "Mga hangganan ng lalawigan", + "onboardingGranted": "Naibigay na", + "@mapAppCopyCoordinates": {}, + "commonClose": "Isara", + "restroomGradeLabel": "Baitang", + "rainIntervalNow": "Ngayon", + "changelogCurrentVersion": "Kasalukuyan", + "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", + "typhoonLabelPressure": "Central pressure", + "aedOpenRemark": "Tala sa oras", + "onboardingPermsBody": "Para maalertuhan ka ng DPIP sa sandaling maganap ang sakuna, mangyaring ibigay ang mga sumusunod. Maaari mo itong baguhin anumang oras sa mga setting ng system.", + "typhoonOverlaySectionWeather": "Weather underlay", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "Kasalukuyang lokasyon lamang", "mapNavRain": "Ulan", - "mapNavDisaster": "Sakuna", - "displayTheme": "Tema", + "moonDays": "days", + "mapLegendUnit": "Yunit: {unit}", + "weatherModeClear": "Maaliwalas", + "meshtasticRadio": "Radio", + "commonEmpty": "Walang Maipakita", + "mapLayerSatelliteB01": "Himawari Blue (B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "Pataas", + "reportFilterApply": "I-apply", + "reportDetailImageUnavailable": "Wala pang available na larawan ng ulat", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "Pinakamataas", + "reportDetailReplay": "I-replay", + "mapLayerRestroom": "Pampublikong Palikuran", + "restroomCategoryWelfare": "Institusyon ng kapakanan", + "restroomGradeExcellent": "Napakahusay", + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "Numerical forecast", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", "themeSystem": "Sistema", - "themeLight": "Maliwanag", - "themeDark": "Madilim", - "moreSectionAbout": "Tungkol", - "termsOfService": "Mga Tuntunin ng Serbisyo", - "faq": "Mga FAQ", - "openSourceLicenses": "Mga lisensya ng open-source", - "sponsorTitle": "Suportahan ang DPIP", - "sponsorIntro": "Nakatuon ang DPIP sa pagbibigay ng real-time na impormasyon sa pag-iwas sa sakuna, nang walang ad o iba pang modelo ng kita. Tumutulong ang inyong suporta na mapanatili ang mga server at magpatuloy sa pagbuo.", - "sponsorSubscriptions": "Mga subscription", - "sponsorRecommended": "Inirerekomenda", - "sponsorOneTime": "Isang beses", - "sponsorPerMonth": "{price} / buwan", - "sponsorRestore": "Ibalik ang mga pagbili", - "sponsorTerms": "Mga Tuntunin ng Paggamit", - "sponsorPrivacy": "Patakaran sa Privacy", - "sponsorRestoring": "Ibinabalik ang mga pagbili…", - "sponsorRestoreUnavailable": "Hindi maabot ang store. Pakisubukan muli mamaya.", - "commonClose": "Isara", + "mapLayerSatelliteNdvi": "Himawari NDVI", + "typhoonLegendForecast": "Tinatayang landas", + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "Pag-ulan", + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "I-tap ang marker sa mapa para sa detalye", + "onboardingSkipLeave": "Laktawan pa rin", + "onboardingBack": "Bumalik", + "aedPlaceDesc": "Lokasyon ng paglagay", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "Hindi pa naibibigay ang mga pahintulot", + "restroomTypeFamily": "Palikuran ng pamilya", + "typhoonValueKm": "{n} km", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "Presyon", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "Exemption sa baterya", + "typhoonLabelNw": "NW", + "dpmDisasterFlood": "Baha", + "moonPhaseWaxingCrescent": "Waxing crescent", + "restroomCategoryLeisure": "Lugar ng libangan", "mapLayerTemperature": "Temperatura", - "trendRange24h": "24 oras", - "trendRange7d": "7 araw", - "trendNoData": "Walang trend data", - "trendCumulativeTotal": "Kabuuang {total} mm", - "chartHourLabel": "{hour}h", - "mapLayerHumidity": "Halumigmig", - "mapLayerPressure": "Presyon", + "aedCategory": "Kategorya", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "Channels", + "monitorWaiting": "Naghihintay ng data…", + "typhoonOverlayForecastCallouts": "Forecast tooltips", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "Coordinates ng Epicenter", + "meshtasticVoltage": "Voltage", + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "Hangin", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "Ulan", - "rainIntervalMenu": "Bintana ng akumulasyon", - "rainIntervalNow": "Ngayon", - "rainInterval10m": "10 min", - "rainInterval1h": "1 oras", - "rainInterval3h": "3 oras", - "rainInterval6h": "6 oras", + "reportDetailMagnitude": "Magnitude", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "Intensity ayon sa lugar", "rainInterval12h": "12 oras", - "rainInterval24h": "24 oras", - "rainInterval2d": "2 araw", - "rainInterval3d": "3 araw", - "mapLayerTyphoon": "Bagyo", - "typhoonNoActive": "Walang aktibong bagyo", - "typhoonWind": "Hangin", - "typhoonGust": "Ugong", - "typhoonPressure": "Presyon", - "typhoonMotion": "Gumagalaw", - "mapLayerMonitor": "Seismic Monitor", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "Disaster Map", - "disasterMapOverlayMenuTooltip": "Disaster map layers", - "disasterMapOverlaySectionLayers": "Layers", - "disasterMapOverlayAedTooltip": "Show AED locations", - "aedAddress": "Address", - "aedRegion": "Rehiyon", - "aedCategory": "Kategorya", - "aedType": "Uri", - "aedPlaceDesc": "Lokasyon ng paglagay", - "aedDescription": "Tala", - "aedHoursWeekday": "Oras sa weekday", - "aedHoursSaturday": "Oras sa Sabado", - "aedHoursSunday": "Oras sa Linggo", - "aedOpenRemark": "Tala sa oras", - "aedEmergencyPhone": "Emergency phone", - "mapLayerRestroom": "Pampublikong Palikuran", - "mapLayerShelter": "Silungan", - "disasterMapOverlayRestroomTooltip": "Ipakita ang mga pampublikong palikuran", - "disasterMapOverlayShelterTooltip": "Ipakita ang mga silungan", - "dpmOpenInMaps": "Buksan sa mapa", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "Pagguho ng lupa", + "notifyMonitor": "Monitor ng malakas na paggalaw", + "onboardingStart": "Magsimula", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / buwan", + "mapLayerPressure": "Presyon", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", + "shelterIndoorLabel": "Silungan sa loob", + "notifyOptOff": "Naka-off", + "reportFilterSortTime": "Oras", + "mapLayerSatelliteCloudProbablyClear": "Probably clear", + "weatherModeThunderstorm": "Kulog at Kidlat", + "homeViewOnMap": "Tingnan sa mapa", + "reportFilterIntensityInfoLegacyTitle": "Luma (bago ang 2020)", + "typhoonLabelSpeed": "Past movement speed", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "Hindi mabuksan ang {app}", + "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "Pinakamababa ngayong araw", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", + "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "shelterCategoryLabel": "Mga uri ng kalamidad", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", + "meshtasticStateConnecting": "Connecting…", + "moonTitle": "Moon", + "weatherRankingGust": "Bugso", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "Mga uri ng sakuna sa silungan", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "Katayuan ng server", + "notifySectionWeather": "Panahon", + "meshtasticPreset": "Modem preset", + "dataSectionSeismic": "Seismic", + "changelogBodyEmpty": "Walang tala para sa release na ito.", + "radarGlobalOutline": "Mga hangganan ng bansa", + "notifyEew": "Emergency na alerto sa lindol", + "regionNationwide": "Buong bansa", + "moreNotifyLog": "Log ng notipikasyon ng DPIP", + "regionCurrent": "Kasalukuyang lokasyon", + "dpmFilterSectionRestroom": "Mga uri ng lugar", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "Niyebe", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "Impormasyon sa debug", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "Kidlat", + "homeForecastEmpty": "Walang forecast", + "sponsorOneTime": "Isang beses", + "mapLayerSatelliteBtdSplit": "Himawari Split Window", + "onboardingPermBackground": "Lokasyon sa background", + "aedEmergencyPhone": "Emergency phone", + "dpmOpenInMaps": "Buksan sa mapa", + "meshtasticNotifyNodes": "Notify on new nodes", + "onboardingPermCriticalDesc": "Hayaang tumunog ang mga nakamamatay na babala sa lindol kahit sa silent mode o Do Not Disturb.", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", + "meshtasticSent": "Sent", + "homeForecastTitle": "24-oras na forecast", + "typhoonLegendWarningAreas": "Warning areas", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "Lokal na intensidad 1 pataas", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "Nakaraan", + "restroomTypeFemale": "Palikuran ng babae", + "reportListToday": "Ngayon", + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "Naglo-load…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "Moderate typhoon", + "typhoonWind": "Hangin", + "mapLayerSatelliteAsh": "Himawari Ash", + "rainInterval3h": "3 oras", + "reportListSearch": "Maghanap", + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "Satellite", + "reportFilterLocation": "Lokasyon", + "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", + "typhoonIntensityTd": "Tropical depression", + "reportFilterDate": "Petsa", + "sponsorRestoreUnavailable": "Hindi maabot ang store. Pakisubukan muli mamaya.", + "homeForecastPop": "{pop}%", + "regionEmpty": "Wala pang naka-save na rehiyon", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "Payagan ang DPIP na patuloy na tumakbo sa background para hindi maantala o mapalampas ang mga alerto.", + "mapNavDisaster": "Sakuna", + "radarScanRangeSubtitle": "Ipinapakita ang aktwal na saklaw ng apat na radar.", + "aedHoursSunday": "Oras sa Linggo", + "reportDetailOriginTime": "Oras ng pangyayari", + "trendNoData": "Walang trend data", + "onboardingPermLocation": "Lokasyon", + "moreDiscord": "Komunidad sa Discord", + "mapNavPressure": "Presyon", + "mapLayerSatelliteB13": "Himawari Infrared (B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "Wala pang release notes", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "Start day: from 00:00(Taipei)", + "eewTitle": "Maagang babala sa lindol", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "fil", + "regionSelectCount": "{count}/{max} ang napili", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", + "meshtasticStateError": "Error", + "weatherModeOvercast": "Makulimlim", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "Lalim ng Hypocenter", + "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", + "reportFilterDatePick": "Pumili ng petsa", + "onboardingSkipStay": "Bumalik", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "Hindi ma-load ang data. Pakisubukan muli.", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "Silungan sa labas", + "meshtasticStateConnected": "Connected", + "mapNavRadar": "Radar", + "mapLayerSatelliteCloudClear": "Clear", + "eewSummary": "M{magnitude} · lalim {depth} km", + "locationBannerPermission": "Naka-off ang pahintulot sa lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.", + "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", + "radarCountyOutlineHint": "Iginuguhit sa ibabaw ng echo", + "windForecastCountyOutlineHint": "Iginuhit sa itaas ng patlang ng hangin", + "homeRainTrendTitle": "Ulan sa susunod na oras", + "moonPhaseFirstQuarter": "First quarter", + "mapLayerCategoryTyphoon": "Bagyo", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "Airtime (24h)", + "restroomTypeMixed": "Pinagsamang palikuran", + "restroomGradeGood": "Mahusay", + "notifyTsunami": "Impormasyon sa tsunami", + "navData": "Datos", + "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "Hindi makatawag ang device na ito", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "Lahat", + "weatherRankingMergeTo": "Pagsamahin", + "notifyIntensity": "Ulat ng intensidad", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "Bintana ng akumulasyon", + "reportDetailLocalFelt": "Lokal na Naramdamang Lindol", + "meshtasticDevice": "Device", + "onboardingGrant": "Ibigay", + "weatherModeRain": "Ulan", + "shelterVulnerableOkLabel": "Angkop para sa mahihina", + "stationSheetEmpty": "I-tap ang istasyon para makita ang datos", + "typhoonLegendProbability": "Strike probability", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "Magnitude", + "skyTimeMorning": "Umaga", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "Mga experimental na feature", + "onboardingTermsBody": "Mangyaring basahin ang mga sumusunod na paunawa bago gamitin ang DPIP:\n\n• Ang lahat ng impormasyon ay dapat sumunod sa nilalamang inilathala ng Central Weather Administration (CWA).\n\n• Depende sa kalagayan ng network, server, app, at pinagmumulan ng datos, may posibilidad na hindi matanggap ang impormasyon; ginagawa namin ang lahat ng aming makakaya upang maiwasan ito ngunit hindi namin magagarantiya na hindi ito mangyayari.\n\n• Maaaring maunang makarating sa iyong lokasyon ang malakas na pagyanig bago pa dumating ang notipikasyon.\n\n• Ang mga maagang babala sa lindol ay mabilis na kinakalkulang resulta na maaaring magtaglay ng malaking pagkakamali — unawain ito at gamitin nang may pag-iingat.\n\n• Anumang gawaing hindi pinahihintulutan ng mga awtoridad ay maaaring magdala ng panganib sa batas; mangyaring sundin ang lahat ng naaangkop na regulasyon.\n\nBukod dito, upang magbigay ng lokal na mga alerto, kinokolekta at ini-upload ng serbisyong ito ang iyong tinatayang lokasyon at push identifier — sa foreground at background — para lamang matukoy kung aling mga alerto ang ipapadala sa iyo.\n\nSa pamamagitan ng pag-tap sa \"Sumang-ayon at magpatuloy\" kinukumpirma mo na nabasa, naunawaan, at sinasang-ayunan mo ang nasa itaas.", + "reportFilterTitle": "Mga filter", + "onboardingPermCritical": "Mga kritikal na alerto", + "trendCumulativeTotal": "Kabuuang {total} mm", + "languageName": "Filipino", + "reportListEmptyFiltered": "Walang ulat na tumutugma sa mga filter", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "Bagyo", + "weatherModeSand": "Alikabok", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "Satellite", + "@dpmOpenInMaps": {}, + "notifyReport": "Ulat ng lindol", + "mapAppCoordinatesCopied": "Na-kopya ang coordinates", + "skyTimeNight": "Gabi", + "sponsorRecommended": "Inirerekomenda", + "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", + "weatherRankingWind": "Bilis ng hangin", + "feedStale": "Maaaring luma na ang datos", + "homeForecastWind": "{direction} · Force {level}", + "navHome": "Tahanan", + "meshtasticRegionLabel": "Region", + "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "Mga lisensya ng open-source", + "weatherRankingLowest": "Pinakamababa", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "Lalim", + "mapTimelineDataTime": "Oras ng data {time}", + "radarScanRange": "Ipakita ang saklaw ng pag-scan", + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "Saklaw {value}°C", + "weatherRankingExtremeHigh": "Pinakamataas ngayong araw", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "Detalye ng release", + "sponsorPrivacy": "Patakaran sa Privacy", + "reportDetailLocalIntensity": "Intensity sa iyong lokasyon", + "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", + "meshtasticAirtime": "Air time (TX)", + "shelterCapacityValue": "{n} katao", + "lightningLegendCc": "Ulap–ulap · {minutes} min", + "meshtasticSendHint": "Message to broadcast", + "monitorDelay": "Pagkaantala {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "Hindi", + "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "Nananatiling mababasa ang mga hangganan ng bayan sa ilalim ng radar echo.", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", + "radarScanRangeHint": "Sa labas: hindi naoobserbahan", + "typhoonPickerTd": "Tropical depression TD {no}", + "mapLayerSatelliteWatervapor": "Himawari Water Vapour", + "regionAddButton": "Magdagdag ng rehiyon", + "displaySettings": "Pagpapakita", + "restroomGradePoor": "Mas mababa sa pamantayan", + "restroomCategoryTourist": "Lugar para sa turista", + "locationBannerServiceOff": "Naka-off ang mga serbisyo ng lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.", + "mapLayerStyleTooltip": "Colour style", + "lightningLegendCg": "Ulap–lupa · {minutes} min", + "skyTimeAuto": "Awtomatiko", + "appLogs": "Mga log ng app", + "feedConnecting": "Kumokonekta…", + "notifyBannerDisabled": "Naka-off ang mga notification — hindi ka makakatanggap ng mga alerto sa sakuna.", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "Halumigmig", + "typhoonValueMs": "{n} m/s", + "homeForecastHumidity": "Halumigmig {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "Transportasyon", + "reportFilterLocationHint": "hal. Hualien, offshore", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "Distansya", + "meshtasticSnrTrend": "Trend ng signal (SNR)", + "meshtasticBatteryTrend": "Trend ng baterya", + "typhoonOverlayMenuTooltip": "Typhoon overlay options", + "mapLayerSatelliteBtdOzone": "Himawari Tropopause", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "Lindol", + "mapLayerDisasterMap": "Disaster Map", + "weatherModeFog": "Makapal na Hamog", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", + "moreAnnouncements": "Mga Anunsyo", + "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "Opisina ng gobyerno", + "typhoonLegendCurrent": "Kasalukuyang sentro", + "aedAddress": "Address", + "mapLayerAed": "AED", + "changelogTypePrerelease": "Beta", + "reportFilterIntensityInfoModernBody": "Antas 0–4, 5−, 5+, 6−, 6+, 7. Gamit ng filter ang bagong scale; ang mga lumang event ay may legacy label sa listahan.", + "typhoonOverlayWeatherNone": "None", + "mapLayerStyleGray": "Grayscale (JMA)", + "weatherModeAuto": "Awtomatiko", + "typhoonLabelProbCircle": "70% probability circle", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "Tumanggap ng lahat", + "displayTheme": "Tema", + "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "Past movement direction", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "Mga naka-save na rehiyon", + "typhoonLegendCone": "Kono ng forecast", + "moreCwaEew": "Maagang babala sa lindol ng CWA", + "onboardingPermsTitle": "Mga Pahintulot", + "mapLayerStyleJma": "Cloud-top enhancement (JMA)", + "rainInterval10m": "10 min", + "weatherRankingAnalysisLow": "Mababa {value}", + "meshtasticConnectAnyway": "Connect anyway", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", + "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", + "chartHourLabel": "{hour}h", + "mapLayerShelter": "Silungan", + "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", + "mapLayerSatelliteNdwi": "Himawari NDWI", + "disasterMapOverlayShelterTooltip": "Ipakita ang mga silungan", + "mapNavHumidity": "Halumigmig", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "Ayusin ayon sa intensity", + "homeRainTrendNoData": "Walang data", + "mapLayerCategoryRadar": "Radar", + "meshtasticShortName": "Short name", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "Himawari Airmass", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "Track detail", + "dataSectionWeather": "Panahon", + "aedHoursWeekday": "Oras sa weekday", + "homeActiveEventsTitle": "Mga aktibong event", + "weatherRankingAnalysisHigh": "Mataas {value}", + "faq": "Mga FAQ", + "typhoonHistoryLive": "Live", + "eewSerial": "Ulat {serial}", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "Pagkakasunud-sunod", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "Mga ulat ng lindol", + "typhoonNoActive": "Walang aktibong bagyo", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", + "navEvents": "Mga Kaganapan", + "onboardingTermsTitle": "Mga Tuntunin ng Serbisyo", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "Mga pangalan ng bayan", + "notifySetFailed": "Hindi ma-save ang setting. Pakisubukan muli.", + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "notifyAnnouncement": "Mga Anunsyo", + "onboardingIntroTitle": "Maligayang pagdating sa DPIP", + "regionCurrentUnavailable": "Hindi makuha ang kasalukuyang lokasyon", + "languageSystem": "Default ng system", + "skyTimeSunset": "Paglubog ng araw", + "mapLayerSatelliteDust": "Himawari Dust", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "I-edit", + "weatherDynamicState": "Animation ng panahon", + "mapPlaceholderDisabled": "Mapa (pansamantalang naka-disable)", + "moonNow": "Ngayon", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "Anyo", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "Pagsikat at paglubog", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "Susunod", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "Kalendaryo", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "Distansya", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "km", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "Lapad sa langit", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "Pagsikat ng buwan", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "Paglubog ng buwan", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "Susunod na bagong buwan", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "Nasa itaas buong araw", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "Wala sa araw na ito", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "Araw", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "Pagsikat, takipsilim at solar terms", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "Liwanag ng araw", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "Takipsilim", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "Liwanag", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "Orasang araw", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "Solar terms", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "Pagsikat ng araw", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "Paglubog ng araw", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "Tanghaling tapat", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "Haba ng araw", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "Sibil", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "Nautical", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "Astronomical", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "Golden hour sa umaga", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "Golden hour sa hapon", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "Blue hour", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "Equation of time", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "min", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "Susunod na termino", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "Mga planeta", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "Nasaan ngayong gabi, at gaano kaliwanag", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "Ngayon", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "Nasa itaas", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "Nasa ibaba", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "Malapit sa araw", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "Magnitude", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "Elongation", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "Panahon", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "Gabi", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "Umaga", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "Distansya", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "Taas", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "Mercury", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "Venus", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "Mars", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "Jupiter", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "Saturn", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "Uranus", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "Neptune", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "Vernal Equinox", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "Pure Brightness", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "Grain Rain", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "Simula ng Tag-init", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "Grain Full", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "Grain in Ear", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "Summer Solstice", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "Minor Heat", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "Major Heat", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "Simula ng Taglagas", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "End of Heat", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "White Dew", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "Autumnal Equinox", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "Cold Dew", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "Frost Descent", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "Simula ng Taglamig", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "Minor Snow", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "Major Snow", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "Winter Solstice", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "Minor Cold", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "Major Cold", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "Simula ng Tagsibol", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "Rain Water", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "Awakening of Insects", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "Ngayong gabi", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "Ano ang makikita, at kailan", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "Oras ng obserbasyon", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "Astronomical na gabi", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "Hindi tuluyang dumidilim", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "Madilim na yugto", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "Buwan nasa langit buong gabi", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "Kabuuang dilim", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "Liwanag ng buwan", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "Mga meteor shower", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "Hindi sumisikat ang radiant", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "/oras", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "Pagdaan ng satelayt", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "Nakikita ngayon", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "Quadrantids", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "Lyrids", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "Eta Aquariids", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "Delta Aquariids", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "Perseids", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "Orionids", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "Southern Taurids", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "Leonids", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "Geminids", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "Ursids", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "Open cluster", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "Globular cluster", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "Spiral galaxy", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "Elliptical galaxy", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "Irregular galaxy", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "Planetary nebula", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "Supernova remnant", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "Emission nebula", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "Reflection nebula", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "Asterism", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "Almanake", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "Petsang lunisolar at mga eklipse", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "Ngayon", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "Gregorian", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "Lunisolar", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "Taon", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app} (default)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "Haba ng buwan", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "Kopyahin ang coordinates", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "30 araw", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "Na-kopya ang coordinates", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "29 araw", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "Hindi mabuksan ang {app}", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "Leap ", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "Hindi makatawag ang device na ito", - - "mapOverlaySectionReference": "Layer ng sanggunian", - "mapLayerCategoryEarthquake": "Lindol", - "mapLayerCategoryTyphoon": "Bagyo", - "mapLayerCategoryWeather": "Obserbasyon sa panahon", - "mapLayerCategorySatellite": "Satellite", - "mapLayerCategoryRadar": "Radar", - "mapLayerCategoryLife": "Pang-araw-araw na buhay", - "mapLayerCategoryForecast": "Numerical forecast", "mapOverlaySectionMap": "Mapa", - "rainIntervalSection": "Window ng oras", - - "mapTownLabels": "Mga pangalan ng bayan", - "mapTownLabelsHint": "Ipakita ang mga pangalan ng bayan kapag naka-zoom", - - "mapTerrainRelief": "Rehiyebo ng terrain", - "mapTerrainReliefHint": "Ipakita ang anino ng terrain sa base map", - - "dpmSheetEmpty": "I-tap ang marker sa mapa para sa detalye", - "dpmAddress": "Address", - "restroomTypeLabel": "Uri", - "restroomCategoryLabel": "Kategorya", - "restroomGradeLabel": "Baitang", - "restroomTypeFemale": "Palikuran ng babae", - "restroomTypeMale": "Palikuran ng lalaki", - "restroomTypeMixed": "Pinagsamang palikuran", - "restroomTypeAccessible": "Palikurang may accessibility", - "restroomTypeGenderNeutral": "Palikurang neutral sa kasarian", - "restroomTypeFamily": "Palikuran ng pamilya", - "restroomTypeUnspecified": "Hindi natukoy", - "restroomCategoryTransport": "Transportasyon", - "restroomCategoryPark": "Parke", - "restroomCategoryCommercial": "Komersyal na establisyimento", - "restroomCategoryReligious": "Relihiyosong lugar", - "restroomCategoryCultural": "Pook na pangkultura", - "restroomCategoryGovernment": "Opisina ng gobyerno", - "restroomCategoryWelfare": "Institusyon ng kapakanan", - "restroomCategoryTourist": "Lugar para sa turista", - "restroomCategoryLeisure": "Lugar ng libangan", - "restroomCategoryOther": "Iba pa", - "restroomGradeExcellent": "Napakahusay", - "restroomGradeGood": "Mahusay", - "restroomGradeAverage": "Katamtaman", - "restroomGradePoor": "Mas mababa sa pamantayan", - "shelterAddressLabel": "Address", - "shelterCapacityLabel": "Kapasidad", - "shelterCapacityValue": "{n} katao", - "shelterCategoryLabel": "Mga uri ng kalamidad", - "shelterIndoorLabel": "Silungan sa loob", - "shelterOutdoorLabel": "Silungan sa labas", - "shelterVulnerableOkLabel": "Angkop para sa mahihina", - "dpmYes": "Oo", - "dpmNo": "Hindi", - "stationSheetEmpty": "I-tap ang istasyon para makita ang datos", - "monitorDelay": "Pagkaantala {value} s", - "monitorWaiting": "Naghihintay ng data…", - "mapLegendUnit": "Yunit: {unit}", - "typhoonLegendPast": "Aktwal na landas", - "typhoonLegendForecast": "Tinatayang landas", - "typhoonLegendForecastPoint": "Punto ng forecast", - "typhoonLegendCurrent": "Kasalukuyang sentro", - "typhoonLegendCone": "Kono ng forecast", - "mapLegendExpand": "Alamat", - "mapLegendCollapse": "Itago ang alamat", - "mapMyLocation": "Aking lokasyon", - "mapResetNorth": "Bumalik sa hilaga", - "typhoonLegendCircle15": "Gale circle (L7)", - "typhoonLegendCircle25": "Storm circle (L10)", - "typhoonLegendProbability": "Strike probability", - "typhoonLegendWarningAreas": "Warning areas", - "typhoonWarningTitle": "Typhoon warning", - "typhoonWarningAreas": "Areas: {areas}", - "typhoonTrackDetail": "Track detail", - "typhoonHistoryTitle": "Dataset time", - "typhoonHistoryLive": "Live", - "typhoonSatelliteTitle": "Satellite", - "typhoonDataTime": "Data time\n{time}", - "typhoonForecastLead": "Forecast +{hours} h", - "typhoonIntensityIntense": "Intense typhoon", - "typhoonIntensityMild": "Mild typhoon", - "typhoonIntensityModerate": "Moderate typhoon", - "typhoonIntensityTd": "Tropical depression", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "Tropical depression TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "Past movement direction", - "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", - "typhoonLabelGust": "Peak gust", - "typhoonLabelNe": "NE", - "typhoonLabelNw": "NW", - "typhoonLabelPosition": "Centre location", - "typhoonLabelPressure": "Central pressure", - "typhoonLabelProbCircle": "70% probability circle", - "typhoonLabelSe": "SE", - "typhoonLabelSpeed": "Past movement speed", - "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", - "typhoonLabelSw": "SW", - "typhoonLabelWind": "Max. sustained wind near centre", - "typhoonLegendCircleAvg": "Average circle", - "typhoonOverlayMenuTooltip": "Typhoon overlay options", - "typhoonOverlayProbabilityHint": "Hides the forecast cone", - "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", - "typhoonOverlaySectionExtra": "Overlays", - "typhoonOverlaySectionStorm": "Storm wind", - "typhoonOverlaySectionWeather": "Weather underlay", - "typhoonOverlayStormBandSubtitle": "With average circle", - "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", - "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", - "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", - "typhoonOverlayWeatherHint": "Aligned to bulletin time", - "typhoonOverlayWeatherNone": "None", - "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", - "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", - "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} hPa", - "typhoonValueKm": "{n} km", - "typhoonValueLat": "{lat}°N", - "typhoonValueLon": "{lon}°E", - "typhoonValueMs": "{n} m/s", - "typhoonOverlayForecastCallouts": "Forecast tooltips", - "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", - "dpmFilterSectionRestroom": "Mga uri ng lugar", - "dpmFilterSectionRestroomType": "Mga uri ng banyo", - "dpmFilterSectionShelter": "Mga uri ng sakuna sa silungan", - "dpmDisasterFlood": "Baha", - "dpmDisasterEarthquake": "Lindol", - "dpmDisasterLandslide": "Pagguho ng lupa", - "dpmDisasterTsunami": "Tsunami", - "dpmDisasterSlope": "Panganib sa dalisdis", - "dpmDisasterNuclear": "Aksidente sa nukleyar", - "skyTime": "Oras ng langit", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "Eklipse ng buwan", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "Awtomatiko", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "Eklipse ng araw", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "Bukang-liwayway", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "Wala sa saklaw", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "Pagsikat ng araw", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "Total", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "Umaga", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "Parsyal", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "Tanghali", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "Annular", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "Hapon", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "Penumbral", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "Gintong oras", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "Daga", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "Paglubog ng araw", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "Baka", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "Takipsilim", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "Tigre", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "Gabi", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "Kuneho", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "Maulap", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "Dragon", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "Makulimlim", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "Ahas", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "Niyebe", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "Kabayo", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "Alikabok", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "Kambing", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "Ipakita ang saklaw ng pag-scan", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "Unggoy", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "Ipinapakita ang aktwal na saklaw ng apat na radar.", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "Manok", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "Sa labas: hindi naoobserbahan", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "Aso", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "Mga opsyon sa layer ng radar", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "Baboy", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "Mga hangganan ng lalawigan", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "Taog", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "Mga hangganan ng bansa", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "Spring, neap at hila ng buwan", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "Panlabas na balangkas ng bawat bansa", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "Astronomikal na puwersa lamang — hindi talaan ng taog sa daungan. Para sa lebel ng tubig, gamitin ang talaan ng CWA.", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "Iginuguhit sa ibabaw ng echo", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "Ngayon", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "Nananatiling mababasa ang mga hangganan sa ilalim ng radar echo.", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "Siklo", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "Mga hangganan ng bayan", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "Spring", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "Mas pinong hati", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "Neap", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "Nananatiling mababasa ang mga hangganan ng bayan sa ilalim ng radar echo.", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "Katamtaman", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "Mga opsyon sa layer ng pagtataya ng pag-ulan", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "Hila ng buwan", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "Mga opsyon sa layer ng pagtataya ng hangin", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "Equilibrium tide", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "Iginuhit sa itaas ng patlang ng hangin", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "m", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "Panlabas na balangkas ng bawat bansa", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "Susunod na perigean spring", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "Ang mas pinong mesh", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "Mga turning point", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "Ulat {serial}", - "eewMaxIntensity": "Pinakamataas na intensidad", - "eewLocalIntensity": "Tantiya sa lokasyon", - "eewSWave": "S wave", - "eewArrived": "Dumating", - "eewCountdown": "{seconds} segundo" + "tideHigh": "Taas", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "Baba", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "Mapa ng langit", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "Ang langit sa itaas mo", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "H", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "S", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "T", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "K", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "{days} araw nang luma ang elements", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}buwan {month}, araw {day}", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "Walang shower ngayon", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "Walang nakikitang pass sa 48 oras", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "Hindi mabasa ang orbit data", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "Walang sapat na taas", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "Hindi mabasa ang star catalogue", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 66ad61ada..67f1fe5f5 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1,679 +1,1739 @@ { - "@@locale": "id", - "languageName": "Bahasa Indonesia", - "navHome": "Beranda", - "navEvents": "Kejadian", - "navMap": "Peta", - "navData": "Data", - "navEarthquake": "Gempa Bumi", - "dataSectionSeismic": "Seismik", - "dataEarthquakeSubtitle": "Laporan gempa", - "dataSectionWeather": "Cuaca", - "dataWeatherRankingSubtitle": "Peringkat stasiun langsung", - "weatherRankingTitle": "Peringkat observasi", - "weatherRankingMeta": "Waktu data: {time}\n{count} stasiun", - "weatherRankingEmpty": "Tidak ada observasi untuk diurutkan", - "weatherRankingBy": "Urut", - "weatherRankingHighest": "Tertinggi", - "weatherRankingLowest": "Terendah", - "weatherRankingMergeTo": "Gabung", - "weatherRankingMergeTown": "Kecamatan", - "weatherRankingMergeCounty": "Kabupaten", - "weatherRankingWind": "Kecepatan angin", - "weatherRankingGust": "Hembusan", + "typhoonValueLat": "{lat}°N", + "onboardingSkipBody": "Tanpa lokasi dan notifikasi, DPIP tidak dapat memperingatkan Anda tentang gempa dan bencana di sekitar Anda secara waktu nyata. Anda masih dapat memberikannya nanti di Pengaturan.", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 jam", + "homeRainTrendHeavyStopping": "Hujan deras diperkirakan berhenti dalam {minutes} menit", + "mapTimelineObserved": "Diamati", + "regionSelectTitle": "Pilih wilayah", + "skyTimeNoon": "Siang", + "radarCountyOutlineSubtitle": "Menjaga batas wilayah tetap terbaca di bawah gema radar.", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "Jenis toilet", + "mapLayerSatelliteB03": "Himawari Red (B03)", + "reportFilterIntensity": "Intensitas", + "mapLayerLightning": "Petir", + "restroomTypeMale": "Toilet pria", + "meshtasticLastReceived": "Last received", + "reportDetailSortByCounty": "Urutkan menurut wilayah", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "Kemungkinan hujan ringan", + "meshtasticUptime": "Uptime", "weatherRankingTempExtremes": "Ekstrem suhu", - "weatherRankingExtremeHigh": "Maksimum hari ini", - "weatherRankingExtremeLow": "Minimum hari ini", + "themeLight": "Terang", + "mapTerrainReliefHint": "Tampilkan relief terrain di peta dasar", + "meshtasticEmptyMessage": "(empty message)", + "moreSectionRegion": "Wilayah", + "dpmDisasterEarthquake": "Gempa", + "mapLayerSatellite": "Himawari Infrared (B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "Jam Sabtu", + "dpmDisasterSlope": "Bencana lereng", + "moonPhaseNew": "New moon", + "notifySectionEew": "Peringatan dini gempa", + "mapResetNorth": "Kembali ke utara", + "rainInterval2d": "2 hr", + "mapTownLabelsHint": "Tampilkan nama kecamatan saat diperbesar", + "commonCancel": "Cancel", + "notifyOptTsunamiWarning": "Hanya peringatan tsunami", + "mapLayerSatelliteBtdFog": "Himawari Night Fog", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "Lanjutan", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "Rentang harian", + "notifySettingsMenu": "Pengaturan notifikasi", + "typhoonHistoryTitle": "Waktu data", + "mapAppDefault": "{app} (bawaan)", + "trendRange24h": "24 jam", + "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", "weatherRankingRecordedAt": "Tercatat pukul {time}", - "weatherRankingAnalysisCurrent": "Sekarang {value}°C", - "weatherRankingAnalysisHigh": "Maks {value}", - "weatherRankingAnalysisLow": "Min {value}", - "weatherRankingAnalysisRange": "Rentang {value}°C", - "reportListEmpty": "Tidak ada laporan gempa", - "reportListEmptyFiltered": "Tidak ada laporan yang cocok dengan filter", - "reportListMeta": "M{magnitude} · {depth} km", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "km", - "reportListLocalFelt": "Terasa lokal", - "reportListToday": "Hari ini", - "reportListYesterday": "Kemarin", - "reportListDayCount": "{count}", - "reportListEnd": "Akhir daftar", - "reportFilterTitle": "Filter", - "reportFilterSort": "Urutan", - "reportFilterSortTime": "Waktu", - "reportFilterSortIntensity": "Intensitas", - "reportFilterSortMagnitude": "Magnitudo", - "reportFilterSortDepth": "Kedalaman", + "mapLayerRain": "Curah hujan", + "mapLayerQpesums": "Prakiraan hujan 1 jam ke depan", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "Peta", + "mapTerrainRelief": "Relief terrain", + "eewMaxIntensity": "Intensitas maks", + "mapLegendCollapse": "Sembunyikan legenda", + "changelogTitle": "Catatan pembaruan", "reportFilterOrderDesc": "Menurun", - "reportFilterOrderAsc": "Menaik", - "reportFilterIntensity": "Intensitas", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", "reportFilterIntensityInfoTitle": "Skala intensitas baru & lama", - "reportFilterIntensityInfoIntro": "CWA mengganti skala intensitas pada 1 Jan 2020 (waktu Taipei).", - "reportFilterIntensityInfoLegacyTitle": "Lama (sebelum 2020)", - "reportFilterIntensityInfoLegacyBody": "Hanya tingkat 0–7, tanpa pemisahan 5−/5+/6−/6+.", - "reportFilterIntensityInfoModernTitle": "Baru (sejak 2020)", - "reportFilterIntensityInfoModernBody": "Tingkat 0–4, 5−, 5+, 6−, 6+, 7. Slider filter memakai skala baru; peristiwa lama tetap memakai label lama di daftar.", - "reportFilterMagnitude": "Magnitudo", - "reportFilterDepth": "Kedalaman", - "reportFilterDepthKm": "{depth} km", - "reportFilterDate": "Tanggal", - "reportFilterDatePick": "Pilih tanggal", - "reportFilterDateStartNote": "Hari mulai: dari 00:00(Taipei)", + "mapLayerTyphoon": "Topan", + "radarOverlayMenuTooltip": "Opsi lapisan radar", + "mapMyLocation": "Lokasi saya", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "Nodes", + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", + "aedType": "Jenis", + "termsOfService": "Ketentuan Layanan", + "typhoonLegendCircle25": "Lingkar badai", + "sponsorTitle": "Dukung DPIP", + "mapNavSatellite": "Satelit", + "homeRainTrendUpdated": "Diperbarui {time}", + "onboardingNext": "Berikutnya", + "weatherRankingMergeTown": "Kecamatan", + "mapLayerMonitor": "Monitor Seismik", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "Langganan", + "typhoonValueLon": "{lon}°E", + "skyTime": "Waktu langit", + "weatherModeCloudy": "Berawan", + "skyTimeDusk": "Senja", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "Hari akhir: hingga 24:00(Taipei)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "Lokasi", - "reportFilterLocationHint": "mis. Hualien, lepas pantai", - "reportFilterAny": "Semua", - "reportFilterApply": "Terapkan", - "reportFilterReset": "Reset", - "reportListSearch": "Cari", - "reportDetailTitle": "Laporan Gempa", - "reportDetailNumbered": "Gempa Dirasakan Signifikan No. {number}", - "reportDetailLocalFelt": "Gempa Dirasakan Lokal", - "reportDetailInfo": "Detail", - "reportDetailOriginTime": "Waktu kejadian", - "reportDetailEpicenter": "Koordinat episentrum", - "reportDetailMagnitude": "Magnitudo", - "reportDetailDepth": "Kedalaman hiposenter", - "reportDetailAreaIntensity": "Intensitas per wilayah", - "reportDetailLocalIntensity": "Intensitas di lokasi Anda", - "reportDetailLocalIntensityUnavailable": "Tidak ada data intensitas", - "reportDetailSortByIntensity": "Urutkan menurut intensitas", - "reportDetailSortByCounty": "Urutkan menurut wilayah", - "reportDetailImage": "Gambar laporan", - "reportDetailImageUnavailable": "Gambar laporan belum tersedia", - "reportDetailOpenReport": "Halaman laporan", - "reportDetailReplay": "Putar ulang", - "navMore": "Lainnya", - "appLogs": "Log aplikasi", - "changelogTitle": "Catatan pembaruan", - "changelogEmpty": "Belum ada catatan rilis", - "changelogTypePrerelease": "Beta", - "changelogTypeStable": "Stabil", - "changelogCurrentVersion": "Saat ini", - "changelogVersionDetails": "Detail rilis", - "changelogBodyEmpty": "Tidak ada catatan untuk rilis ini.", - "mapPlaceholderDisabled": "Peta (dinonaktifkan sementara)", - "moreSectionRegion": "Wilayah", - "moreSectionNotify": "Notifikasi", - "moreSectionDisplay": "Tampilan", - "regionManageTitle": "Wilayah tersimpan", - "regionAddButton": "Tambah wilayah", - "regionEmpty": "Belum ada wilayah tersimpan", - "regionSelectTitle": "Pilih wilayah", - "regionSelectCount": "{count}/{max} dipilih", - "regionSelectFull": "Anda dapat menyimpan hingga {max} wilayah", - "regionEdit": "Ubah", - "moreSectionAdvanced": "Lanjutan", - "moreDeveloper": "Info debug", - "experimentalFeatures": "Fitur eksperimental", - "moreSectionLinks": "Tautan", - "moreCwaEew": "Peringatan dini gempa CWA", - "moreTremReport": "Laporan deteksi TREM", - "moreServerStatus": "Status server", - "moreAnnouncements": "Pengumuman", - "moreDiscord": "Komunitas Discord", - "moreNotifyLog": "Log notifikasi DPIP", - "moreLinkOpenFailed": "Tidak dapat membuka tautan", - "weatherDynamicState": "Animasi cuaca", - "weatherDynamicStateSubtitle": "Ganti cuaca latar beranda", - "weatherModeAuto": "Otomatis", - "weatherModeClear": "Cerah", - "weatherModeRain": "Hujan", - "weatherModeFog": "Kabut", - "weatherModeThunderstorm": "Badai petir", - "commonLoading": "Memuat…", - "commonRetry": "Coba lagi", - "commonError": "Terjadi kesalahan", - "commonFetchFailed": "Tidak dapat memuat data. Silakan coba lagi.", - "commonEmpty": "Tidak ada yang ditampilkan", - "feedConnecting": "Menghubungkan…", - "feedStale": "Data mungkin sudah usang", - "feedOffline": "Koneksi terputus", - "eewTitle": "Peringatan dini gempa", - "eewNone": "Tidak ada peringatan dini gempa aktif", - "eewSummary": "M{magnitude} · kedalaman {depth} km", - "regionNationwide": "Seluruh negeri", - "regionCurrent": "Lokasi saat ini", - "regionCurrentUnavailable": "Tidak dapat memperoleh lokasi saat ini", - "weatherPrecipitation": "Curah hujan", - "weatherHumidity": "Kelembapan", - "weatherDataTime": "{station} · Waktu data {time}", - "homeViewOnMap": "Lihat di peta", - "homeForecastTitle": "Prakiraan 24 jam", + "meshtasticSilent": "Silent", + "reportFilterSortMagnitude": "Magnitudo", + "mapLayerCategoryEarthquake": "Gempa", + "mapLayerSatelliteB12": "Himawari Ozone (B12)", + "typhoonLegendPast": "Jalur aktual", + "restroomCategoryOther": "Lainnya", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "T {high}° · R {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "Terasa {temp}°", - "homeForecastHumidity": "Kelembapan {value}%", - "homeForecastWind": "{direction} · Skala {level}", - "homeForecastUnavailable": "Pilih wilayah untuk melihat prakiraan", - "homeForecastEmpty": "Tidak ada data prakiraan", - "homeActiveEventsTitle": "Peristiwa aktif", - "homeActiveEventsEmpty": "Tidak ada peristiwa aktif", - "homeRainTrendTitle": "Hujan 1 jam ke depan", - "homeRainTrendMinute": "{minute} mnt", - "homeRainTrendUpdated": "Diperbarui {time}", - "homeRainTrendNoData": "Tidak ada data", - - "homeRainTrendScattered": "Kemungkinan hujan ringan", - "homeRainTrendLightSustained": "Hujan ringan berlanjut selama 1 jam ke depan", - "homeRainTrendLightStopping": "Hujan ringan diperkirakan berhenti dalam {minutes} menit", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "Buka pengaturan", + "mapLegendExpand": "Legenda", + "eewNone": "Tidak ada peringatan dini gempa aktif", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "Imbauan dan peringatan tsunami", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "Node options", + "onboardingAgreeContinue": "Setuju dan lanjutkan", + "meshtasticNodeId": "Node ID", + "commonRetry": "Coba lagi", + "reportDetailNumbered": "Gempa Dirasakan Signifikan No. {number}", + "typhoonOverlayStormBandSubtitle": "With average circle", + "disasterMapOverlayRestroomTooltip": "Tampilkan toilet umum", + "weatherRankingTitle": "Peringkat observasi", "homeRainTrendHeavySustained": "Hujan deras berlanjut selama 1 jam ke depan", - "homeRainTrendHeavyStopping": "Hujan deras diperkirakan berhenti dalam {minutes} menit", - "mapLayers": "Lapisan", - "mapLayerOrderTitle": "Urutkan lapisan", - "mapLayerOrderReset": "Atur ulang urutan", - "mapLayerRadar": "Radar Komposit", - "mapLayerSatellite": "Himawari Infrared (B13)", - "mapLayerSatelliteB01": "Himawari Blue (B01)", - "mapLayerSatelliteB02": "Himawari Green (B02)", - "mapLayerSatelliteB03": "Himawari Red (B03)", - "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "notifySectionTsunami": "Tsunami", + "restroomCategoryPark": "Taman", + "moreLinkOpenFailed": "Tidak dapat membuka tautan", + "themeDark": "Gelap", + "sponsorRestore": "Pulihkan pembelian", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "Traffic", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", + "disasterMapOverlayAedTooltip": "Tampilkan lokasi AED", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "Kelembapan", + "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", + "meshtasticScanning": "Scanning…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "Anda dapat menyimpan hingga {max} wilayah", + "meshtasticTitle": "Meshtastic", + "navMore": "Lainnya", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "Lapisan", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "Himawari Near-Infrared (B05)", - "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", - "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", - "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", - "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", - "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", - "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", - "mapLayerSatelliteB12": "Himawari Ozone (B12)", - "mapLayerSatelliteB13": "Himawari Infrared (B13)", - "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", - "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", - "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "NE", + "meshtasticCopied": "Message copied", + "reportListEmpty": "Tidak ada laporan gempa", + "reportListEnd": "Akhir daftar", "mapLayerSatelliteTruecolor": "Himawari True Color", - "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", - "mapLayerSatelliteAsh": "Himawari Ash", - "mapLayerSatelliteDust": "Himawari Dust", - "mapLayerSatelliteAirmass": "Himawari Airmass", - "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", - "mapLayerSatelliteWatervapor": "Himawari Water Vapour", - "mapLayerSatelliteBtdSplit": "Himawari Split Window", - "mapLayerSatelliteBtdFog": "Himawari Night Fog", - "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", - "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", - "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", - "mapLayerSatelliteBtdOzone": "Himawari Tropopause", - "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", - "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", - "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", - "mapLayerSatelliteNdvi": "Himawari NDVI", - "mapLayerSatelliteNdwi": "Himawari NDWI", - "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionExtra": "Overlays", + "eewSWave": "Gelombang S", + "meshtasticBusyTitle": "Another app is using this radio", + "restroomCategoryCultural": "Tempat budaya", + "typhoonLabelWind": "Max. sustained wind near centre", + "radarGlobalOutlineHint": "Bingkai luar setiap negara", + "notifyEvacuation": "Informasi bencana", + "typhoonLegendCircle15": "Lingkar angin kencang", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "Astronomy", + "homeRainTrendLightSustained": "Hujan ringan berlanjut selama 1 jam ke depan", + "commonError": "Terjadi kesalahan", + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "Sekarang", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "Halaman laporan", + "trendRange7d": "7 hari", + "typhoonWarningAreas": "Wilayah: {areas}", + "rainIntervalSection": "Jendela waktu", + "notifyTitle": "Notifikasi", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "Kategori", + "sponsorRestoring": "Memulihkan pembelian…", + "sponsorIntro": "DPIP berdedikasi menyediakan informasi mitigasi bencana secara real-time, tanpa iklan atau model bisnis lainnya. Dukungan Anda membantu kami menjaga server tetap berjalan dan terus mengembangkan aplikasi.", + "shelterAddressLabel": "Alamat", + "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "Tempat komersial", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "Wilayah", + "homeRainTrendLightStopping": "Hujan ringan diperkirakan berhenti dalam {minutes} menit", + "reportDetailInfo": "Detail", + "mapNavWind": "Angin", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "Opsi lapisan prakiraan angin", + "dataWeatherRankingSubtitle": "Peringkat stasiun langsung", + "rainInterval6h": "6 jam", + "homeRainTrendMinute": "{minute} mnt", + "restroomTypeUnspecified": "Tidak ditentukan", + "typhoonOverlayProbabilityHint": "Hides the forecast cone", "mapLayerSatelliteGlobalOutline": "Country border", - "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", - "mapLayerSatelliteCloudClear": "Clear", - "mapLayerSatelliteCloudProbablyClear": "Probably clear", - "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "mapNavTemperature": "Suhu", + "typhoonLegendForecastPoint": "Titik prakiraan", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "Kemarin", + "moreSectionLinks": "Tautan", + "feedOffline": "Koneksi terputus", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "Tampilan", + "rainInterval3d": "3 hr", + "defaultMapLayerSubtitle": "Tab Peta membuka lapisan ini. Ikon dan label navigasi bawah ikut pilihan ini.", + "aedDescription": "Catatan", + "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", + "onboardingPermLocationDesc": "Menargetkan peringatan ke lokasi Anda.", + "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "Tidak ada peristiwa aktif", + "typhoonLabelPosition": "Centre location", + "weatherRankingBy": "Urut", + "typhoonIntensityMild": "Mild typhoon", + "windForecastGlobalOutlineHint": "Bingkai luar setiap negara", + "rainInterval1h": "1 jam", + "eewLocalIntensity": "Perkiraan di lokasi", + "mapLayerRadar": "Radar Komposit", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "Tempat ibadah", + "meshtasticRole": "Role", "mapLayerSatelliteCloudCloudy": "Cloudy", - "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", - "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", - "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", - "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", - "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", - "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", - "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "skyTimeSunrise": "Matahari terbit", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "No messages yet", + "onboardingPermNotifyDesc": "Menyampaikan peringatan gempa, cuaca, dan bencana pada saat terjadi.", + "radarTownOutline": "Batas kecamatan", "mapLayerStyleSection": "Colour style", - "mapLayerStyleTooltip": "Colour style", - "mapLayerStyleGray": "Grayscale (JMA)", - "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", - "mapLayerStyleJma": "Cloud-top enhancement (JMA)", - "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", - "mapLayerQpesums": "Prakiraan hujan 1 jam ke depan", - "mapLayerLightning": "Petir", - "lightningLegendCg": "Awan–tanah · {minutes} mnt", - "lightningLegendCc": "Awan–awan · {minutes} mnt", - "mapTimelineNow": "Sekarang", - "mapTimelinePast": "Lampau", - "mapTimelineFuture": "Mendatang", - "mapTimelineObserved": "Diamati", - "mapTimelineForecast": "Prakiraan", - "mapTimelineDataTime": "Waktu data {time}", - "notifySettingsMenu": "Pengaturan notifikasi", - "notifyTitle": "Notifikasi", - "notifyUnavailable": "Notifikasi push belum siap — coba lagi sebentar lagi.", - "notifySetFailed": "Tidak dapat menyimpan pengaturan. Silakan coba lagi.", - "notifySectionEew": "Peringatan dini gempa", - "notifySectionEarthquake": "Gempa bumi", - "notifySectionWeather": "Cuaca", - "notifySectionTsunami": "Tsunami", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "Lapisan peta bencana", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "SW", + "typhoonForecastLead": "Forecast +{hours} h", + "dpmDisasterTsunami": "Tsunami", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "Stabil", + "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "Lapisan referensi", + "mapLayerSatelliteB02": "Himawari Green (B02)", + "reportListLocalFelt": "Terasa lokal", + "weatherRankingEmpty": "Tidak ada observasi untuk diurutkan", "notifySectionOther": "Lainnya", - "notifyEew": "Peringatan gempa darurat", - "notifyMonitor": "Pemantau getaran kuat", - "notifyReport": "Laporan gempa", - "notifyIntensity": "Laporan intensitas", - "notifyThunderstorm": "Peringatan badai petir", - "notifyAdvisory": "Imbauan cuaca", - "notifyEvacuation": "Informasi bencana", - "notifyTsunami": "Informasi tsunami", - "notifyAnnouncement": "Pengumuman", - "notifyOptOff": "Nonaktif", - "notifyOptAll": "Terima semua", + "weatherRankingMeta": "Waktu data: {time}\n{count} stasiun", + "onboardingTermsAgree": "Saya telah membaca dan menyetujui Ketentuan Layanan", + "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", "notifyOptLocalIntensity4": "Intensitas lokal 4 atau lebih", - "notifyOptLocalIntensity1": "Intensitas lokal 1 atau lebih", - "notifyOptWeatherLocal": "Hanya lokasi saat ini", - "notifyOptTsunamiWarning": "Hanya peringatan tsunami", - "notifyOptTsunamiAll": "Imbauan dan peringatan tsunami", - "onboardingNext": "Berikutnya", - "onboardingBack": "Kembali", + "eewArrived": "Tiba", + "meshtasticNoDevices": "No Meshtastic devices found", + "mapLayerCategoryLife": "Kehidupan sehari-hari", + "reportFilterSortIntensity": "Intensitas", + "typhoonMotion": "Bergerak", + "meshtasticStateDisconnected": "Disconnected", + "typhoonIntensityIntense": "Intense typhoon", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "Urutkan lapisan", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "Ya", + "meshtasticNoHistory": "Not enough history yet", + "reportDetailLocalIntensityUnavailable": "Tidak ada data intensitas", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "Kedalaman", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "Gulir ke bawah untuk melanjutkan", - "onboardingIntroTitle": "Selamat datang di DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "Prakiraan", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "Peta", + "notifyAdvisory": "Imbauan cuaca", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "Reset", + "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionStorm": "Storm wind", + "moonPhaseFull": "Full moon", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "weatherDynamicStateSubtitle": "Ganti cuaca latar beranda", + "reportFilterIntensityInfoModernTitle": "Baru (sejak 2020)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "Data time\n{time}", + "restroomTypeAccessible": "Toilet aksesibel", + "moreSectionAbout": "Tentang", + "meshtasticSelectDevice": "Select a radio", "onboardingIntroBody": "DPIP adalah pendamping pencegahan bencana Anda. DPIP menyatukan peringatan dini gempa, laporan gempa, cuaca, dan informasi bahaya, serta memberi tahu Anda pada saat yang penting.\n\n• Gempa bumi: peringatan dini, laporan intensitas, dan laporan rinci\n• Cuaca: pesan badai petir waktu nyata dan imbauan cuaca\n• Tsunami dan informasi bencana\n\nSelanjutnya, kami akan meminta Anda meninjau Ketentuan Layanan dan memberikan beberapa izin agar DPIP dapat melindungi Anda secara waktu nyata.", - "onboardingTermsTitle": "Ketentuan Layanan", - "onboardingTermsBody": "Harap baca pemberitahuan berikut sebelum menggunakan DPIP:\n\n• Semua informasi harus mengacu pada konten yang diterbitkan oleh Central Weather Administration (CWA) Taiwan.\n\n• Bergantung pada kondisi jaringan, server, aplikasi, dan sumber data hulu, informasi mungkin tidak diterima; kami berupaya sebaik mungkin untuk menghindari hal ini tetapi tidak dapat menjamin bahwa hal itu tidak akan pernah terjadi.\n\n• Guncangan kuat dapat mencapai lokasi Anda sebelum notifikasi tiba.\n\n• Peringatan dini gempa adalah hasil perhitungan cepat yang mungkin mengandung kesalahan yang signifikan — pahami hal ini dan gunakan dengan hati-hati.\n\n• Setiap tindakan yang tidak disahkan oleh pihak berwenang dapat menimbulkan risiko hukum; harap patuhi semua peraturan yang berlaku.\n\nSelain itu, untuk menyediakan peringatan yang dilokalkan, layanan ini mengumpulkan dan mengunggah perkiraan lokasi Anda dan pengidentifikasi push — di latar depan maupun latar belakang — semata-mata untuk menentukan peringatan mana yang akan dikirimkan kepada Anda.\n\nDengan mengetuk \"Setuju dan lanjutkan\", Anda mengonfirmasi bahwa Anda telah membaca, memahami, dan menyetujui hal-hal di atas.", - "onboardingTermsAgree": "Saya telah membaca dan menyetujui Ketentuan Layanan", - "onboardingAgreeContinue": "Setuju dan lanjutkan", - "onboardingPermsTitle": "Izin", - "onboardingPermsBody": "Agar DPIP dapat memperingatkan Anda saat bencana terjadi, harap berikan izin berikut. Anda dapat mengubahnya kapan saja di pengaturan sistem.", + "shelterCapacityLabel": "Kapasitas", + "reportDetailImage": "Gambar laporan", + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", "onboardingPermNotify": "Notifikasi", - "onboardingPermNotifyDesc": "Menyampaikan peringatan gempa, cuaca, dan bencana pada saat terjadi.", - "onboardingPermCritical": "Peringatan kritis", - "onboardingPermCriticalDesc": "Memungkinkan peringatan gempa yang mengancam jiwa tetap berbunyi bahkan dalam mode senyap atau Jangan Ganggu.", - "onboardingPermLocation": "Lokasi", - "onboardingPermLocationDesc": "Menargetkan peringatan ke lokasi Anda.", - "onboardingPermBackground": "Lokasi latar belakang", - "onboardingPermBackgroundDesc": "Izinkan \"Selalu\" agar peringatan tetap menargetkan Anda saat aplikasi ditutup.", - "onboardingPermBattery": "Pengecualian baterai", - "onboardingPermBatteryDesc": "Izinkan DPIP terus berjalan di latar belakang agar peringatan tidak tertunda atau terlewat.", - "onboardingGrant": "Berikan", - "onboardingGranted": "Diberikan", - "onboardingStart": "Mulai", - "language": "Bahasa", - "languageSettings": "Bahasa", - "languageSystem": "Bawaan sistem", - "locationBannerServiceOff": "Layanan lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.", - "locationBannerPermission": "Izin lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.", - "locationBannerFix": "Buka pengaturan", - "notifyBannerDisabled": "Notifikasi mati — Anda tidak akan menerima peringatan bencana.", - "onboardingSkipTitle": "Izin belum diberikan", - "onboardingSkipBody": "Tanpa lokasi dan notifikasi, DPIP tidak dapat memperingatkan Anda tentang gempa dan bencana di sekitar Anda secara waktu nyata. Anda masih dapat memberikannya nanti di Pengaturan.", - "onboardingSkipStay": "Kembali", - "onboardingSkipLeave": "Tetap lewati", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "Clear messages", + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "Lapisan peta bawaan", + "moreSectionNotify": "Notifikasi", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "Notifikasi push belum siap — coba lagi sebentar lagi.", + "mapLayerOrderReset": "Atur ulang urutan", + "dpmAddress": "Alamat", + "weatherRankingMergeCounty": "Kabupaten", + "moreSectionApp": "Dapatkan aplikasi", + "reportFilterIntensityInfoLegacyBody": "Hanya tingkat 0–7, tanpa pemisahan 5−/5+/6−/6+.", + "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", + "qpesumsOverlayMenuTooltip": "Opsi lapisan prakiraan curah hujan", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "Mendatang", + "typhoonLegendCircleAvg": "Average circle", + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "SE", + "radarTownOutlineHint": "Kisi yang lebih rapat", + "eewCountdown": "{seconds} detik", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "Peak gust", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "Ketentuan Penggunaan", + "restroomTypeGenderNeutral": "Toilet netral gender", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "Peringatan badai petir", + "skyTimeGolden": "Jam emas", + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "Sekarang {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "Pilih wilayah untuk melihat prakiraan", + "mapLayers": "Lapisan", + "meshtasticHardware": "Hardware", + "languageSettings": "Bahasa", + "dpmDisasterNuclear": "Kecelakaan nuklir", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "Bahasa", + "homeForecastFeelsLike": "Terasa {temp}°", + "typhoonOverlayWeatherHint": "Aligned to bulletin time", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "Fajar", + "skyTimeAfternoon": "Sore", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "Peringatan topan", "moreSourceCode": "Kode sumber", - "moreSectionApp": "Dapatkan aplikasi", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "Tampilan", - "defaultMapLayerSettings": "Lapisan peta bawaan", - "defaultMapLayerSubtitle": "Tab Peta membuka lapisan ini. Ikon dan label navigasi bawah ikut pilihan ini.", - "mapNavRadar": "Radar", - "mapNavQpesums": "Prakiraan", - "mapNavSatellite": "Satelit", - "mapNavLightning": "Petir", - "mapNavTyphoon": "Topan", + "mapLayerCategoryWeather": "Pengamatan cuaca", + "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", + "windForecastTownOutlineHint": "Jaring yang lebih halus", + "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", + "mapAppCopyCoordinates": "Salin koordinat", + "reportFilterIntensityInfoIntro": "CWA mengganti skala intensitas pada 1 Jan 2020 (waktu Taipei).", "mapNavEarthquake": "Gempa", - "mapNavTemperature": "Suhu", - "mapNavHumidity": "Kelembapan", - "mapNavPressure": "Tekanan", - "mapNavWind": "Angin", + "typhoonGust": "Embusan", + "restroomGradeAverage": "Sedang", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", + "onboardingPermBackgroundDesc": "Izinkan \"Selalu\" agar peringatan tetap menargetkan Anda saat aplikasi ditutup.", + "mapTimelineForecast": "Prakiraan", + "restroomTypeLabel": "Jenis", + "navEarthquake": "Gempa Bumi", + "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "Laporan Gempa", + "moreTremReport": "Laporan deteksi TREM", + "weatherDataTime": "{station} · Waktu data {time}", + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "radarCountyOutline": "Batas kabupaten/kota", + "onboardingGranted": "Diberikan", + "@mapAppCopyCoordinates": {}, + "commonClose": "Tutup", + "restroomGradeLabel": "Nilai", + "rainIntervalNow": "Hari ini", + "changelogCurrentVersion": "Saat ini", + "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", + "typhoonLabelPressure": "Central pressure", + "aedOpenRemark": "Catatan jam buka", + "onboardingPermsBody": "Agar DPIP dapat memperingatkan Anda saat bencana terjadi, harap berikan izin berikut. Anda dapat mengubahnya kapan saja di pengaturan sistem.", + "typhoonOverlaySectionWeather": "Weather underlay", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "Hanya lokasi saat ini", "mapNavRain": "Hujan", - "mapNavDisaster": "Bencana", - "displayTheme": "Tema", + "moonDays": "days", + "mapLegendUnit": "Satuan: {unit}", + "weatherModeClear": "Cerah", + "meshtasticRadio": "Radio", + "commonEmpty": "Tidak ada yang ditampilkan", + "mapLayerSatelliteB01": "Himawari Blue (B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "Menaik", + "reportFilterApply": "Terapkan", + "reportDetailImageUnavailable": "Gambar laporan belum tersedia", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "Tertinggi", + "reportDetailReplay": "Putar ulang", + "mapLayerRestroom": "Toilet Umum", + "restroomCategoryWelfare": "Lembaga kesejahteraan", + "restroomGradeExcellent": "Sangat baik", + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "Prakiraan numerik", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", "themeSystem": "Sistem", - "themeLight": "Terang", - "themeDark": "Gelap", - "moreSectionAbout": "Tentang", - "termsOfService": "Ketentuan Layanan", - "faq": "FAQ", - "openSourceLicenses": "Lisensi sumber terbuka", - "sponsorTitle": "Dukung DPIP", - "sponsorIntro": "DPIP berdedikasi menyediakan informasi mitigasi bencana secara real-time, tanpa iklan atau model bisnis lainnya. Dukungan Anda membantu kami menjaga server tetap berjalan dan terus mengembangkan aplikasi.", - "sponsorSubscriptions": "Langganan", - "sponsorRecommended": "Direkomendasikan", - "sponsorOneTime": "Sekali bayar", - "sponsorPerMonth": "{price} / bulan", - "sponsorRestore": "Pulihkan pembelian", - "sponsorTerms": "Ketentuan Penggunaan", - "sponsorPrivacy": "Kebijakan Privasi", - "sponsorRestoring": "Memulihkan pembelian…", - "sponsorRestoreUnavailable": "Tidak dapat terhubung ke toko. Coba lagi nanti.", - "commonClose": "Tutup", + "mapLayerSatelliteNdvi": "Himawari NDVI", + "typhoonLegendForecast": "Jalur prakiraan", + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "Curah hujan", + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "Ketuk penanda di peta untuk detail", + "onboardingSkipLeave": "Tetap lewati", + "onboardingBack": "Kembali", + "aedPlaceDesc": "Lokasi peletakan", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "Izin belum diberikan", + "restroomTypeFamily": "Toilet keluarga", + "typhoonValueKm": "{n} km", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "Tekanan", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "Pengecualian baterai", + "typhoonLabelNw": "NW", + "dpmDisasterFlood": "Banjir", + "moonPhaseWaxingCrescent": "Waxing crescent", + "restroomCategoryLeisure": "Tempat rekreasi", "mapLayerTemperature": "Suhu", - "trendRange24h": "24 jam", - "trendRange7d": "7 hari", - "trendNoData": "Tidak ada data tren", - "trendCumulativeTotal": "Total {total} mm", - "chartHourLabel": "{hour}j", - "mapLayerHumidity": "Kelembapan", - "mapLayerPressure": "Tekanan", + "aedCategory": "Kategori", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "Channels", + "monitorWaiting": "Menunggu data…", + "typhoonOverlayForecastCallouts": "Forecast tooltips", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "Koordinat episentrum", + "meshtasticVoltage": "Voltage", + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "Angin", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "Curah hujan", - "rainIntervalMenu": "Jendela akumulasi", - "rainIntervalNow": "Hari ini", - "rainInterval10m": "10 mnt", - "rainInterval1h": "1 jam", - "rainInterval3h": "3 jam", - "rainInterval6h": "6 jam", + "reportDetailMagnitude": "Magnitudo", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "Intensitas per wilayah", "rainInterval12h": "12 jam", - "rainInterval24h": "24 jam", - "rainInterval2d": "2 hr", - "rainInterval3d": "3 hr", - "mapLayerTyphoon": "Topan", - "typhoonNoActive": "Tidak ada topan aktif", - "typhoonWind": "Angin", - "typhoonGust": "Embusan", - "typhoonPressure": "Tekanan", - "typhoonMotion": "Bergerak", - "mapLayerMonitor": "Monitor Seismik", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "Peta Bencana", - "disasterMapOverlayMenuTooltip": "Lapisan peta bencana", - "disasterMapOverlaySectionLayers": "Lapisan", - "disasterMapOverlayAedTooltip": "Tampilkan lokasi AED", - "aedAddress": "Alamat", - "aedRegion": "Wilayah", - "aedCategory": "Kategori", - "aedType": "Jenis", - "aedPlaceDesc": "Lokasi peletakan", - "aedDescription": "Catatan", - "aedHoursWeekday": "Jam hari kerja", - "aedHoursSaturday": "Jam Sabtu", - "aedHoursSunday": "Jam Minggu", - "aedOpenRemark": "Catatan jam buka", - "aedEmergencyPhone": "Telepon darurat", - "mapLayerRestroom": "Toilet Umum", - "mapLayerShelter": "Tempat Evakuasi", - "disasterMapOverlayRestroomTooltip": "Tampilkan toilet umum", - "disasterMapOverlayShelterTooltip": "Tampilkan tempat evakuasi", - "dpmOpenInMaps": "Buka di peta", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "Tanah longsor", + "notifyMonitor": "Pemantau getaran kuat", + "onboardingStart": "Mulai", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / bulan", + "mapLayerPressure": "Tekanan", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", + "shelterIndoorLabel": "Penampungan dalam ruangan", + "notifyOptOff": "Nonaktif", + "reportFilterSortTime": "Waktu", + "mapLayerSatelliteCloudProbablyClear": "Probably clear", + "weatherModeThunderstorm": "Badai petir", + "homeViewOnMap": "Lihat di peta", + "reportFilterIntensityInfoLegacyTitle": "Lama (sebelum 2020)", + "typhoonLabelSpeed": "Past movement speed", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "Tidak dapat membuka {app}", + "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "Minimum hari ini", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", + "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "shelterCategoryLabel": "Jenis bencana", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", + "meshtasticStateConnecting": "Connecting…", + "moonTitle": "Moon", + "weatherRankingGust": "Hembusan", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "Jenis bencana tempat berlindung", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "Status server", + "notifySectionWeather": "Cuaca", + "meshtasticPreset": "Modem preset", + "dataSectionSeismic": "Seismik", + "changelogBodyEmpty": "Tidak ada catatan untuk rilis ini.", + "radarGlobalOutline": "Batas negara", + "notifyEew": "Peringatan gempa darurat", + "regionNationwide": "Seluruh negeri", + "moreNotifyLog": "Log notifikasi DPIP", + "regionCurrent": "Lokasi saat ini", + "dpmFilterSectionRestroom": "Jenis tempat", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "Salju", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "Info debug", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "Petir", + "homeForecastEmpty": "Tidak ada data prakiraan", + "sponsorOneTime": "Sekali bayar", + "mapLayerSatelliteBtdSplit": "Himawari Split Window", + "onboardingPermBackground": "Lokasi latar belakang", + "aedEmergencyPhone": "Telepon darurat", + "dpmOpenInMaps": "Buka di peta", + "meshtasticNotifyNodes": "Notify on new nodes", + "onboardingPermCriticalDesc": "Memungkinkan peringatan gempa yang mengancam jiwa tetap berbunyi bahkan dalam mode senyap atau Jangan Ganggu.", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", + "meshtasticSent": "Sent", + "homeForecastTitle": "Prakiraan 24 jam", + "typhoonLegendWarningAreas": "Area peringatan", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "Intensitas lokal 1 atau lebih", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "Lampau", + "restroomTypeFemale": "Toilet wanita", + "reportListToday": "Hari ini", + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "Memuat…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "Moderate typhoon", + "typhoonWind": "Angin", + "mapLayerSatelliteAsh": "Himawari Ash", + "rainInterval3h": "3 jam", + "reportListSearch": "Cari", + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "Satelit", + "reportFilterLocation": "Lokasi", + "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", + "typhoonIntensityTd": "Tropical depression", + "reportFilterDate": "Tanggal", + "sponsorRestoreUnavailable": "Tidak dapat terhubung ke toko. Coba lagi nanti.", + "homeForecastPop": "{pop}%", + "regionEmpty": "Belum ada wilayah tersimpan", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "Izinkan DPIP terus berjalan di latar belakang agar peringatan tidak tertunda atau terlewat.", + "mapNavDisaster": "Bencana", + "radarScanRangeSubtitle": "Menandai area yang benar-benar dipantau keempat radar.", + "aedHoursSunday": "Jam Minggu", + "reportDetailOriginTime": "Waktu kejadian", + "trendNoData": "Tidak ada data tren", + "onboardingPermLocation": "Lokasi", + "moreDiscord": "Komunitas Discord", + "mapNavPressure": "Tekanan", + "mapLayerSatelliteB13": "Himawari Infrared (B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "Belum ada catatan rilis", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "Hari mulai: dari 00:00(Taipei)", + "eewTitle": "Peringatan dini gempa", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "id", + "regionSelectCount": "{count}/{max} dipilih", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", + "meshtasticStateError": "Error", + "weatherModeOvercast": "Mendung", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "Kedalaman hiposenter", + "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", + "reportFilterDatePick": "Pilih tanggal", + "onboardingSkipStay": "Kembali", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "Tidak dapat memuat data. Silakan coba lagi.", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "Penampungan luar ruangan", + "meshtasticStateConnected": "Connected", + "mapNavRadar": "Radar", + "mapLayerSatelliteCloudClear": "Clear", + "eewSummary": "M{magnitude} · kedalaman {depth} km", + "locationBannerPermission": "Izin lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.", + "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", + "radarCountyOutlineHint": "Digambar di atas gema", + "windForecastCountyOutlineHint": "Digambar di atas bidang angin", + "homeRainTrendTitle": "Hujan 1 jam ke depan", + "moonPhaseFirstQuarter": "First quarter", + "mapLayerCategoryTyphoon": "Topan", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "Airtime (24h)", + "restroomTypeMixed": "Toilet campuran", + "restroomGradeGood": "Baik", + "notifyTsunami": "Informasi tsunami", + "navData": "Data", + "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "Perangkat ini tidak dapat melakukan panggilan telepon", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "Semua", + "weatherRankingMergeTo": "Gabung", + "notifyIntensity": "Laporan intensitas", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "Jendela akumulasi", + "reportDetailLocalFelt": "Gempa Dirasakan Lokal", + "meshtasticDevice": "Device", + "onboardingGrant": "Berikan", + "weatherModeRain": "Hujan", + "shelterVulnerableOkLabel": "Ramah kelompok rentan", + "stationSheetEmpty": "Ketuk stasiun untuk melihat bacaannya", + "typhoonLegendProbability": "Probabilitas serangan", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "Magnitudo", + "skyTimeMorning": "Pagi", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "Fitur eksperimental", + "onboardingTermsBody": "Harap baca pemberitahuan berikut sebelum menggunakan DPIP:\n\n• Semua informasi harus mengacu pada konten yang diterbitkan oleh Central Weather Administration (CWA) Taiwan.\n\n• Bergantung pada kondisi jaringan, server, aplikasi, dan sumber data hulu, informasi mungkin tidak diterima; kami berupaya sebaik mungkin untuk menghindari hal ini tetapi tidak dapat menjamin bahwa hal itu tidak akan pernah terjadi.\n\n• Guncangan kuat dapat mencapai lokasi Anda sebelum notifikasi tiba.\n\n• Peringatan dini gempa adalah hasil perhitungan cepat yang mungkin mengandung kesalahan yang signifikan — pahami hal ini dan gunakan dengan hati-hati.\n\n• Setiap tindakan yang tidak disahkan oleh pihak berwenang dapat menimbulkan risiko hukum; harap patuhi semua peraturan yang berlaku.\n\nSelain itu, untuk menyediakan peringatan yang dilokalkan, layanan ini mengumpulkan dan mengunggah perkiraan lokasi Anda dan pengidentifikasi push — di latar depan maupun latar belakang — semata-mata untuk menentukan peringatan mana yang akan dikirimkan kepada Anda.\n\nDengan mengetuk \"Setuju dan lanjutkan\", Anda mengonfirmasi bahwa Anda telah membaca, memahami, dan menyetujui hal-hal di atas.", + "reportFilterTitle": "Filter", + "onboardingPermCritical": "Peringatan kritis", + "trendCumulativeTotal": "Total {total} mm", + "languageName": "Bahasa Indonesia", + "reportListEmptyFiltered": "Tidak ada laporan yang cocok dengan filter", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "Topan", + "weatherModeSand": "Debu", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "Satelit", + "@dpmOpenInMaps": {}, + "notifyReport": "Laporan gempa", + "mapAppCoordinatesCopied": "Koordinat disalin", + "skyTimeNight": "Malam", + "sponsorRecommended": "Direkomendasikan", + "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", + "weatherRankingWind": "Kecepatan angin", + "feedStale": "Data mungkin sudah usang", + "homeForecastWind": "{direction} · Skala {level}", + "navHome": "Beranda", + "meshtasticRegionLabel": "Region", + "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "Lisensi sumber terbuka", + "weatherRankingLowest": "Terendah", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "Kedalaman", + "mapTimelineDataTime": "Waktu data {time}", + "radarScanRange": "Tampilkan jangkauan pindai", + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "Rentang {value}°C", + "weatherRankingExtremeHigh": "Maksimum hari ini", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "Detail rilis", + "sponsorPrivacy": "Kebijakan Privasi", + "reportDetailLocalIntensity": "Intensitas di lokasi Anda", + "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", + "meshtasticAirtime": "Air time (TX)", + "shelterCapacityValue": "{n} orang", + "lightningLegendCc": "Awan–awan · {minutes} mnt", + "meshtasticSendHint": "Message to broadcast", + "monitorDelay": "Latensi {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "Tidak", + "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "Menjaga batas kecamatan tetap terbaca di bawah gema radar.", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", + "radarScanRangeHint": "Di luar kotak berarti tak terpantau", + "typhoonPickerTd": "Tropical depression TD {no}", + "mapLayerSatelliteWatervapor": "Himawari Water Vapour", + "regionAddButton": "Tambah wilayah", + "displaySettings": "Tampilan", + "restroomGradePoor": "Di bawah standar", + "restroomCategoryTourist": "Kawasan wisata", + "locationBannerServiceOff": "Layanan lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.", + "mapLayerStyleTooltip": "Colour style", + "lightningLegendCg": "Awan–tanah · {minutes} mnt", + "skyTimeAuto": "Otomatis", + "appLogs": "Log aplikasi", + "feedConnecting": "Menghubungkan…", + "notifyBannerDisabled": "Notifikasi mati — Anda tidak akan menerima peringatan bencana.", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "Kelembapan", + "typhoonValueMs": "{n} m/s", + "homeForecastHumidity": "Kelembapan {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "Transportasi", + "reportFilterLocationHint": "mis. Hualien, lepas pantai", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "Jarak", + "meshtasticSnrTrend": "Tren sinyal (SNR)", + "meshtasticBatteryTrend": "Tren baterai", + "typhoonOverlayMenuTooltip": "Typhoon overlay options", + "mapLayerSatelliteBtdOzone": "Himawari Tropopause", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "Gempa bumi", + "mapLayerDisasterMap": "Peta Bencana", + "weatherModeFog": "Kabut", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", + "moreAnnouncements": "Pengumuman", + "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "Kantor pelayanan publik", + "typhoonLegendCurrent": "Pusat saat ini", + "aedAddress": "Alamat", + "mapLayerAed": "AED", + "changelogTypePrerelease": "Beta", + "reportFilterIntensityInfoModernBody": "Tingkat 0–4, 5−, 5+, 6−, 6+, 7. Slider filter memakai skala baru; peristiwa lama tetap memakai label lama di daftar.", + "typhoonOverlayWeatherNone": "None", + "mapLayerStyleGray": "Grayscale (JMA)", + "weatherModeAuto": "Otomatis", + "typhoonLabelProbCircle": "70% probability circle", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "Terima semua", + "displayTheme": "Tema", + "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "Past movement direction", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "Wilayah tersimpan", + "typhoonLegendCone": "Kerucut prakiraan", + "moreCwaEew": "Peringatan dini gempa CWA", + "onboardingPermsTitle": "Izin", + "mapLayerStyleJma": "Cloud-top enhancement (JMA)", + "rainInterval10m": "10 mnt", + "weatherRankingAnalysisLow": "Min {value}", + "meshtasticConnectAnyway": "Connect anyway", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", + "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", + "chartHourLabel": "{hour}j", + "mapLayerShelter": "Tempat Evakuasi", + "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", + "mapLayerSatelliteNdwi": "Himawari NDWI", + "disasterMapOverlayShelterTooltip": "Tampilkan tempat evakuasi", + "mapNavHumidity": "Kelembapan", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "Urutkan menurut intensitas", + "homeRainTrendNoData": "Tidak ada data", + "mapLayerCategoryRadar": "Radar", + "meshtasticShortName": "Short name", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "Himawari Airmass", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "Detail jalur", + "dataSectionWeather": "Cuaca", + "aedHoursWeekday": "Jam hari kerja", + "homeActiveEventsTitle": "Peristiwa aktif", + "weatherRankingAnalysisHigh": "Maks {value}", + "faq": "FAQ", + "typhoonHistoryLive": "Langsung", + "eewSerial": "Laporan {serial}", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "Urutan", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "Laporan gempa", + "typhoonNoActive": "Tidak ada topan aktif", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", + "navEvents": "Kejadian", + "onboardingTermsTitle": "Ketentuan Layanan", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "Nama kecamatan", + "notifySetFailed": "Tidak dapat menyimpan pengaturan. Silakan coba lagi.", + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "notifyAnnouncement": "Pengumuman", + "onboardingIntroTitle": "Selamat datang di DPIP", + "regionCurrentUnavailable": "Tidak dapat memperoleh lokasi saat ini", + "languageSystem": "Bawaan sistem", + "skyTimeSunset": "Matahari terbenam", + "mapLayerSatelliteDust": "Himawari Dust", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "Ubah", + "weatherDynamicState": "Animasi cuaca", + "mapPlaceholderDisabled": "Peta (dinonaktifkan sementara)", + "moonNow": "Sekarang", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "Penampakan", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "Terbit dan terbenam", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "Mendatang", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "Kalender", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "Jarak", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "km", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "Ukuran tampak", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "Bulan terbit", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "Bulan terbenam", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "Bulan baru berikutnya", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "Di atas ufuk sepanjang hari", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "Tidak ada hari ini", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "Matahari", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "Matahari terbit, senja, dan istilah surya", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "Cahaya siang", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "Senja", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "Cahaya", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "Jam matahari", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "Istilah surya", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "Matahari terbit", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "Matahari terbenam", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "Tengah hari surya", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "Panjang hari", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "Sipil", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "Nautika", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "Astronomi", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "Golden hour pagi", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "Golden hour sore", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "Blue hour", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "Persamaan waktu", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "mnt", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "Istilah berikutnya", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "Planet", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "Di mana malam ini, dan seberapa terang", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "Saat ini", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "Di atas ufuk", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "Di bawah ufuk", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "Terlalu dekat Matahari", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "Magnitudo", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "Elongasi", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "Waktu", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "Petang", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "Pagi", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "Jarak", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "Ketinggian", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "Merkurius", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "Venus", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "Mars", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "Jupiter", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "Saturnus", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "Uranus", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "Neptunus", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "Ekuinoks Musim Semi", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "Pure Brightness", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "Grain Rain", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "Awal Musim Panas", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "Grain Full", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "Grain in Ear", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "Solstis Musim Panas", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "Minor Heat", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "Major Heat", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "Awal Musim Gugur", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "End of Heat", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "White Dew", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "Ekuinoks Musim Gugur", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "Cold Dew", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "Frost Descent", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "Awal Musim Dingin", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "Minor Snow", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "Major Snow", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "Solstis Musim Dingin", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "Minor Cold", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "Major Cold", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "Awal Musim Semi", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "Rain Water", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "Awakening of Insects", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "Malam ini", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "Apa yang terlihat, dan kapan", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "Jendela pengamatan", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "Malam astronomis", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "Tak pernah gelap total", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "Jendela gelap", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "Bulan terbit sepanjang malam", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "Total gelap", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "Cahaya bulan", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "Hujan meteor", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "Radian tidak terbit", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "/jam", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "Lintasan satelit", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "Sasaran yang terlihat", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "Quadrantids", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "Lyrids", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "Eta Aquariids", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "Delta Aquariids", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "Perseids", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "Orionids", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "Taurids Selatan", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "Leonids", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "Geminids", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "Ursids", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "Gugus terbuka", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "Gugus bola", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "Galaksi spiral", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "Galaksi elips", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "Galaksi tak beraturan", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "Nebula planeter", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "Sisa supernova", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "Nebula emisi", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "Nebula refleksi", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "Asterisme", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "Almanak", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "Tanggal lunisolar dan gerhana mendatang", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "Hari ini", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "Masehi", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "Lunisolar", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "Tahun", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app} (bawaan)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "Panjang bulan", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "Salin koordinat", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "30 hari", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "Koordinat disalin", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "29 hari", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "Tidak dapat membuka {app}", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "Kabisat ", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "Perangkat ini tidak dapat melakukan panggilan telepon", - - "mapOverlaySectionReference": "Lapisan referensi", - "mapLayerCategoryEarthquake": "Gempa", - "mapLayerCategoryTyphoon": "Topan", - "mapLayerCategoryWeather": "Pengamatan cuaca", - "mapLayerCategorySatellite": "Satelit", - "mapLayerCategoryRadar": "Radar", - "mapLayerCategoryLife": "Kehidupan sehari-hari", - "mapLayerCategoryForecast": "Prakiraan numerik", "mapOverlaySectionMap": "Peta", - "rainIntervalSection": "Jendela waktu", - - "mapTownLabels": "Nama kecamatan", - "mapTownLabelsHint": "Tampilkan nama kecamatan saat diperbesar", - - "mapTerrainRelief": "Relief terrain", - "mapTerrainReliefHint": "Tampilkan relief terrain di peta dasar", - - "dpmSheetEmpty": "Ketuk penanda di peta untuk detail", - "dpmAddress": "Alamat", - "restroomTypeLabel": "Jenis", - "restroomCategoryLabel": "Kategori", - "restroomGradeLabel": "Nilai", - "restroomTypeFemale": "Toilet wanita", - "restroomTypeMale": "Toilet pria", - "restroomTypeMixed": "Toilet campuran", - "restroomTypeAccessible": "Toilet aksesibel", - "restroomTypeGenderNeutral": "Toilet netral gender", - "restroomTypeFamily": "Toilet keluarga", - "restroomTypeUnspecified": "Tidak ditentukan", - "restroomCategoryTransport": "Transportasi", - "restroomCategoryPark": "Taman", - "restroomCategoryCommercial": "Tempat komersial", - "restroomCategoryReligious": "Tempat ibadah", - "restroomCategoryCultural": "Tempat budaya", - "restroomCategoryGovernment": "Kantor pelayanan publik", - "restroomCategoryWelfare": "Lembaga kesejahteraan", - "restroomCategoryTourist": "Kawasan wisata", - "restroomCategoryLeisure": "Tempat rekreasi", - "restroomCategoryOther": "Lainnya", - "restroomGradeExcellent": "Sangat baik", - "restroomGradeGood": "Baik", - "restroomGradeAverage": "Sedang", - "restroomGradePoor": "Di bawah standar", - "shelterAddressLabel": "Alamat", - "shelterCapacityLabel": "Kapasitas", - "shelterCapacityValue": "{n} orang", - "shelterCategoryLabel": "Jenis bencana", - "shelterIndoorLabel": "Penampungan dalam ruangan", - "shelterOutdoorLabel": "Penampungan luar ruangan", - "shelterVulnerableOkLabel": "Ramah kelompok rentan", - "dpmYes": "Ya", - "dpmNo": "Tidak", - "stationSheetEmpty": "Ketuk stasiun untuk melihat bacaannya", - "monitorDelay": "Latensi {value} s", - "monitorWaiting": "Menunggu data…", - "mapLegendUnit": "Satuan: {unit}", - "typhoonLegendPast": "Jalur aktual", - "typhoonLegendForecast": "Jalur prakiraan", - "typhoonLegendForecastPoint": "Titik prakiraan", - "typhoonLegendCurrent": "Pusat saat ini", - "typhoonLegendCone": "Kerucut prakiraan", - "mapLegendExpand": "Legenda", - "mapLegendCollapse": "Sembunyikan legenda", - "mapMyLocation": "Lokasi saya", - "mapResetNorth": "Kembali ke utara", - "typhoonLegendCircle15": "Lingkar angin kencang", - "typhoonLegendCircle25": "Lingkar badai", - "typhoonLegendProbability": "Probabilitas serangan", - "typhoonLegendWarningAreas": "Area peringatan", - "typhoonWarningTitle": "Peringatan topan", - "typhoonWarningAreas": "Wilayah: {areas}", - "typhoonTrackDetail": "Detail jalur", - "typhoonHistoryTitle": "Waktu data", - "typhoonHistoryLive": "Langsung", - "typhoonSatelliteTitle": "Satelit", - "typhoonDataTime": "Data time\n{time}", - "typhoonForecastLead": "Forecast +{hours} h", - "typhoonIntensityIntense": "Intense typhoon", - "typhoonIntensityMild": "Mild typhoon", - "typhoonIntensityModerate": "Moderate typhoon", - "typhoonIntensityTd": "Tropical depression", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "Tropical depression TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "Past movement direction", - "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", - "typhoonLabelGust": "Peak gust", - "typhoonLabelNe": "NE", - "typhoonLabelNw": "NW", - "typhoonLabelPosition": "Centre location", - "typhoonLabelPressure": "Central pressure", - "typhoonLabelProbCircle": "70% probability circle", - "typhoonLabelSe": "SE", - "typhoonLabelSpeed": "Past movement speed", - "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", - "typhoonLabelSw": "SW", - "typhoonLabelWind": "Max. sustained wind near centre", - "typhoonLegendCircleAvg": "Average circle", - "typhoonOverlayMenuTooltip": "Typhoon overlay options", - "typhoonOverlayProbabilityHint": "Hides the forecast cone", - "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", - "typhoonOverlaySectionExtra": "Overlays", - "typhoonOverlaySectionStorm": "Storm wind", - "typhoonOverlaySectionWeather": "Weather underlay", - "typhoonOverlayStormBandSubtitle": "With average circle", - "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", - "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", - "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", - "typhoonOverlayWeatherHint": "Aligned to bulletin time", - "typhoonOverlayWeatherNone": "None", - "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", - "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", - "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} hPa", - "typhoonValueKm": "{n} km", - "typhoonValueLat": "{lat}°N", - "typhoonValueLon": "{lon}°E", - "typhoonValueMs": "{n} m/s", - "typhoonOverlayForecastCallouts": "Forecast tooltips", - "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", - "dpmFilterSectionRestroom": "Jenis tempat", - "dpmFilterSectionRestroomType": "Jenis toilet", - "dpmFilterSectionShelter": "Jenis bencana tempat berlindung", - "dpmDisasterFlood": "Banjir", - "dpmDisasterEarthquake": "Gempa", - "dpmDisasterLandslide": "Tanah longsor", - "dpmDisasterTsunami": "Tsunami", - "dpmDisasterSlope": "Bencana lereng", - "dpmDisasterNuclear": "Kecelakaan nuklir", - "skyTime": "Waktu langit", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "Gerhana bulan", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "Otomatis", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "Gerhana matahari", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "Fajar", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "Tidak ada", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "Matahari terbit", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "Total", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "Pagi", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "Sebagian", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "Siang", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "Cincin", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "Sore", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "Penumbra", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "Jam emas", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "Tikus", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "Matahari terbenam", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "Kerbau", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "Senja", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "Macan", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "Malam", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "Kelinci", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "Berawan", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "Naga", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "Mendung", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "Ular", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "Salju", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "Kuda", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "Debu", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "Kambing", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "Tampilkan jangkauan pindai", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "Monyet", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "Menandai area yang benar-benar dipantau keempat radar.", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "Ayam", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "Di luar kotak berarti tak terpantau", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "Anjing", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "Opsi lapisan radar", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "Babi", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "Batas kabupaten/kota", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "Pasang surut", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "Batas negara", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "Purnama, perbani, dan tarikan Bulan", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "Bingkai luar setiap negara", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "Hanya gaya astronomis — bukan tabel pasang surut pelabuhan. Untuk tinggi muka air gunakan tabel CWA.", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "Digambar di atas gema", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "Saat ini", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "Menjaga batas wilayah tetap terbaca di bawah gema radar.", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "Siklus", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "Batas kecamatan", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "Purnama", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "Kisi yang lebih rapat", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "Perbani", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "Menjaga batas kecamatan tetap terbaca di bawah gema radar.", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "Sedang", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "Opsi lapisan prakiraan curah hujan", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "Tarikan Bulan", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "Opsi lapisan prakiraan angin", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "Pasang setimbang", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "Digambar di atas bidang angin", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "m", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "Bingkai luar setiap negara", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "Purnama perigee berikutnya", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "Jaring yang lebih halus", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "Titik balik", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "Laporan {serial}", - "eewMaxIntensity": "Intensitas maks", - "eewLocalIntensity": "Perkiraan di lokasi", - "eewSWave": "Gelombang S", - "eewArrived": "Tiba", - "eewCountdown": "{seconds} detik" + "tideHigh": "Tinggi", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "Rendah", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "Peta langit", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "Langit yang terlihat mata telanjang", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "U", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "T", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "S", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "B", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "elemen orbit {days} hari lalu", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}bulan {month}, hari {day}", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "Tidak ada hujan meteor", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "Tidak ada lintasan terlihat dalam 48 jam", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "Data orbit tidak terbaca", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "Tidak ada sasaran cukup tinggi", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "Katalog bintang tidak terbaca", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 2e393108b..4aa35a4fa 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1,679 +1,1739 @@ { - "@@locale": "ja", - "languageName": "日本語", - "navHome": "ホーム", - "navEvents": "イベント", - "navMap": "地図", - "navData": "データ", - "navEarthquake": "地震", - "dataSectionSeismic": "地震", - "dataEarthquakeSubtitle": "地震報告", - "dataSectionWeather": "気象", - "dataWeatherRankingSubtitle": "即時観測ランキング", - "weatherRankingTitle": "観測ランキング", - "weatherRankingMeta": "データ時刻:{time}\n観測点 {count}", - "weatherRankingEmpty": "並べ替え可能な観測がありません", - "weatherRankingBy": "並び", - "weatherRankingHighest": "最高", - "weatherRankingLowest": "最低", - "weatherRankingMergeTo": "統合", - "weatherRankingMergeTown": "町村", - "weatherRankingMergeCounty": "県市", - "weatherRankingWind": "風速", - "weatherRankingGust": "突風", + "typhoonValueLat": "北緯 {lat} 度", + "onboardingSkipBody": "位置情報と通知を許可しないと、DPIP はお近くの地震や災害をリアルタイムでお知らせできません。設定から後で許可することもできます。", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24時間", + "homeRainTrendHeavyStopping": "{minutes}分後に大雨が止む見込みです", + "mapTimelineObserved": "観測", + "regionSelectTitle": "地域を選択", + "skyTimeNoon": "正午", + "radarCountyOutlineSubtitle": "レーダーエコーの下でも県市境界が見えるようにします。", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "トイレの種類", + "mapLayerSatelliteB03": "ひまわり 可視赤(B03)", + "reportFilterIntensity": "震度", + "mapLayerLightning": "雷", + "restroomTypeMale": "男性用トイレ", + "meshtasticLastReceived": "Last received", + "reportDetailSortByCounty": "地域順に並べ替え", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "にわか雨の可能性があります", + "meshtasticUptime": "Uptime", "weatherRankingTempExtremes": "気温極値", - "weatherRankingExtremeHigh": "今日の最高", - "weatherRankingExtremeLow": "今日の最低", + "themeLight": "ライト", + "mapTerrainReliefHint": "ベースマップに地形の陰影を表示", + "meshtasticEmptyMessage": "(empty message)", + "moreSectionRegion": "地域", + "dpmDisasterEarthquake": "震災", + "mapLayerSatellite": "ひまわり 赤外線(B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "土曜の開館時間", + "dpmDisasterSlope": "斜面災害", + "moonPhaseNew": "New moon", + "notifySectionEew": "緊急地震速報", + "mapResetNorth": "北を上にする", + "rainInterval2d": "2日", + "mapTownLabelsHint": "拡大すると郷鎮名を表示", + "commonCancel": "Cancel", + "notifyOptTsunamiWarning": "津波警報のみ", + "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "詳細設定", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "日較差", + "notifySettingsMenu": "通知設定", + "typhoonHistoryTitle": "資料時刻", + "mapAppDefault": "{app}(デフォルト)", + "trendRange24h": "24時間", + "mapLayerStyleJmaTooltip": "グレースケールをベースに −40 °C 以下を着色し、雲頂高度を強調", "weatherRankingRecordedAt": "記録時刻 {time}", - "weatherRankingAnalysisCurrent": "現在 {value}°C", - "weatherRankingAnalysisHigh": "最高 {value}", - "weatherRankingAnalysisLow": "最低 {value}", - "weatherRankingAnalysisRange": "較差 {value}°C", - "reportListEmpty": "地震報告はありません", - "reportListEmptyFiltered": "条件に一致する地震報告はありません", - "reportListMeta": "M{magnitude} · {depth} km", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "km", - "reportListLocalFelt": "局地有感", - "reportListToday": "今日", - "reportListYesterday": "昨日", - "reportListDayCount": "{count}", - "reportListEnd": "これ以上ありません", - "reportFilterTitle": "絞り込み", - "reportFilterSort": "並び替え", - "reportFilterSortTime": "時間", - "reportFilterSortIntensity": "震度", - "reportFilterSortMagnitude": "規模", - "reportFilterSortDepth": "深さ", + "mapLayerRain": "降水量", + "mapLayerQpesums": "1時間降水量予報", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "地図", + "mapTerrainRelief": "地形の立体感", + "eewMaxIntensity": "最大震度", + "mapLegendCollapse": "凡例を閉じる", + "changelogTitle": "更新履歴", "reportFilterOrderDesc": "降順", - "reportFilterOrderAsc": "昇順", - "reportFilterIntensity": "震度", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", "reportFilterIntensityInfoTitle": "震度の新制と旧制", - "reportFilterIntensityInfoIntro": "気象署は 2020 年 1 月 1 日(台北時間)から新制震度を採用しています。", - "reportFilterIntensityInfoLegacyTitle": "旧制(2020 年より前)", - "reportFilterIntensityInfoLegacyBody": "震度は 0–7 のみ。5弱/5強/6弱/6強の区分はありません。", - "reportFilterIntensityInfoModernTitle": "新制(2020 年以降)", - "reportFilterIntensityInfoModernBody": "震度は 0–4、5弱、5強、6弱、6強、7。フィルタは新制に準拠し、それ以前の地震はリストで旧制表記になります。", - "reportFilterMagnitude": "マグニチュード", - "reportFilterDepth": "深さ", - "reportFilterDepthKm": "{depth} km", - "reportFilterDate": "日付", - "reportFilterDatePick": "日付を選択", - "reportFilterDateStartNote": "開始日:当日 00:00(台北時間)", + "mapLayerTyphoon": "台風", + "radarOverlayMenuTooltip": "レーダーレイヤー設定", + "mapMyLocation": "現在地", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "Nodes", + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "強風域 + 平均円(紫)", + "aedType": "種類", + "termsOfService": "利用規約", + "typhoonLegendCircle25": "暴風域(50kt)", + "sponsorTitle": "DPIP を支援", + "mapNavSatellite": "衛星", + "homeRainTrendUpdated": "更新 {time}", + "onboardingNext": "次へ", + "weatherRankingMergeTown": "町村", + "mapLayerMonitor": "強震モニタ", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "サブスクリプション", + "typhoonValueLon": "東経 {lon} 度", + "skyTime": "空の時刻", + "weatherModeCloudy": "曇り", + "skyTimeDusk": "薄暮", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "終了日:当日 24:00(台北時間)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "場所", - "reportFilterLocationHint": "例:花蓮、海域", - "reportFilterAny": "指定なし", - "reportFilterApply": "適用", - "reportFilterReset": "リセット", - "reportListSearch": "検索", - "reportDetailTitle": "地震レポート", - "reportDetailNumbered": "No.{number} 顕著有感地震", - "reportDetailLocalFelt": "局地的な有感地震", - "reportDetailInfo": "詳細情報", - "reportDetailOriginTime": "発震時刻", - "reportDetailEpicenter": "震央座標", - "reportDetailMagnitude": "地震規模", - "reportDetailDepth": "震源の深さ", - "reportDetailAreaIntensity": "地域別震度", - "reportDetailLocalIntensity": "現在地の震度", - "reportDetailLocalIntensityUnavailable": "震度情報なし", - "reportDetailSortByIntensity": "震度順に並べ替え", - "reportDetailSortByCounty": "地域順に並べ替え", - "reportDetailImage": "地震レポート画像", - "reportDetailImageUnavailable": "レポート画像はまだありません", - "reportDetailOpenReport": "レポートページ", - "reportDetailReplay": "リプレイ", - "navMore": "その他", - "appLogs": "アプリログ", - "changelogTitle": "更新履歴", - "changelogEmpty": "リリースノートはまだありません", - "changelogTypePrerelease": "ベータ", - "changelogTypeStable": "正式", - "changelogCurrentVersion": "現行", - "changelogVersionDetails": "リリース詳細", - "changelogBodyEmpty": "このリリースの説明はありません。", - "mapPlaceholderDisabled": "地図(一時的に無効)", - "moreSectionRegion": "地域", - "moreSectionNotify": "通知", - "moreSectionDisplay": "表示", - "regionManageTitle": "登録地域", - "regionAddButton": "地域を追加", - "regionEmpty": "登録地域がありません", - "regionSelectTitle": "地域を選択", - "regionSelectCount": "{count}/{max} 件選択中", - "regionSelectFull": "地域は最大 {max} 件まで登録できます", - "regionEdit": "変更", - "moreSectionAdvanced": "詳細設定", - "moreDeveloper": "デバッグ情報", - "experimentalFeatures": "実験的機能", - "moreSectionLinks": "関連リンク", - "moreCwaEew": "中央気象署 緊急地震速報", - "moreTremReport": "TREM 検知レポート", - "moreServerStatus": "サーバー状態", - "moreAnnouncements": "お知らせ", - "moreDiscord": "Discord コミュニティ", - "moreNotifyLog": "DPIP 通知送信履歴", - "moreLinkOpenFailed": "リンクを開けませんでした", - "weatherDynamicState": "天気アニメーション", - "weatherDynamicStateSubtitle": "ホーム背景の天気を上書きします", - "weatherModeAuto": "自動", - "weatherModeClear": "晴れ", - "weatherModeRain": "雨", - "weatherModeFog": "霧", - "weatherModeThunderstorm": "雷雨", - "commonLoading": "読み込み中…", - "commonRetry": "再試行", - "commonError": "問題が発生しました", - "commonFetchFailed": "データを取得できませんでした。しばらくしてから再度お試しください。", - "commonEmpty": "表示する項目がありません", - "feedConnecting": "接続中…", - "feedStale": "データが最新でない可能性があります", - "feedOffline": "接続が切断されました", - "eewTitle": "緊急地震速報", - "eewNone": "現在、緊急地震速報はありません", - "eewSummary": "M{magnitude}・深さ {depth} km", - "regionNationwide": "全国", - "regionCurrent": "現在地", - "regionCurrentUnavailable": "現在地を取得できません", - "weatherPrecipitation": "降水量", - "weatherHumidity": "湿度", - "weatherDataTime": "{station} · データ時刻 {time}", - "homeViewOnMap": "地図で見る", - "homeForecastTitle": "24時間予報", + "meshtasticSilent": "Silent", + "reportFilterSortMagnitude": "規模", + "mapLayerCategoryEarthquake": "地震", + "mapLayerSatelliteB12": "ひまわり オゾン(B12)", + "typhoonLegendPast": "実況経路", + "restroomCategoryOther": "その他", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "高 {high}° · 低 {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "体感 {temp}°", - "homeForecastHumidity": "湿度 {value}%", - "homeForecastWind": "{direction} · 風力{level}", - "homeForecastUnavailable": "地域を選ぶと予報を表示します", - "homeForecastEmpty": "予報データがありません", - "homeActiveEventsTitle": "発生中の事象", - "homeActiveEventsEmpty": "発生中の事象はありません", - "homeRainTrendTitle": "今後1時間の雨", - "homeRainTrendMinute": "{minute}分", - "homeRainTrendUpdated": "更新 {time}", - "homeRainTrendNoData": "データなし", - - "homeRainTrendScattered": "にわか雨の可能性があります", - "homeRainTrendLightSustained": "今後1時間は小雨が続きます", - "homeRainTrendLightStopping": "{minutes}分後に小雨が止む見込みです", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "設定を開く", + "mapLegendExpand": "凡例", + "eewNone": "現在、緊急地震速報はありません", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "津波情報・津波警報", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "Node options", + "onboardingAgreeContinue": "同意して続行", + "meshtasticNodeId": "Node ID", + "commonRetry": "再試行", + "reportDetailNumbered": "No.{number} 顕著有感地震", + "typhoonOverlayStormBandSubtitle": "平均円付き", + "disasterMapOverlayRestroomTooltip": "トイレを表示", + "weatherRankingTitle": "観測ランキング", "homeRainTrendHeavySustained": "今後1時間は大雨が続きます", - "homeRainTrendHeavyStopping": "{minutes}分後に大雨が止む見込みです", - "mapLayers": "レイヤー", - "mapLayerOrderTitle": "レイヤーの順番", - "mapLayerOrderReset": "既定の順序に戻す", - "mapLayerRadar": "レーダー合成エコー図", - "mapLayerSatellite": "ひまわり 赤外線(B13)", - "mapLayerSatelliteB01": "ひまわり 可視青(B01)", - "mapLayerSatelliteB02": "ひまわり 可視緑(B02)", - "mapLayerSatelliteB03": "ひまわり 可視赤(B03)", - "mapLayerSatelliteB04": "ひまわり 近赤外(B04)", + "notifySectionTsunami": "津波", + "restroomCategoryPark": "公園", + "moreLinkOpenFailed": "リンクを開けませんでした", + "themeDark": "ダーク", + "sponsorRestore": "購入を復元", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "Traffic", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD カーブ——熱帯低気圧の強度解析に使う階段グレースケール", + "disasterMapOverlayAedTooltip": "AEDの位置を表示", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "湿度", + "mapLayerSatelliteTransparentNight": "夜間 = 透明、地図が透ける", + "meshtasticScanning": "Scanning…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "地域は最大 {max} 件まで登録できます", + "meshtasticTitle": "Meshtastic", + "navMore": "その他", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "レイヤー", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "ひまわり 近赤外(B05)", - "mapLayerSatelliteB06": "ひまわり 近赤外(B06)", - "mapLayerSatelliteB07": "ひまわり 短波長赤外(B07)", - "mapLayerSatelliteB08": "ひまわり 上層水蒸気(B08)", - "mapLayerSatelliteB09": "ひまわり 中層水蒸気(B09)", - "mapLayerSatelliteB10": "ひまわり 下層水蒸気(B10)", - "mapLayerSatelliteB11": "ひまわり 二酸化硫黄/雲相(B11)", - "mapLayerSatelliteB12": "ひまわり オゾン(B12)", - "mapLayerSatelliteB13": "ひまわり 赤外線(B13)", - "mapLayerSatelliteB14": "ひまわり 長波長赤外線(B14)", - "mapLayerSatelliteB15": "ひまわり 長波長赤外線(B15)", - "mapLayerSatelliteB16": "ひまわり 二酸化炭素(B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "北東", + "meshtasticCopied": "Message copied", + "reportListEmpty": "地震報告はありません", + "reportListEnd": "これ以上ありません", "mapLayerSatelliteTruecolor": "ひまわり トゥルーカラー", - "mapLayerSatelliteNaturalcolor": "ひまわり ナチュラルカラー", - "mapLayerSatelliteAsh": "ひまわり 火山灰", - "mapLayerSatelliteDust": "ひまわり 黄砂", - "mapLayerSatelliteAirmass": "ひまわり エアマス", - "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", - "mapLayerSatelliteWatervapor": "ひまわり 水蒸気", - "mapLayerSatelliteBtdSplit": "ひまわり スプリットウィンドウ", - "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", - "mapLayerSatelliteBtdWvirw": "ひまわり オーバーシューティングトップ", - "mapLayerSatelliteBtdSo2": "ひまわり 二酸化硫黄/雲相", - "mapLayerSatelliteBtdCo2": "ひまわり 巻雲/雲頂高度", - "mapLayerSatelliteBtdOzone": "ひまわり 対流圏界面", - "mapLayerSatelliteCloudtop": "ひまわり 雲頂温度", - "mapLayerSatelliteCloudmask": "ひまわり 雲マスク", - "mapLayerSatelliteSst": "ひまわり 海面水温", - "mapLayerSatelliteNdvi": "ひまわり NDVI", - "mapLayerSatelliteNdwi": "ひまわり NDWI", - "mapLayerSatelliteMndwi": "ひまわり MNDWI", + "typhoonOverlaySectionExtra": "オーバーレイ", + "eewSWave": "S波", + "meshtasticBusyTitle": "Another app is using this radio", + "restroomCategoryCultural": "文化・娯楽施設", + "typhoonLabelWind": "中心付近の最大風速", + "radarGlobalOutlineHint": "各国の国境外枠", + "notifyEvacuation": "防災情報", + "typhoonLegendCircle15": "強風域(30kt)", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "Astronomy", + "homeRainTrendLightSustained": "今後1時間は小雨が続きます", + "commonError": "問題が発生しました", + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "現在", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "レポートページ", + "trendRange7d": "7日間", + "typhoonWarningAreas": "対象地域:{areas}", + "rainIntervalSection": "集計時間", + "notifyTitle": "通知", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "区分", + "sponsorRestoring": "購入を復元しています…", + "sponsorIntro": "DPIP はリアルタイムの防災情報の提供に取り組んでおり、広告やその他の収益モデルはありません。皆さまのご支援はサーバーの運用と継続的な開発に役立ちます。", + "shelterAddressLabel": "住所", + "typhoonLabelStormAvg": "暴風域の平均半径", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "商業・営業施設", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "地域", + "homeRainTrendLightStopping": "{minutes}分後に小雨が止む見込みです", + "reportDetailInfo": "詳細情報", + "mapNavWind": "風向", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "風予報レイヤー設定", + "dataWeatherRankingSubtitle": "即時観測ランキング", + "rainInterval6h": "6時間", + "homeRainTrendMinute": "{minute}分", + "restroomTypeUnspecified": "未設定", + "typhoonOverlayProbabilityHint": "予報円を隠します", "mapLayerSatelliteGlobalOutline": "国境線", - "mapLayerSatelliteRgbComposite": "RGB 合成(JMA レシピ)", - "mapLayerSatelliteCloudClear": "晴れ", - "mapLayerSatelliteCloudProbablyClear": "おそらく晴れ", - "mapLayerSatelliteCloudProbablyCloudy": "おそらく雲", + "mapNavTemperature": "気温", + "typhoonLegendForecastPoint": "予報点", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "昨日", + "moreSectionLinks": "関連リンク", + "feedOffline": "接続が切断されました", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "表示", + "rainInterval3d": "3日", + "defaultMapLayerSubtitle": "地図タブを開いたときに表示するレイヤーです。下部ナビのアイコンとラベルもこれに合わせます。", + "aedDescription": "備考", + "typhoonOverlayWeatherRadarTooltip": "通報時刻に最も近いレーダー", + "onboardingPermLocationDesc": "あなたの所在地に合わせて警報を配信します。", + "mapLayerSatelliteB16": "ひまわり 二酸化炭素(B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "発生中の事象はありません", + "typhoonLabelPosition": "中心位置", + "weatherRankingBy": "並び", + "typhoonIntensityMild": "弱い台風", + "windForecastGlobalOutlineHint": "各国の国境外枠", + "rainInterval1h": "1時間", + "eewLocalIntensity": "現在地の推定", + "mapLayerRadar": "レーダー合成エコー図", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "宗教・礼拝施設", + "meshtasticRole": "Role", "mapLayerSatelliteCloudCloudy": "雲", - "mapLayerSatelliteTransparentWarm": "晴れ(暖域) = 透明、地図が透ける", - "mapLayerSatelliteTransparentReflectance": "低反射率・夜間 = 透明、地図が透ける", - "mapLayerSatelliteTransparentZero": "差ゼロ = 透明(信号なし)", - "mapLayerSatelliteTransparentNight": "夜間 = 透明、地図が透ける", - "mapLayerSatelliteTransparentNoData": "データなし(陸上) = 透明", - "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(植生なし)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(水域なし)", - "mapLayerSatelliteTransparentClear": "晴れ = 透明、地図が透ける", + "skyTimeSunrise": "日の出", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "No messages yet", + "onboardingPermNotifyDesc": "地震、天気、災害の発生時に、警報をすぐお届けします。", + "radarTownOutline": "市町村境界", "mapLayerStyleSection": "色調", - "mapLayerStyleTooltip": "色調", - "mapLayerStyleGray": "グレースケール(JMA)", - "mapLayerStyleGrayTooltip": "気象庁の赤外画像の慣例:温度が低いほど白", - "mapLayerStyleJma": "雲頂強調(JMA)", - "mapLayerStyleJmaTooltip": "グレースケールをベースに −40 °C 以下を着色し、雲頂高度を強調", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD カーブ——熱帯低気圧の強度解析に使う階段グレースケール", - "mapLayerQpesums": "1時間降水量予報", - "mapLayerLightning": "雷", - "lightningLegendCg": "対地 · {minutes} 分以内", - "lightningLegendCc": "雲間 · {minutes} 分以内", - "mapTimelineNow": "現在", - "mapTimelinePast": "過去", - "mapTimelineFuture": "未来", - "mapTimelineObserved": "観測", - "mapTimelineForecast": "予報", - "mapTimelineDataTime": "データ時刻 {time}", - "notifySettingsMenu": "通知設定", - "notifyTitle": "通知", - "notifyUnavailable": "プッシュ通知はまだ準備できていません。しばらくしてから再度お試しください。", - "notifySetFailed": "設定を保存できませんでした。もう一度お試しください。", - "notifySectionEew": "緊急地震速報", - "notifySectionEarthquake": "地震", - "notifySectionWeather": "天気", - "notifySectionTsunami": "津波", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "防災マップのレイヤー", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "南西", + "typhoonForecastLead": "予報 +{hours} 時間", + "dpmDisasterTsunami": "津波", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "正式", + "mapLayerSatelliteTransparentClear": "晴れ = 透明、地図が透ける", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "参照レイヤー", + "mapLayerSatelliteB02": "ひまわり 可視緑(B02)", + "reportListLocalFelt": "局地有感", + "weatherRankingEmpty": "並べ替え可能な観測がありません", "notifySectionOther": "その他", - "notifyEew": "緊急地震速報", - "notifyMonitor": "強震モニタ", - "notifyReport": "地震報告", - "notifyIntensity": "震度速報", - "notifyThunderstorm": "雷雨情報", - "notifyAdvisory": "気象警報・注意報", - "notifyEvacuation": "防災情報", - "notifyTsunami": "津波情報", - "notifyAnnouncement": "お知らせ", - "notifyOptOff": "オフ", - "notifyOptAll": "すべて受信", + "weatherRankingMeta": "データ時刻:{time}\n観測点 {count}", + "onboardingTermsAgree": "サービス利用規約を読み、同意します", + "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(植生なし)", "notifyOptLocalIntensity4": "所在地の震度4以上", - "notifyOptLocalIntensity1": "所在地の震度1以上", - "notifyOptWeatherLocal": "現在地のみ", - "notifyOptTsunamiWarning": "津波警報のみ", - "notifyOptTsunamiAll": "津波情報・津波警報", - "onboardingNext": "次へ", - "onboardingBack": "戻る", + "eewArrived": "到達", + "meshtasticNoDevices": "No Meshtastic devices found", + "mapLayerCategoryLife": "生活", + "reportFilterSortIntensity": "震度", + "typhoonMotion": "進行", + "meshtasticStateDisconnected": "Disconnected", + "typhoonIntensityIntense": "強い台風", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "レイヤーの順番", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "はい", + "meshtasticNoHistory": "Not enough history yet", + "reportDetailLocalIntensityUnavailable": "震度情報なし", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "深さ", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "下にスクロールして続行してください", - "onboardingIntroTitle": "DPIP へようこそ", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "予報", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "地図", + "notifyAdvisory": "気象警報・注意報", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "リセット", + "mapLayerSatelliteMndwi": "ひまわり MNDWI", + "typhoonOverlaySectionStorm": "暴風域", + "moonPhaseFull": "Full moon", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "weatherDynamicStateSubtitle": "ホーム背景の天気を上書きします", + "reportFilterIntensityInfoModernTitle": "新制(2020 年以降)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "資料時刻\n{time}", + "restroomTypeAccessible": "バリアフリートイレ", + "moreSectionAbout": "情報", + "meshtasticSelectDevice": "Select a radio", "onboardingIntroBody": "DPIP はあなたと共にある防災パートナーです。緊急地震速報、地震報告、天気、各種災害情報を統合し、重要な瞬間にすぐお知らせします。\n\n• 地震:緊急地震速報、震度速報、地震報告\n• 天気:雷雨即時情報、気象警報・注意報\n• 津波・防災情報\n\n次に、サービス利用規約をご確認いただき、DPIP がリアルタイムであなたを守れるよう、いくつかの権限の許可をお願いします。", - "onboardingTermsTitle": "サービス利用規約", - "onboardingTermsBody": "DPIP をご利用になる前に、以下の注意事項を必ずお読みください:\n\n• すべての情報は、台湾中央気象署(CWA)が発表する内容を優先してください。\n\n• ネットワーク、サーバー、アプリ、上流のデータソースの状態によっては、情報を受信できない場合があります。可能な限り回避に努めますが、決して発生しないことを保証するものではありません。\n\n• 強い揺れが、通知より先にあなたの所在地へ到達する場合があります。\n\n• 緊急地震速報は高速に計算された結果であり、大きな誤差を含む可能性があります。この点を理解したうえで、慎重にご利用ください。\n\n• 公的機関に認められていない行為には法的リスクが伴う可能性があります。関連する規定を必ずお守りください。\n\nまた、地域に応じた警報を提供するため、本サービスは、どの警報をあなたに送信するかを判断する目的にのみ、あなたのおおよその位置情報とプッシュ識別子を、フォアグラウンドおよびバックグラウンドで収集・アップロードします。\n\n下部の「同意して続行」をタップすることで、上記を読み、理解し、同意したものとみなされます。", - "onboardingTermsAgree": "サービス利用規約を読み、同意します", - "onboardingAgreeContinue": "同意して続行", - "onboardingPermsTitle": "権限の許可", - "onboardingPermsBody": "災害が発生した瞬間に DPIP がお知らせできるよう、以下の権限を許可してください。これらはシステム設定でいつでも変更できます。", + "shelterCapacityLabel": "収容人数", + "reportDetailImage": "地震レポート画像", + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "強風域の平均半径", "onboardingPermNotify": "通知", - "onboardingPermNotifyDesc": "地震、天気、災害の発生時に、警報をすぐお届けします。", - "onboardingPermCritical": "重大な通知", - "onboardingPermCriticalDesc": "生命に関わる緊急地震速報を、消音モードやおやすみモードでも鳴らせるようにします。", - "onboardingPermLocation": "位置情報", - "onboardingPermLocationDesc": "あなたの所在地に合わせて警報を配信します。", - "onboardingPermBackground": "バックグラウンドの位置情報", - "onboardingPermBackgroundDesc": "「常に許可」を選択すると、アプリを閉じていても所在地に合わせて警報を配信できます。", - "onboardingPermBattery": "バッテリー最適化の除外", - "onboardingPermBatteryDesc": "DPIP がバックグラウンドで動作し続けられるようにして、警報の遅延や取りこぼしを防ぎます。", - "onboardingGrant": "許可", - "onboardingGranted": "許可済み", - "onboardingStart": "はじめる", - "language": "言語", - "languageSettings": "言語設定", - "languageSystem": "システムの既定", - "locationBannerServiceOff": "位置情報サービスがオフです。所在地に合わせた警報を配信できません。", - "locationBannerPermission": "位置情報の許可がオフです。所在地に合わせた警報を配信できません。", - "locationBannerFix": "設定を開く", - "notifyBannerDisabled": "通知がオフです — 災害警報を受け取れません。", - "onboardingSkipTitle": "権限が許可されていません", - "onboardingSkipBody": "位置情報と通知を許可しないと、DPIP はお近くの地震や災害をリアルタイムでお知らせできません。設定から後で許可することもできます。", - "onboardingSkipStay": "戻って許可", - "onboardingSkipLeave": "このままスキップ", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "Clear messages", + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "地図の初期レイヤー", + "moreSectionNotify": "通知", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "プッシュ通知はまだ準備できていません。しばらくしてから再度お試しください。", + "mapLayerOrderReset": "既定の順序に戻す", + "dpmAddress": "住所", + "weatherRankingMergeCounty": "県市", + "moreSectionApp": "アプリを入手", + "reportFilterIntensityInfoLegacyBody": "震度は 0–7 のみ。5弱/5強/6弱/6強の区分はありません。", + "mapLayerSatelliteSst": "ひまわり 海面水温", + "qpesumsOverlayMenuTooltip": "定量降水予報レイヤー設定", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "未来", + "typhoonLegendCircleAvg": "平均円", + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "南東", + "radarTownOutlineHint": "より細かい区分", + "eewCountdown": "あと {seconds} 秒", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "最大瞬間風速", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "利用規約", + "restroomTypeGenderNeutral": "ジェンダーニュートラルトイレ", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "雷雨情報", + "skyTimeGolden": "ゴールデンアワー", + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "現在 {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "地域を選ぶと予報を表示します", + "mapLayers": "レイヤー", + "meshtasticHardware": "Hardware", + "languageSettings": "言語設定", + "dpmDisasterNuclear": "原子力事故", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "言語", + "homeForecastFeelsLike": "体感 {temp}°", + "typhoonOverlayWeatherHint": "通報時刻に合わせる", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "夜明け前", + "skyTimeAfternoon": "午後", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "台風警報", "moreSourceCode": "ソースコード", - "moreSectionApp": "アプリを入手", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "表示", - "defaultMapLayerSettings": "地図の初期レイヤー", - "defaultMapLayerSubtitle": "地図タブを開いたときに表示するレイヤーです。下部ナビのアイコンとラベルもこれに合わせます。", - "mapNavRadar": "レーダー", - "mapNavQpesums": "予報", - "mapNavSatellite": "衛星", - "mapNavLightning": "稲妻", - "mapNavTyphoon": "台風", + "mapLayerCategoryWeather": "気象観測", + "mapLayerSatelliteB09": "ひまわり 中層水蒸気(B09)", + "windForecastTownOutlineHint": "より細かいメッシュ", + "mapLayerSatelliteCloudmask": "ひまわり 雲マスク", + "mapAppCopyCoordinates": "座標をコピー", + "reportFilterIntensityInfoIntro": "気象署は 2020 年 1 月 1 日(台北時間)から新制震度を採用しています。", "mapNavEarthquake": "地震", - "mapNavTemperature": "気温", - "mapNavHumidity": "湿度", - "mapNavPressure": "気圧", - "mapNavWind": "風向", + "typhoonGust": "最大瞬間風速", + "restroomGradeAverage": "普通", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "ひまわり 巻雲/雲頂高度", + "onboardingPermBackgroundDesc": "「常に許可」を選択すると、アプリを閉じていても所在地に合わせて警報を配信できます。", + "mapTimelineForecast": "予報", + "restroomTypeLabel": "種別", + "navEarthquake": "地震", + "typhoonOverlayStormL10Tooltip": "暴風域 + 平均円(黄)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "地震レポート", + "moreTremReport": "TREM 検知レポート", + "weatherDataTime": "{station} · データ時刻 {time}", + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "radarCountyOutline": "県市境界", + "onboardingGranted": "許可済み", + "@mapAppCopyCoordinates": {}, + "commonClose": "閉じる", + "restroomGradeLabel": "等級", + "rainIntervalNow": "今日", + "changelogCurrentVersion": "現行", + "typhoonOverlayForecastCalloutsTooltip": "拡大時に予報点の詳細カードを表示", + "typhoonLabelPressure": "中心気圧", + "aedOpenRemark": "開館時間メモ", + "onboardingPermsBody": "災害が発生した瞬間に DPIP がお知らせできるよう、以下の権限を許可してください。これらはシステム設定でいつでも変更できます。", + "typhoonOverlaySectionWeather": "天気下敷き", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "現在地のみ", "mapNavRain": "雨量", - "mapNavDisaster": "防災", - "displayTheme": "テーマ", + "moonDays": "days", + "mapLegendUnit": "単位:{unit}", + "weatherModeClear": "晴れ", + "meshtasticRadio": "Radio", + "commonEmpty": "表示する項目がありません", + "mapLayerSatelliteB01": "ひまわり 可視青(B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "昇順", + "reportFilterApply": "適用", + "reportDetailImageUnavailable": "レポート画像はまだありません", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "最高", + "reportDetailReplay": "リプレイ", + "mapLayerRestroom": "トイレ", + "restroomCategoryWelfare": "社会福祉施設・集会所", + "restroomGradeExcellent": "最上級", + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "数値予報", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", "themeSystem": "システム", - "themeLight": "ライト", - "themeDark": "ダーク", - "moreSectionAbout": "情報", - "termsOfService": "利用規約", - "faq": "よくある質問", - "openSourceLicenses": "オープンソースライセンス", - "sponsorTitle": "DPIP を支援", - "sponsorIntro": "DPIP はリアルタイムの防災情報の提供に取り組んでおり、広告やその他の収益モデルはありません。皆さまのご支援はサーバーの運用と継続的な開発に役立ちます。", - "sponsorSubscriptions": "サブスクリプション", - "sponsorRecommended": "おすすめ", - "sponsorOneTime": "一回限りの支援", - "sponsorPerMonth": "{price} / 月", - "sponsorRestore": "購入を復元", - "sponsorTerms": "利用規約", - "sponsorPrivacy": "プライバシーポリシー", - "sponsorRestoring": "購入を復元しています…", - "sponsorRestoreUnavailable": "ストアに接続できません。しばらくしてからもう一度お試しください。", - "commonClose": "閉じる", + "mapLayerSatelliteNdvi": "ひまわり NDVI", + "typhoonLegendForecast": "予報経路", + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "降水量", + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "地図上のマーカーをタップして詳細を表示", + "onboardingSkipLeave": "このままスキップ", + "onboardingBack": "戻る", + "aedPlaceDesc": "設置場所", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "権限が許可されていません", + "restroomTypeFamily": "親子トイレ", + "typhoonValueKm": "{n} km", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "気圧", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "バッテリー最適化の除外", + "typhoonLabelNw": "北西", + "dpmDisasterFlood": "洪水", + "moonPhaseWaxingCrescent": "Waxing crescent", + "restroomCategoryLeisure": "レジャー・娯楽施設", "mapLayerTemperature": "気温", - "trendRange24h": "24時間", - "trendRange7d": "7日間", - "trendNoData": "トレンドデータがありません", - "trendCumulativeTotal": "累計 {total} mm", - "chartHourLabel": "{hour}時", - "mapLayerHumidity": "湿度", - "mapLayerPressure": "気圧", + "aedCategory": "分類", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "Channels", + "monitorWaiting": "データ待機中…", + "typhoonOverlayForecastCallouts": "予報点の情報", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "震央座標", + "meshtasticVoltage": "Voltage", + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "風向", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "降水量", - "rainIntervalMenu": "累積期間", - "rainIntervalNow": "今日", - "rainInterval10m": "10分", - "rainInterval1h": "1時間", - "rainInterval3h": "3時間", - "rainInterval6h": "6時間", + "reportDetailMagnitude": "地震規模", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "地域別震度", "rainInterval12h": "12時間", - "rainInterval24h": "24時間", - "rainInterval2d": "2日", - "rainInterval3d": "3日", - "mapLayerTyphoon": "台風", - "typhoonNoActive": "発生中の台風なし", - "typhoonWind": "風速", - "typhoonGust": "最大瞬間風速", - "typhoonPressure": "気圧", - "typhoonMotion": "進行", - "mapLayerMonitor": "強震モニタ", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "防災マップ", - "disasterMapOverlayMenuTooltip": "防災マップのレイヤー", - "disasterMapOverlaySectionLayers": "レイヤー", - "disasterMapOverlayAedTooltip": "AEDの位置を表示", - "aedAddress": "住所", - "aedRegion": "地域", - "aedCategory": "分類", - "aedType": "種類", - "aedPlaceDesc": "設置場所", - "aedDescription": "備考", - "aedHoursWeekday": "平日の開館時間", - "aedHoursSaturday": "土曜の開館時間", - "aedHoursSunday": "日曜の開館時間", - "aedOpenRemark": "開館時間メモ", - "aedEmergencyPhone": "緊急連絡先", - "mapLayerRestroom": "トイレ", - "mapLayerShelter": "避難所", - "disasterMapOverlayRestroomTooltip": "トイレを表示", - "disasterMapOverlayShelterTooltip": "避難所を表示", - "dpmOpenInMaps": "地図アプリで開く", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "土石流", + "notifyMonitor": "強震モニタ", + "onboardingStart": "はじめる", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / 月", + "mapLayerPressure": "気圧", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "ひまわり 近赤外(B04)", + "mapLayerSatelliteTransparentZero": "差ゼロ = 透明(信号なし)", + "shelterIndoorLabel": "屋内収容", + "notifyOptOff": "オフ", + "reportFilterSortTime": "時間", + "mapLayerSatelliteCloudProbablyClear": "おそらく晴れ", + "weatherModeThunderstorm": "雷雨", + "homeViewOnMap": "地図で見る", + "reportFilterIntensityInfoLegacyTitle": "旧制(2020 年より前)", + "typhoonLabelSpeed": "これまでの移動速度", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "{app} を開けませんでした", + "mapLayerSatelliteRgbComposite": "RGB 合成(JMA レシピ)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "今日の最低", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "ひまわり 下層水蒸気(B10)", + "mapLayerSatelliteCloudProbablyCloudy": "おそらく雲", + "shelterCategoryLabel": "対象災害", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(水域なし)", + "meshtasticStateConnecting": "Connecting…", + "moonTitle": "Moon", + "weatherRankingGust": "突風", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "避難所の災害種別", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "サーバー状態", + "notifySectionWeather": "天気", + "meshtasticPreset": "Modem preset", + "dataSectionSeismic": "地震", + "changelogBodyEmpty": "このリリースの説明はありません。", + "radarGlobalOutline": "国境線", + "notifyEew": "緊急地震速報", + "regionNationwide": "全国", + "moreNotifyLog": "DPIP 通知送信履歴", + "regionCurrent": "現在地", + "dpmFilterSectionRestroom": "施設の種類", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "雪", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "デバッグ情報", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "ひまわり 長波長赤外線(B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "稲妻", + "homeForecastEmpty": "予報データがありません", + "sponsorOneTime": "一回限りの支援", + "mapLayerSatelliteBtdSplit": "ひまわり スプリットウィンドウ", + "onboardingPermBackground": "バックグラウンドの位置情報", + "aedEmergencyPhone": "緊急連絡先", + "dpmOpenInMaps": "地図アプリで開く", + "meshtasticNotifyNodes": "Notify on new nodes", + "onboardingPermCriticalDesc": "生命に関わる緊急地震速報を、消音モードやおやすみモードでも鳴らせるようにします。", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "晴れ(暖域) = 透明、地図が透ける", + "meshtasticSent": "Sent", + "homeForecastTitle": "24時間予報", + "typhoonLegendWarningAreas": "警報区域", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "所在地の震度1以上", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "過去", + "restroomTypeFemale": "女性用トイレ", + "reportListToday": "今日", + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "読み込み中…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "並の台風", + "typhoonWind": "風速", + "mapLayerSatelliteAsh": "ひまわり 火山灰", + "rainInterval3h": "3時間", + "reportListSearch": "検索", + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "衛星", + "reportFilterLocation": "場所", + "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", + "typhoonIntensityTd": "熱帯低気圧", + "reportFilterDate": "日付", + "sponsorRestoreUnavailable": "ストアに接続できません。しばらくしてからもう一度お試しください。", + "homeForecastPop": "{pop}%", + "regionEmpty": "登録地域がありません", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "DPIP がバックグラウンドで動作し続けられるようにして、警報の遅延や取りこぼしを防ぎます。", + "mapNavDisaster": "防災", + "radarScanRangeSubtitle": "4基のレーダーが実際に観測する範囲を示します。", + "aedHoursSunday": "日曜の開館時間", + "reportDetailOriginTime": "発震時刻", + "trendNoData": "トレンドデータがありません", + "onboardingPermLocation": "位置情報", + "moreDiscord": "Discord コミュニティ", + "mapNavPressure": "気圧", + "mapLayerSatelliteB13": "ひまわり 赤外線(B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "リリースノートはまだありません", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "開始日:当日 00:00(台北時間)", + "eewTitle": "緊急地震速報", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "ja", + "regionSelectCount": "{count}/{max} 件選択中", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "ひまわり 二酸化硫黄/雲相", + "meshtasticStateError": "Error", + "weatherModeOvercast": "本曇り", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "震源の深さ", + "typhoonOverlayWarningTooltip": "台風警報対象の県を強調", + "reportFilterDatePick": "日付を選択", + "onboardingSkipStay": "戻って許可", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "データを取得できませんでした。しばらくしてから再度お試しください。", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "屋外収容", + "meshtasticStateConnected": "Connected", + "mapNavRadar": "レーダー", + "mapLayerSatelliteCloudClear": "晴れ", + "eewSummary": "M{magnitude}・深さ {depth} km", + "locationBannerPermission": "位置情報の許可がオフです。所在地に合わせた警報を配信できません。", + "typhoonOverlayWeatherNoneTooltip": "レーダー/赤外線なし", + "radarCountyOutlineHint": "エコーの上に描画", + "windForecastCountyOutlineHint": "風場の上に描画", + "homeRainTrendTitle": "今後1時間の雨", + "moonPhaseFirstQuarter": "First quarter", + "mapLayerCategoryTyphoon": "台風", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "Airtime (24h)", + "restroomTypeMixed": "男女共用トイレ", + "restroomGradeGood": "優良", + "notifyTsunami": "津波情報", + "navData": "データ", + "mapLayerSatelliteBtdWvirw": "ひまわり オーバーシューティングトップ", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "この端末では通話できません", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "指定なし", + "weatherRankingMergeTo": "統合", + "notifyIntensity": "震度速報", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "累積期間", + "reportDetailLocalFelt": "局地的な有感地震", + "meshtasticDevice": "Device", + "onboardingGrant": "許可", + "weatherModeRain": "雨", + "shelterVulnerableOkLabel": "要配慮者向け収容", + "stationSheetEmpty": "観測点をタップして値を表示", + "typhoonLegendProbability": "接近確率", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "マグニチュード", + "skyTimeMorning": "午前", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "実験的機能", + "onboardingTermsBody": "DPIP をご利用になる前に、以下の注意事項を必ずお読みください:\n\n• すべての情報は、台湾中央気象署(CWA)が発表する内容を優先してください。\n\n• ネットワーク、サーバー、アプリ、上流のデータソースの状態によっては、情報を受信できない場合があります。可能な限り回避に努めますが、決して発生しないことを保証するものではありません。\n\n• 強い揺れが、通知より先にあなたの所在地へ到達する場合があります。\n\n• 緊急地震速報は高速に計算された結果であり、大きな誤差を含む可能性があります。この点を理解したうえで、慎重にご利用ください。\n\n• 公的機関に認められていない行為には法的リスクが伴う可能性があります。関連する規定を必ずお守りください。\n\nまた、地域に応じた警報を提供するため、本サービスは、どの警報をあなたに送信するかを判断する目的にのみ、あなたのおおよその位置情報とプッシュ識別子を、フォアグラウンドおよびバックグラウンドで収集・アップロードします。\n\n下部の「同意して続行」をタップすることで、上記を読み、理解し、同意したものとみなされます。", + "reportFilterTitle": "絞り込み", + "onboardingPermCritical": "重大な通知", + "trendCumulativeTotal": "累計 {total} mm", + "languageName": "日本語", + "reportListEmptyFiltered": "条件に一致する地震報告はありません", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "台風", + "weatherModeSand": "砂じん", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "衛星", + "@dpmOpenInMaps": {}, + "notifyReport": "地震報告", + "mapAppCoordinatesCopied": "座標をコピーしました", + "skyTimeNight": "夜", + "sponsorRecommended": "おすすめ", + "mapLayerSatelliteB15": "ひまわり 長波長赤外線(B15)", + "weatherRankingWind": "風速", + "feedStale": "データが最新でない可能性があります", + "homeForecastWind": "{direction} · 風力{level}", + "navHome": "ホーム", + "meshtasticRegionLabel": "Region", + "mapLayerSatelliteCloudtop": "ひまわり 雲頂温度", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "オープンソースライセンス", + "weatherRankingLowest": "最低", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "深さ", + "mapTimelineDataTime": "データ時刻 {time}", + "radarScanRange": "走査範囲を表示", + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "較差 {value}°C", + "weatherRankingExtremeHigh": "今日の最高", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "リリース詳細", + "sponsorPrivacy": "プライバシーポリシー", + "reportDetailLocalIntensity": "現在地の震度", + "mapLayerSatelliteNaturalcolor": "ひまわり ナチュラルカラー", + "meshtasticAirtime": "Air time (TX)", + "shelterCapacityValue": "{n} 人", + "lightningLegendCc": "雲間 · {minutes} 分以内", + "meshtasticSendHint": "Message to broadcast", + "monitorDelay": "遅延 {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "いいえ", + "mapLayerSatelliteB08": "ひまわり 上層水蒸気(B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "レーダーエコーの下でも市町村境界が見えるようにします。", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "通報時刻に最も近い赤外線", + "radarScanRangeHint": "枠外の空白は未観測", + "typhoonPickerTd": "熱帯低気圧 TD {no}", + "mapLayerSatelliteWatervapor": "ひまわり 水蒸気", + "regionAddButton": "地域を追加", + "displaySettings": "表示", + "restroomGradePoor": "不合格", + "restroomCategoryTourist": "観光地・景勝地", + "locationBannerServiceOff": "位置情報サービスがオフです。所在地に合わせた警報を配信できません。", + "mapLayerStyleTooltip": "色調", + "lightningLegendCg": "対地 · {minutes} 分以内", + "skyTimeAuto": "自動", + "appLogs": "アプリログ", + "feedConnecting": "接続中…", + "notifyBannerDisabled": "通知がオフです — 災害警報を受け取れません。", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "湿度", + "typhoonValueMs": "毎秒 {n} m", + "homeForecastHumidity": "湿度 {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "交通", + "reportFilterLocationHint": "例:花蓮、海域", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "距離", + "meshtasticSnrTrend": "信号トレンド (SNR)", + "meshtasticBatteryTrend": "バッテリー推移", + "typhoonOverlayMenuTooltip": "台風オーバーレイ設定", + "mapLayerSatelliteBtdOzone": "ひまわり 対流圏界面", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "地震", + "mapLayerDisasterMap": "防災マップ", + "weatherModeFog": "霧", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "気象庁の赤外画像の慣例:温度が低いほど白", + "moreAnnouncements": "お知らせ", + "mapLayerSatelliteTransparentNoData": "データなし(陸上) = 透明", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "行政サービス施設", + "typhoonLegendCurrent": "現在中心", + "aedAddress": "住所", + "mapLayerAed": "AED", + "changelogTypePrerelease": "ベータ", + "reportFilterIntensityInfoModernBody": "震度は 0–4、5弱、5強、6弱、6強、7。フィルタは新制に準拠し、それ以前の地震はリストで旧制表記になります。", + "typhoonOverlayWeatherNone": "なし", + "mapLayerStyleGray": "グレースケール(JMA)", + "weatherModeAuto": "自動", + "typhoonLabelProbCircle": "70%確率円", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "すべて受信", + "displayTheme": "テーマ", + "mapLayerSatelliteB07": "ひまわり 短波長赤外(B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "これまでの進行方向", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "登録地域", + "typhoonLegendCone": "予報円", + "moreCwaEew": "中央気象署 緊急地震速報", + "onboardingPermsTitle": "権限の許可", + "mapLayerStyleJma": "雲頂強調(JMA)", + "rainInterval10m": "10分", + "weatherRankingAnalysisLow": "最低 {value}", + "meshtasticConnectAnyway": "Connect anyway", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "ひまわり 近赤外(B06)", + "mapLayerSatelliteTransparentReflectance": "低反射率・夜間 = 透明、地図が透ける", + "chartHourLabel": "{hour}時", + "mapLayerShelter": "避難所", + "typhoonOverlayProbabilityTooltip": "接近確率を表示(予報円を隠す)", + "mapLayerSatelliteNdwi": "ひまわり NDWI", + "disasterMapOverlayShelterTooltip": "避難所を表示", + "mapNavHumidity": "湿度", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "震度順に並べ替え", + "homeRainTrendNoData": "データなし", + "mapLayerCategoryRadar": "レーダー", + "meshtasticShortName": "Short name", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "ひまわり エアマス", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "経路詳細", + "dataSectionWeather": "気象", + "aedHoursWeekday": "平日の開館時間", + "homeActiveEventsTitle": "発生中の事象", + "weatherRankingAnalysisHigh": "最高 {value}", + "faq": "よくある質問", + "typhoonHistoryLive": "最新", + "eewSerial": "第 {serial} 報", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "並び替え", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "地震報告", + "typhoonNoActive": "発生中の台風なし", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "ひまわり 二酸化硫黄/雲相(B11)", + "navEvents": "イベント", + "onboardingTermsTitle": "サービス利用規約", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "郷鎮名", + "notifySetFailed": "設定を保存できませんでした。もう一度お試しください。", + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "notifyAnnouncement": "お知らせ", + "onboardingIntroTitle": "DPIP へようこそ", + "regionCurrentUnavailable": "現在地を取得できません", + "languageSystem": "システムの既定", + "skyTimeSunset": "日の入り", + "mapLayerSatelliteDust": "ひまわり 黄砂", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "変更", + "weatherDynamicState": "天気アニメーション", + "mapPlaceholderDisabled": "地図(一時的に無効)", + "moonNow": "現在", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "見え方", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "月の出・月の入り", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "次の月相", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "月齢カレンダー", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "距離", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "km", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "視直径", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "月の出", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "月の入り", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "次の新月", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "終日地平線上", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "この日はなし", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "太陽", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "日の出・薄明・二十四節気", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "日照", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "薄明", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "光", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "日時計", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "二十四節気", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "日の出", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "日の入り", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "南中", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "昼の長さ", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "市民", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "航海", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "天文", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "朝のゴールデンアワー", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "夕のゴールデンアワー", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "ブルーアワー", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "均時差", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "分", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "次の節気", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "惑星", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "今夜の位置と明るさ", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "現在", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "地平線上", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "地平線下", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "太陽に近い", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "等級", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "離角", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "時間帯", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "宵の明星", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "明けの明星", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "距離", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "高度", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "水星", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "金星", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "火星", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "木星", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "土星", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "天王星", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "海王星", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "春分", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "清明", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "穀雨", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "立夏", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "小満", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "芒種", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "夏至", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "小暑", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "大暑", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "立秋", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "処暑", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "白露", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "秋分", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "寒露", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "霜降", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "立冬", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "小雪", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "大雪", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "冬至", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "小寒", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "大寒", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "立春", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "雨水", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "啓蟄", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "今夜", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "何が見えるか、いつ見えるか", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "観測ウィンドウ", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "天文薄明終了", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "完全に暗くならない", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "暗夜の時間帯", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "月が一晩中出ている", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "暗夜合計", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "月明かり", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "流星群", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "放射点が昇らない", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "個/時", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "衛星の通過", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "今見られる天体", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "しぶんぎ座", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "こと座", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "みずがめ座η", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "みずがめ座δ", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "ペルセウス座", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "オリオン座", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "おうし座南", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "しし座", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "ふたご座", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "こぐま座", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "散開星団", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "球状星団", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "渦巻銀河", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "楕円銀河", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "不規則銀河", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "惑星状星雲", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "超新星残骸", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "散光星雲", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "反射星雲", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "アステリズム", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "暦", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "旧暦と今後の日食・月食", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "今日", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "西暦", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "旧暦", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "歳次", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app}(デフォルト)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "月の大小", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "座標をコピー", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "30日", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "座標をコピーしました", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "29日", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "{app} を開けませんでした", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "閏", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "この端末では通話できません", - - "mapOverlaySectionReference": "参照レイヤー", - "mapLayerCategoryEarthquake": "地震", - "mapLayerCategoryTyphoon": "台風", - "mapLayerCategoryWeather": "気象観測", - "mapLayerCategorySatellite": "衛星", - "mapLayerCategoryRadar": "レーダー", - "mapLayerCategoryLife": "生活", - "mapLayerCategoryForecast": "数値予報", "mapOverlaySectionMap": "地図", - "rainIntervalSection": "集計時間", - - "mapTownLabels": "郷鎮名", - "mapTownLabelsHint": "拡大すると郷鎮名を表示", - - "mapTerrainRelief": "地形の立体感", - "mapTerrainReliefHint": "ベースマップに地形の陰影を表示", - - "dpmSheetEmpty": "地図上のマーカーをタップして詳細を表示", - "dpmAddress": "住所", - "restroomTypeLabel": "種別", - "restroomCategoryLabel": "区分", - "restroomGradeLabel": "等級", - "restroomTypeFemale": "女性用トイレ", - "restroomTypeMale": "男性用トイレ", - "restroomTypeMixed": "男女共用トイレ", - "restroomTypeAccessible": "バリアフリートイレ", - "restroomTypeGenderNeutral": "ジェンダーニュートラルトイレ", - "restroomTypeFamily": "親子トイレ", - "restroomTypeUnspecified": "未設定", - "restroomCategoryTransport": "交通", - "restroomCategoryPark": "公園", - "restroomCategoryCommercial": "商業・営業施設", - "restroomCategoryReligious": "宗教・礼拝施設", - "restroomCategoryCultural": "文化・娯楽施設", - "restroomCategoryGovernment": "行政サービス施設", - "restroomCategoryWelfare": "社会福祉施設・集会所", - "restroomCategoryTourist": "観光地・景勝地", - "restroomCategoryLeisure": "レジャー・娯楽施設", - "restroomCategoryOther": "その他", - "restroomGradeExcellent": "最上級", - "restroomGradeGood": "優良", - "restroomGradeAverage": "普通", - "restroomGradePoor": "不合格", - "shelterAddressLabel": "住所", - "shelterCapacityLabel": "収容人数", - "shelterCapacityValue": "{n} 人", - "shelterCategoryLabel": "対象災害", - "shelterIndoorLabel": "屋内収容", - "shelterOutdoorLabel": "屋外収容", - "shelterVulnerableOkLabel": "要配慮者向け収容", - "dpmYes": "はい", - "dpmNo": "いいえ", - "stationSheetEmpty": "観測点をタップして値を表示", - "monitorDelay": "遅延 {value} s", - "monitorWaiting": "データ待機中…", - "mapLegendUnit": "単位:{unit}", - "typhoonLegendPast": "実況経路", - "typhoonLegendForecast": "予報経路", - "typhoonLegendForecastPoint": "予報点", - "typhoonLegendCurrent": "現在中心", - "typhoonLegendCone": "予報円", - "mapLegendExpand": "凡例", - "mapLegendCollapse": "凡例を閉じる", - "mapMyLocation": "現在地", - "mapResetNorth": "北を上にする", - "typhoonLegendCircle15": "強風域(30kt)", - "typhoonLegendCircle25": "暴風域(50kt)", - "typhoonLegendProbability": "接近確率", - "typhoonLegendWarningAreas": "警報区域", - "typhoonWarningTitle": "台風警報", - "typhoonWarningAreas": "対象地域:{areas}", - "typhoonTrackDetail": "経路詳細", - "typhoonHistoryTitle": "資料時刻", - "typhoonHistoryLive": "最新", - "typhoonSatelliteTitle": "衛星", - "typhoonDataTime": "資料時刻\n{time}", - "typhoonForecastLead": "予報 +{hours} 時間", - "typhoonIntensityIntense": "強い台風", - "typhoonIntensityMild": "弱い台風", - "typhoonIntensityModerate": "並の台風", - "typhoonIntensityTd": "熱帯低気圧", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "熱帯低気圧 TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "これまでの進行方向", - "typhoonLabelGaleAvg": "強風域の平均半径", - "typhoonLabelGust": "最大瞬間風速", - "typhoonLabelNe": "北東", - "typhoonLabelNw": "北西", - "typhoonLabelPosition": "中心位置", - "typhoonLabelPressure": "中心気圧", - "typhoonLabelProbCircle": "70%確率円", - "typhoonLabelSe": "南東", - "typhoonLabelSpeed": "これまでの移動速度", - "typhoonLabelStormAvg": "暴風域の平均半径", - "typhoonLabelSw": "南西", - "typhoonLabelWind": "中心付近の最大風速", - "typhoonLegendCircleAvg": "平均円", - "typhoonOverlayMenuTooltip": "台風オーバーレイ設定", - "typhoonOverlayProbabilityHint": "予報円を隠します", - "typhoonOverlayProbabilityTooltip": "接近確率を表示(予報円を隠す)", - "typhoonOverlaySectionExtra": "オーバーレイ", - "typhoonOverlaySectionStorm": "暴風域", - "typhoonOverlaySectionWeather": "天気下敷き", - "typhoonOverlayStormBandSubtitle": "平均円付き", - "typhoonOverlayStormL10Tooltip": "暴風域 + 平均円(黄)", - "typhoonOverlayStormL7Tooltip": "強風域 + 平均円(紫)", - "typhoonOverlayWarningTooltip": "台風警報対象の県を強調", - "typhoonOverlayWeatherHint": "通報時刻に合わせる", - "typhoonOverlayWeatherNone": "なし", - "typhoonOverlayWeatherNoneTooltip": "レーダー/赤外線なし", - "typhoonOverlayWeatherRadarTooltip": "通報時刻に最も近いレーダー", - "typhoonOverlayWeatherSatelliteTooltip": "通報時刻に最も近い赤外線", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} hPa", - "typhoonValueKm": "{n} km", - "typhoonValueLat": "北緯 {lat} 度", - "typhoonValueLon": "東経 {lon} 度", - "typhoonValueMs": "毎秒 {n} m", - "typhoonOverlayForecastCallouts": "予報点の情報", - "typhoonOverlayForecastCalloutsTooltip": "拡大時に予報点の詳細カードを表示", - "dpmFilterSectionRestroom": "施設の種類", - "dpmFilterSectionRestroomType": "トイレの種類", - "dpmFilterSectionShelter": "避難所の災害種別", - "dpmDisasterFlood": "洪水", - "dpmDisasterEarthquake": "震災", - "dpmDisasterLandslide": "土石流", - "dpmDisasterTsunami": "津波", - "dpmDisasterSlope": "斜面災害", - "dpmDisasterNuclear": "原子力事故", - "skyTime": "空の時刻", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "月食", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "自動", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "日食", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "夜明け前", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "範囲内になし", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "日の出", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "皆既", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "午前", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "部分", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "正午", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "金環", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "午後", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "半影", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "ゴールデンアワー", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "子", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "日の入り", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "丑", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "薄暮", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "寅", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "夜", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "卯", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "曇り", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "辰", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "本曇り", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "巳", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "雪", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "午", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "砂じん", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "未", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "走査範囲を表示", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "申", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "4基のレーダーが実際に観測する範囲を示します。", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "酉", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "枠外の空白は未観測", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "戌", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "レーダーレイヤー設定", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "亥", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "県市境界", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "潮汐", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "国境線", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "大潮・小潮と月の引力", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "各国の国境外枠", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "天文起潮力のみで、港湾の潮汐表ではありません。潮位は気象庁の公表値をご覧ください。", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "エコーの上に描画", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "現在", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "レーダーエコーの下でも県市境界が見えるようにします。", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "周期", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "市町村境界", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "大潮", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "より細かい区分", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "小潮", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "レーダーエコーの下でも市町村境界が見えるようにします。", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "中潮", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "定量降水予報レイヤー設定", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "月の引力", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "風予報レイヤー設定", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "平衡潮位", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "風場の上に描画", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "m", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "各国の国境外枠", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "次の近地点大潮", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "より細かいメッシュ", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "転換点", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "第 {serial} 報", - "eewMaxIntensity": "最大震度", - "eewLocalIntensity": "現在地の推定", - "eewSWave": "S波", - "eewArrived": "到達", - "eewCountdown": "あと {seconds} 秒" + "tideHigh": "高", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "低", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "星図", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "頭上の肉眼で見える空", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "北", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "東", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "南", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "西", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "軌道要素 {days} 日前", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}{month} 月 {day} 日", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "流星群なし", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "48 時間以内に可視通過なし", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "軌道データを読み込めません", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "十分な高度の天体なし", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "星表を読み込めません", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index e15428fa2..40e4d2541 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1,679 +1,1739 @@ { - "@@locale": "ko", - "languageName": "한국어", - "navHome": "홈", - "navEvents": "이벤트", - "navMap": "지도", - "navData": "자료", - "navEarthquake": "지진", - "dataSectionSeismic": "지진", - "dataEarthquakeSubtitle": "지진 보고서", - "dataSectionWeather": "기상", - "dataWeatherRankingSubtitle": "실시간 관측 순위", - "weatherRankingTitle": "관측 순위", - "weatherRankingMeta": "자료 시각: {time}\n관측점 {count}", - "weatherRankingEmpty": "정렬할 관측이 없습니다", - "weatherRankingBy": "정렬", - "weatherRankingHighest": "최고", - "weatherRankingLowest": "최저", - "weatherRankingMergeTo": "병합", - "weatherRankingMergeTown": "향진", - "weatherRankingMergeCounty": "현시", - "weatherRankingWind": "풍속", - "weatherRankingGust": "돌풍", + "typhoonValueLat": "{lat}°N", + "onboardingSkipBody": "위치 및 알림 권한이 없으면 DPIP가 주변의 지진과 재난을 실시간으로 알려드릴 수 없습니다. 나중에 설정에서 권한을 허용할 수 있습니다.", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24시간", + "homeRainTrendHeavyStopping": "{minutes}분 후에 강한 비가 그칠 것으로 예상돼요", + "mapTimelineObserved": "관측", + "regionSelectTitle": "지역 선택", + "skyTimeNoon": "정오", + "radarCountyOutlineSubtitle": "레이더 에코 아래에서도 경계가 보이도록 합니다.", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "화장실 유형", + "mapLayerSatelliteB03": "히마와리 가시 적색(B03)", + "reportFilterIntensity": "진도", + "mapLayerLightning": "번개", + "restroomTypeMale": "남자 화장실", + "meshtasticLastReceived": "Last received", + "reportDetailSortByCounty": "지역순 정렬", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "약한 비가 올 수 있어요", + "meshtasticUptime": "Uptime", "weatherRankingTempExtremes": "기온 극값", - "weatherRankingExtremeHigh": "오늘 최고", - "weatherRankingExtremeLow": "오늘 최저", + "themeLight": "라이트", + "mapTerrainReliefHint": "기본 지도에 지형 음영 표시", + "meshtasticEmptyMessage": "(empty message)", + "moreSectionRegion": "지역", + "dpmDisasterEarthquake": "지진", + "mapLayerSatellite": "히마와리 적외(B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "토요일 운영시간", + "dpmDisasterSlope": "사면 재해", + "moonPhaseNew": "New moon", + "notifySectionEew": "지진 조기경보", + "mapResetNorth": "북쪽으로 되돌리기", + "rainInterval2d": "2일", + "mapTownLabelsHint": "확대하면 읍면동 이름 표시", + "commonCancel": "Cancel", + "notifyOptTsunamiWarning": "지진해일 경보만", + "mapLayerSatelliteBtdFog": "히마와리 야간 안개", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "고급", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "일교차", + "notifySettingsMenu": "알림 설정", + "typhoonHistoryTitle": "자료 시각", + "mapAppDefault": "{app} (기본)", + "trendRange24h": "24시간", + "mapLayerStyleJmaTooltip": "그레이스케일 바탕에 −40 °C 이하를 채색, 운정 고도 강조", "weatherRankingRecordedAt": "기록 시각 {time}", - "weatherRankingAnalysisCurrent": "현재 {value}°C", - "weatherRankingAnalysisHigh": "최고 {value}", - "weatherRankingAnalysisLow": "최저 {value}", - "weatherRankingAnalysisRange": "일교차 {value}°C", - "reportListEmpty": "지진 보고서가 없습니다", - "reportListEmptyFiltered": "조건에 맞는 지진 보고서가 없습니다", - "reportListMeta": "M{magnitude} · {depth} km", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "km", - "reportListLocalFelt": "소규모 유감", - "reportListToday": "오늘", - "reportListYesterday": "어제", - "reportListDayCount": "{count}", - "reportListEnd": "마지막입니다", - "reportFilterTitle": "필터", - "reportFilterSort": "정렬", - "reportFilterSortTime": "시간", - "reportFilterSortIntensity": "진도", - "reportFilterSortMagnitude": "규모", - "reportFilterSortDepth": "깊이", + "mapLayerRain": "강수량", + "mapLayerQpesums": "1시간 강수 예보", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "지도", + "mapTerrainRelief": "지형 입체감", + "eewMaxIntensity": "최대 진도", + "mapLegendCollapse": "범례 숨기기", + "changelogTitle": "변경 로그", "reportFilterOrderDesc": "내림차순", - "reportFilterOrderAsc": "오름차순", - "reportFilterIntensity": "진도", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", "reportFilterIntensityInfoTitle": "진도 신제·구제", - "reportFilterIntensityInfoIntro": "기상서는 2020년 1월 1일(타이베이 시간)부터 신제 진도를 사용합니다.", - "reportFilterIntensityInfoLegacyTitle": "구제(2020년 이전)", - "reportFilterIntensityInfoLegacyBody": "진도는 0–7만 있으며 5약/5강/6약/6강 구분이 없습니다.", - "reportFilterIntensityInfoModernTitle": "신제(2020년 이후)", - "reportFilterIntensityInfoModernBody": "진도는 0–4, 5약, 5강, 6약, 6강, 7입니다. 필터는 신제를 따르며, 이전 지진은 목록에서 구제 표기로 표시됩니다.", - "reportFilterMagnitude": "규모", - "reportFilterDepth": "깊이", - "reportFilterDepthKm": "{depth} km", - "reportFilterDate": "날짜", - "reportFilterDatePick": "날짜 선택", - "reportFilterDateStartNote": "시작일: 당일 00:00(타이베이)", + "mapLayerTyphoon": "태풍", + "radarOverlayMenuTooltip": "레이더 레이어 옵션", + "mapMyLocation": "내 위치", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "Nodes", + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", + "aedType": "유형", + "termsOfService": "서비스 약관", + "typhoonLegendCircle25": "폭풍권 (10급)", + "sponsorTitle": "DPIP 후원하기", + "mapNavSatellite": "위성", + "homeRainTrendUpdated": "업데이트 {time}", + "onboardingNext": "다음", + "weatherRankingMergeTown": "향진", + "mapLayerMonitor": "실시간 지진 모니터", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "구독", + "typhoonValueLon": "{lon}°E", + "skyTime": "하늘 시각", + "weatherModeCloudy": "구름 많음", + "skyTimeDusk": "땅거미", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "종료일: 당일 24:00(타이베이)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "위치", - "reportFilterLocationHint": "예: 화롄, 해역", - "reportFilterAny": "전체", - "reportFilterApply": "적용", - "reportFilterReset": "초기화", - "reportListSearch": "조회", - "reportDetailTitle": "지진 보고서", - "reportDetailNumbered": "번호 {number} 유의미 유감지진", - "reportDetailLocalFelt": "국지적 유감지진", - "reportDetailInfo": "상세 정보", - "reportDetailOriginTime": "발생 시각", - "reportDetailEpicenter": "진앙 좌표", - "reportDetailMagnitude": "지진 규모", - "reportDetailDepth": "진원 깊이", - "reportDetailAreaIntensity": "지역별 진도", - "reportDetailLocalIntensity": "내 위치의 진도", - "reportDetailLocalIntensityUnavailable": "진도 정보 없음", - "reportDetailSortByIntensity": "진도순 정렬", - "reportDetailSortByCounty": "지역순 정렬", - "reportDetailImage": "지진 보고서 이미지", - "reportDetailImageUnavailable": "보고서 이미지가 아직 없습니다", - "reportDetailOpenReport": "보고서 페이지", - "reportDetailReplay": "다시 보기", - "navMore": "더보기", - "appLogs": "앱 로그", - "changelogTitle": "변경 로그", - "changelogEmpty": "아직 릴리스 노트가 없습니다", - "changelogTypePrerelease": "베타", - "changelogTypeStable": "정식", - "changelogCurrentVersion": "현재", - "changelogVersionDetails": "릴리스 상세", - "changelogBodyEmpty": "이 릴리스에 대한 설명이 없습니다.", - "mapPlaceholderDisabled": "지도 (일시 사용 중지)", - "moreSectionRegion": "지역", - "moreSectionNotify": "알림", - "moreSectionDisplay": "표시", - "regionManageTitle": "저장한 지역", - "regionAddButton": "지역 추가", - "regionEmpty": "저장된 지역이 없습니다", - "regionSelectTitle": "지역 선택", - "regionSelectCount": "{count}/{max} 선택됨", - "regionSelectFull": "최대 {max}개 지역까지 저장할 수 있습니다", - "regionEdit": "수정", - "moreSectionAdvanced": "고급", - "moreDeveloper": "디버그 정보", - "experimentalFeatures": "실험적 기능", - "moreSectionLinks": "링크", - "moreCwaEew": "중앙기상청(CWA) 지진 조기경보", - "moreTremReport": "TREM 탐지 보고", - "moreServerStatus": "서버 상태", - "moreAnnouncements": "공지사항", - "moreDiscord": "Discord 커뮤니티", - "moreNotifyLog": "DPIP 알림 발송 기록", - "moreLinkOpenFailed": "링크를 열 수 없습니다", - "weatherDynamicState": "날씨 애니메이션", - "weatherDynamicStateSubtitle": "홈 배경 날씨를 재정의합니다", - "weatherModeAuto": "자동", - "weatherModeClear": "맑음", - "weatherModeRain": "비", - "weatherModeFog": "안개", - "weatherModeThunderstorm": "뇌우", - "commonLoading": "불러오는 중…", - "commonRetry": "다시 시도", - "commonError": "문제가 발생했습니다", - "commonFetchFailed": "데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.", - "commonEmpty": "표시할 내용이 없습니다", - "feedConnecting": "연결 중…", - "feedStale": "데이터가 오래되었을 수 있습니다", - "feedOffline": "연결이 끊어졌습니다", - "eewTitle": "지진 조기경보", - "eewNone": "현재 지진 조기경보가 없습니다", - "eewSummary": "규모 {magnitude} · 깊이 {depth} km", - "regionNationwide": "전국", - "regionCurrent": "현재 위치", - "regionCurrentUnavailable": "현재 위치를 가져올 수 없습니다", - "weatherPrecipitation": "강수량", - "weatherHumidity": "습도", - "weatherDataTime": "{station} · 데이터 시간 {time}", - "homeViewOnMap": "지도에서 보기", - "homeForecastTitle": "24시간 예보", + "meshtasticSilent": "Silent", + "reportFilterSortMagnitude": "규모", + "mapLayerCategoryEarthquake": "지진", + "mapLayerSatelliteB12": "히마와리 오존(B12)", + "typhoonLegendPast": "실황 경로", + "restroomCategoryOther": "기타", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "최고 {high}° · 최저 {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "체감 {temp}°", - "homeForecastHumidity": "습도 {value}%", - "homeForecastWind": "{direction} · 풍력 {level}", - "homeForecastUnavailable": "지역을 선택하면 예보를 볼 수 있습니다", - "homeForecastEmpty": "예보 데이터가 없습니다", - "homeActiveEventsTitle": "발효 중 이벤트", - "homeActiveEventsEmpty": "발효 중인 이벤트가 없습니다", - "homeRainTrendTitle": "향후 1시간 강수", - "homeRainTrendMinute": "{minute}분", - "homeRainTrendUpdated": "업데이트 {time}", - "homeRainTrendNoData": "데이터 없음", - - "homeRainTrendScattered": "약한 비가 올 수 있어요", - "homeRainTrendLightSustained": "앞으로 1시간 동안 약한 비가 이어질 거예요", - "homeRainTrendLightStopping": "{minutes}분 후에 비가 그칠 것으로 예상돼요", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "설정 열기", + "mapLegendExpand": "범례", + "eewNone": "현재 지진 조기경보가 없습니다", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "지진해일 주의보 및 경보", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "Node options", + "onboardingAgreeContinue": "동의하고 계속", + "meshtasticNodeId": "Node ID", + "commonRetry": "다시 시도", + "reportDetailNumbered": "번호 {number} 유의미 유감지진", + "typhoonOverlayStormBandSubtitle": "With average circle", + "disasterMapOverlayRestroomTooltip": "공중화장실 표시", + "weatherRankingTitle": "관측 순위", "homeRainTrendHeavySustained": "앞으로 1시간 동안 강한 비가 이어질 거예요", - "homeRainTrendHeavyStopping": "{minutes}분 후에 강한 비가 그칠 것으로 예상돼요", - "mapLayers": "레이어", - "mapLayerOrderTitle": "레이어 순서", - "mapLayerOrderReset": "기본 순서로 재설정", - "mapLayerRadar": "레이더 합성 에코", - "mapLayerSatellite": "히마와리 적외(B13)", - "mapLayerSatelliteB01": "히마와리 가시 청색(B01)", - "mapLayerSatelliteB02": "히마와리 가시 녹색(B02)", - "mapLayerSatelliteB03": "히마와리 가시 적색(B03)", - "mapLayerSatelliteB04": "히마와리 근적외(B04)", + "notifySectionTsunami": "지진해일", + "restroomCategoryPark": "공원", + "moreLinkOpenFailed": "링크를 열 수 없습니다", + "themeDark": "다크", + "sponsorRestore": "구매 복원", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "Traffic", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD 커브——열대저기압 강도 분석용 계단 그레이스케일", + "disasterMapOverlayAedTooltip": "AED 위치 표시", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "습도", + "mapLayerSatelliteTransparentNight": "야간 = 투명,배경 지도 표시", + "meshtasticScanning": "Scanning…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "최대 {max}개 지역까지 저장할 수 있습니다", + "meshtasticTitle": "Meshtastic", + "navMore": "더보기", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "레이어", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "히마와리 근적외(B05)", - "mapLayerSatelliteB06": "히마와리 근적외(B06)", - "mapLayerSatelliteB07": "히마와리 단파 적외(B07)", - "mapLayerSatelliteB08": "히마와리 상층 수증기(B08)", - "mapLayerSatelliteB09": "히마와리 중층 수증기(B09)", - "mapLayerSatelliteB10": "히마와리 하층 수증기(B10)", - "mapLayerSatelliteB11": "히마와리 이산화황/구름상(B11)", - "mapLayerSatelliteB12": "히마와리 오존(B12)", - "mapLayerSatelliteB13": "히마와리 적외(B13)", - "mapLayerSatelliteB14": "히마와리 장파 적외(B14)", - "mapLayerSatelliteB15": "히마와리 장파 적외(B15)", - "mapLayerSatelliteB16": "히마와리 이산화탄소(B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "NE", + "meshtasticCopied": "Message copied", + "reportListEmpty": "지진 보고서가 없습니다", + "reportListEnd": "마지막입니다", "mapLayerSatelliteTruecolor": "히마와리 트루컬러", - "mapLayerSatelliteNaturalcolor": "히마와리 내추럴컬러", - "mapLayerSatelliteAsh": "히마와리 화산재", - "mapLayerSatelliteDust": "히마와리 황사", - "mapLayerSatelliteAirmass": "히마와리 에어매스", - "mapLayerSatelliteNightmicrophysics": "히마와리 야간 미세물리", - "mapLayerSatelliteWatervapor": "히마와리 수증기", - "mapLayerSatelliteBtdSplit": "히마와리 스플릿 윈도우", - "mapLayerSatelliteBtdFog": "히마와리 야간 안개", - "mapLayerSatelliteBtdWvirw": "히마와리 오버슈팅 탑", - "mapLayerSatelliteBtdSo2": "히마와리 이산화황/구름상", - "mapLayerSatelliteBtdCo2": "히마와리 권운/운고", - "mapLayerSatelliteBtdOzone": "히마와리 대류권계면", - "mapLayerSatelliteCloudtop": "히마와리 운정 온도", - "mapLayerSatelliteCloudmask": "히마와리 구름 마스크", - "mapLayerSatelliteSst": "히마와리 해수면 온도", - "mapLayerSatelliteNdvi": "히마와리 NDVI", - "mapLayerSatelliteNdwi": "히마와리 NDWI", - "mapLayerSatelliteMndwi": "히마와리 MNDWI", + "typhoonOverlaySectionExtra": "Overlays", + "eewSWave": "S파", + "meshtasticBusyTitle": "Another app is using this radio", + "restroomCategoryCultural": "문화·여가 시설", + "typhoonLabelWind": "Max. sustained wind near centre", + "radarGlobalOutlineHint": "각국 국경선", + "notifyEvacuation": "재난 정보", + "typhoonLegendCircle15": "강풍권 (7급)", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "Astronomy", + "homeRainTrendLightSustained": "앞으로 1시간 동안 약한 비가 이어질 거예요", + "commonError": "문제가 발생했습니다", + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "현재", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "보고서 페이지", + "trendRange7d": "7일", + "typhoonWarningAreas": "대상 지역: {areas}", + "rainIntervalSection": "집계 시간", + "notifyTitle": "알림", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "구분", + "sponsorRestoring": "구매를 복원하는 중…", + "sponsorIntro": "DPIP는 실시간 재난 예방 정보를 제공하는 데 전념하며, 광고나 다른 수익 모델이 없습니다. 여러분의 후원은 서버 운영과 지속적인 개발에 도움이 됩니다.", + "shelterAddressLabel": "주소", + "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "상업·영업 시설", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "지역", + "homeRainTrendLightStopping": "{minutes}분 후에 비가 그칠 것으로 예상돼요", + "reportDetailInfo": "상세 정보", + "mapNavWind": "풍향", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "바람 예보 레이어 옵션", + "dataWeatherRankingSubtitle": "실시간 관측 순위", + "rainInterval6h": "6시간", + "homeRainTrendMinute": "{minute}분", + "restroomTypeUnspecified": "미설정", + "typhoonOverlayProbabilityHint": "Hides the forecast cone", "mapLayerSatelliteGlobalOutline": "국경선", - "mapLayerSatelliteRgbComposite": "RGB 합성(JMA 레시피)", - "mapLayerSatelliteCloudClear": "맑음", - "mapLayerSatelliteCloudProbablyClear": "아마 맑음", - "mapLayerSatelliteCloudProbablyCloudy": "아마 구름", + "mapNavTemperature": "온도", + "typhoonLegendForecastPoint": "예보 지점", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "어제", + "moreSectionLinks": "링크", + "feedOffline": "연결이 끊어졌습니다", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "표시", + "rainInterval3d": "3일", + "defaultMapLayerSubtitle": "지도 탭을 열 때 표시할 레이어입니다. 하단 탐색 아이콘과 라벨도 함께 바뀝니다.", + "aedDescription": "비고", + "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", + "onboardingPermLocationDesc": "현재 위치에 맞춰 경보를 전달합니다.", + "mapLayerSatelliteB16": "히마와리 이산화탄소(B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "발효 중인 이벤트가 없습니다", + "typhoonLabelPosition": "Centre location", + "weatherRankingBy": "정렬", + "typhoonIntensityMild": "Mild typhoon", + "windForecastGlobalOutlineHint": "각국 국경선", + "rainInterval1h": "1시간", + "eewLocalIntensity": "현재 위치 예상", + "mapLayerRadar": "레이더 합성 에코", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "종교·의례 시설", + "meshtasticRole": "Role", "mapLayerSatelliteCloudCloudy": "구름", - "mapLayerSatelliteTransparentWarm": "맑음(고온부) = 투명,배경 지도 표시", - "mapLayerSatelliteTransparentReflectance": "낮은 반사율/야간 = 투명,배경 지도 표시", - "mapLayerSatelliteTransparentZero": "차이 0 = 투명(신호 없음)", - "mapLayerSatelliteTransparentNight": "야간 = 투명,배경 지도 표시", - "mapLayerSatelliteTransparentNoData": "데이터 없음(육지) = 투명", - "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 투명(식생 없음)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = 투명(수역 없음)", - "mapLayerSatelliteTransparentClear": "맑음 = 투명,배경 지도 표시", + "skyTimeSunrise": "일출", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "No messages yet", + "onboardingPermNotifyDesc": "지진, 날씨, 재해가 발생하는 즉시 경보를 전달합니다.", + "radarTownOutline": "읍·면·동 경계", "mapLayerStyleSection": "색상 스타일", - "mapLayerStyleTooltip": "색상 스타일", - "mapLayerStyleGray": "그레이스케일(JMA)", - "mapLayerStyleGrayTooltip": "기상청 적외 영상 관례:온도가 낮을수록 흰색", - "mapLayerStyleJma": "운정 강조(JMA)", - "mapLayerStyleJmaTooltip": "그레이스케일 바탕에 −40 °C 이하를 채색, 운정 고도 강조", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD 커브——열대저기압 강도 분석용 계단 그레이스케일", - "mapLayerQpesums": "1시간 강수 예보", - "mapLayerLightning": "번개", - "lightningLegendCg": "대지로 · {minutes}분 이내", - "lightningLegendCc": "구름 사이 · {minutes}분 이내", - "mapTimelineNow": "현재", - "mapTimelinePast": "과거", - "mapTimelineFuture": "미래", - "mapTimelineObserved": "관측", - "mapTimelineForecast": "예보", - "mapTimelineDataTime": "데이터 시간 {time}", - "notifySettingsMenu": "알림 설정", - "notifyTitle": "알림", - "notifyUnavailable": "푸시 알림이 아직 준비되지 않았습니다. 잠시 후 다시 시도해 주세요.", - "notifySetFailed": "설정을 저장하지 못했습니다. 다시 시도해 주세요.", - "notifySectionEew": "지진 조기경보", - "notifySectionEarthquake": "지진", - "notifySectionWeather": "날씨", - "notifySectionTsunami": "지진해일", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "방재 지도 레이어", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "SW", + "typhoonForecastLead": "Forecast +{hours} h", + "dpmDisasterTsunami": "쓰나미", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "정식", + "mapLayerSatelliteTransparentClear": "맑음 = 투명,배경 지도 표시", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "참조 레이어", + "mapLayerSatelliteB02": "히마와리 가시 녹색(B02)", + "reportListLocalFelt": "소규모 유감", + "weatherRankingEmpty": "정렬할 관측이 없습니다", "notifySectionOther": "기타", - "notifyEew": "긴급 지진 경보", - "notifyMonitor": "강진 감시기", - "notifyReport": "지진 보고", - "notifyIntensity": "진도 속보", - "notifyThunderstorm": "뇌우 알림", - "notifyAdvisory": "기상 특보", - "notifyEvacuation": "재난 정보", - "notifyTsunami": "지진해일 정보", - "notifyAnnouncement": "공지사항", - "notifyOptOff": "끄기", - "notifyOptAll": "전체 수신", + "weatherRankingMeta": "자료 시각: {time}\n관측점 {count}", + "onboardingTermsAgree": "서비스 약관을 읽었으며 이에 동의합니다", + "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 투명(식생 없음)", "notifyOptLocalIntensity4": "현재 위치 진도 4 이상", - "notifyOptLocalIntensity1": "현재 위치 진도 1 이상", - "notifyOptWeatherLocal": "현재 위치만", - "notifyOptTsunamiWarning": "지진해일 경보만", - "notifyOptTsunamiAll": "지진해일 주의보 및 경보", - "onboardingNext": "다음", - "onboardingBack": "이전", + "eewArrived": "도달", + "meshtasticNoDevices": "No Meshtastic devices found", + "mapLayerCategoryLife": "생활", + "reportFilterSortIntensity": "진도", + "typhoonMotion": "이동", + "meshtasticStateDisconnected": "Disconnected", + "typhoonIntensityIntense": "Intense typhoon", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "레이어 순서", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "예", + "meshtasticNoHistory": "Not enough history yet", + "reportDetailLocalIntensityUnavailable": "진도 정보 없음", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "깊이", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "계속하려면 아래로 스크롤하세요", - "onboardingIntroTitle": "DPIP에 오신 것을 환영합니다", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "예보", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "지도", + "notifyAdvisory": "기상 특보", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "초기화", + "mapLayerSatelliteMndwi": "히마와리 MNDWI", + "typhoonOverlaySectionStorm": "Storm wind", + "moonPhaseFull": "Full moon", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "weatherDynamicStateSubtitle": "홈 배경 날씨를 재정의합니다", + "reportFilterIntensityInfoModernTitle": "신제(2020년 이후)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "Data time\n{time}", + "restroomTypeAccessible": "장애인 화장실", + "moreSectionAbout": "정보", + "meshtasticSelectDevice": "Select a radio", "onboardingIntroBody": "DPIP는 여러분과 함께하는 방재 파트너입니다. 지진 조기경보, 지진 보고, 날씨, 각종 재해 정보를 통합하여 중요한 순간에 실시간으로 알려드립니다.\n\n• 지진: 지진 조기경보, 진도 속보, 상세 지진 보고\n• 날씨: 뇌우 실시간 메시지, 기상 특보\n• 지진해일 및 재난 정보\n\n다음으로, 서비스 약관을 확인하고 DPIP가 실시간으로 여러분을 보호할 수 있도록 몇 가지 권한을 허용해 주시기 바랍니다.", - "onboardingTermsTitle": "서비스 약관", - "onboardingTermsBody": "DPIP를 사용하기 전에 다음 유의 사항을 반드시 읽어 주세요:\n\n• 모든 정보는 중앙기상청(CWA)에서 발표한 내용을 기준으로 합니다.\n\n• 네트워크, 서버, 애플리케이션, 상위 데이터 출처의 상태에 따라 정보를 받지 못할 수 있습니다. 이러한 상황을 방지하기 위해 최선을 다하고 있으나, 절대 발생하지 않는다고 보장할 수는 없습니다.\n\n• 강한 흔들림이 알림보다 먼저 귀하의 위치에 도달할 수 있습니다.\n\n• 지진 조기경보는 빠르게 계산된 결과로 상당한 오차가 있을 수 있으므로, 이를 이해하고 신중하게 사용하시기 바랍니다.\n\n• 당국이 승인하지 않은 모든 행위는 법적 위험을 수반할 수 있으니, 관련 규정을 반드시 준수해 주세요.\n\n또한 지역 맞춤형 경보를 제공하기 위해, 본 서비스는 귀하에게 어떤 경보를 보낼지 결정하기 위한 목적으로만 포그라운드 및 백그라운드에서 귀하의 대략적인 위치와 푸시 식별자를 수집하여 업로드합니다.\n\n하단의 “동의하고 계속”을 누르면 위 사항을 읽고 이해했으며 이에 동의함을 확인하는 것입니다.", - "onboardingTermsAgree": "서비스 약관을 읽었으며 이에 동의합니다", - "onboardingAgreeContinue": "동의하고 계속", - "onboardingPermsTitle": "권한 허용", - "onboardingPermsBody": "재해가 발생하는 즉시 알려드릴 수 있도록 다음 권한을 허용해 주세요. 시스템 설정에서 언제든지 변경할 수 있습니다.", + "shelterCapacityLabel": "수용 인원", + "reportDetailImage": "지진 보고서 이미지", + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", "onboardingPermNotify": "알림", - "onboardingPermNotifyDesc": "지진, 날씨, 재해가 발생하는 즉시 경보를 전달합니다.", - "onboardingPermCritical": "중요 알림", - "onboardingPermCriticalDesc": "생명을 위협하는 지진 경보가 무음 모드나 방해 금지 모드에서도 소리를 낼 수 있도록 합니다.", - "onboardingPermLocation": "위치", - "onboardingPermLocationDesc": "현재 위치에 맞춰 경보를 전달합니다.", - "onboardingPermBackground": "백그라운드 위치", - "onboardingPermBackgroundDesc": "“항상 허용”을 선택하면 앱이 종료된 상태에서도 위치 맞춤 경보를 받을 수 있습니다.", - "onboardingPermBattery": "배터리 최적화 제외", - "onboardingPermBatteryDesc": "DPIP가 백그라운드에서 계속 실행되어 경보가 지연되거나 누락되지 않도록 허용합니다.", - "onboardingGrant": "허용", - "onboardingGranted": "허용됨", - "onboardingStart": "시작하기", - "language": "언어", - "languageSettings": "언어", - "languageSystem": "시스템 기본값", - "locationBannerServiceOff": "위치 서비스가 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.", - "locationBannerPermission": "위치 권한이 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.", - "locationBannerFix": "설정 열기", - "notifyBannerDisabled": "알림이 꺼져 있어 재난 경보를 받을 수 없습니다.", - "onboardingSkipTitle": "권한이 허용되지 않았습니다", - "onboardingSkipBody": "위치 및 알림 권한이 없으면 DPIP가 주변의 지진과 재난을 실시간으로 알려드릴 수 없습니다. 나중에 설정에서 권한을 허용할 수 있습니다.", - "onboardingSkipStay": "돌아가기", - "onboardingSkipLeave": "그래도 건너뛰기", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "Clear messages", + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "지도 기본 레이어", + "moreSectionNotify": "알림", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "푸시 알림이 아직 준비되지 않았습니다. 잠시 후 다시 시도해 주세요.", + "mapLayerOrderReset": "기본 순서로 재설정", + "dpmAddress": "주소", + "weatherRankingMergeCounty": "현시", + "moreSectionApp": "앱 다운로드", + "reportFilterIntensityInfoLegacyBody": "진도는 0–7만 있으며 5약/5강/6약/6강 구분이 없습니다.", + "mapLayerSatelliteSst": "히마와리 해수면 온도", + "qpesumsOverlayMenuTooltip": "정량 강수 예보 레이어 옵션", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "미래", + "typhoonLegendCircleAvg": "Average circle", + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "SE", + "radarTownOutlineHint": "더 세밀한 구획", + "eewCountdown": "{seconds}초", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "Peak gust", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "이용약관", + "restroomTypeGenderNeutral": "성중립 화장실", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "뇌우 알림", + "skyTimeGolden": "골든아워", + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "현재 {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "지역을 선택하면 예보를 볼 수 있습니다", + "mapLayers": "레이어", + "meshtasticHardware": "Hardware", + "languageSettings": "언어", + "dpmDisasterNuclear": "핵 사고", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "언어", + "homeForecastFeelsLike": "체감 {temp}°", + "typhoonOverlayWeatherHint": "Aligned to bulletin time", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "여명", + "skyTimeAfternoon": "오후", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "태풍 경보", "moreSourceCode": "소스 코드", - "moreSectionApp": "앱 다운로드", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "화면", - "defaultMapLayerSettings": "지도 기본 레이어", - "defaultMapLayerSubtitle": "지도 탭을 열 때 표시할 레이어입니다. 하단 탐색 아이콘과 라벨도 함께 바뀝니다.", - "mapNavRadar": "레이더", - "mapNavQpesums": "예보", - "mapNavSatellite": "위성", - "mapNavLightning": "번개", - "mapNavTyphoon": "태풍", + "mapLayerCategoryWeather": "기상 관측", + "mapLayerSatelliteB09": "히마와리 중층 수증기(B09)", + "windForecastTownOutlineHint": "더 촘촘한 망", + "mapLayerSatelliteCloudmask": "히마와리 구름 마스크", + "mapAppCopyCoordinates": "좌표 복사", + "reportFilterIntensityInfoIntro": "기상서는 2020년 1월 1일(타이베이 시간)부터 신제 진도를 사용합니다.", "mapNavEarthquake": "지진", - "mapNavTemperature": "온도", - "mapNavHumidity": "습도", - "mapNavPressure": "기압", - "mapNavWind": "풍향", + "typhoonGust": "순간최대풍속", + "restroomGradeAverage": "보통", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "히마와리 권운/운고", + "onboardingPermBackgroundDesc": "“항상 허용”을 선택하면 앱이 종료된 상태에서도 위치 맞춤 경보를 받을 수 있습니다.", + "mapTimelineForecast": "예보", + "restroomTypeLabel": "유형", + "navEarthquake": "지진", + "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "지진 보고서", + "moreTremReport": "TREM 탐지 보고", + "weatherDataTime": "{station} · 데이터 시간 {time}", + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "radarCountyOutline": "시·군 경계", + "onboardingGranted": "허용됨", + "@mapAppCopyCoordinates": {}, + "commonClose": "닫기", + "restroomGradeLabel": "등급", + "rainIntervalNow": "오늘", + "changelogCurrentVersion": "현재", + "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", + "typhoonLabelPressure": "Central pressure", + "aedOpenRemark": "운영시간 비고", + "onboardingPermsBody": "재해가 발생하는 즉시 알려드릴 수 있도록 다음 권한을 허용해 주세요. 시스템 설정에서 언제든지 변경할 수 있습니다.", + "typhoonOverlaySectionWeather": "Weather underlay", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "현재 위치만", "mapNavRain": "강우", - "mapNavDisaster": "방재", - "displayTheme": "테마", + "moonDays": "days", + "mapLegendUnit": "단위: {unit}", + "weatherModeClear": "맑음", + "meshtasticRadio": "Radio", + "commonEmpty": "표시할 내용이 없습니다", + "mapLayerSatelliteB01": "히마와리 가시 청색(B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "오름차순", + "reportFilterApply": "적용", + "reportDetailImageUnavailable": "보고서 이미지가 아직 없습니다", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "최고", + "reportDetailReplay": "다시 보기", + "mapLayerRestroom": "공중화장실", + "restroomCategoryWelfare": "사회복지 기관·집회 시설", + "restroomGradeExcellent": "최우수", + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "수치 예보", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", "themeSystem": "시스템", - "themeLight": "라이트", - "themeDark": "다크", - "moreSectionAbout": "정보", - "termsOfService": "서비스 약관", - "faq": "자주 묻는 질문", - "openSourceLicenses": "오픈소스 라이선스", - "sponsorTitle": "DPIP 후원하기", - "sponsorIntro": "DPIP는 실시간 재난 예방 정보를 제공하는 데 전념하며, 광고나 다른 수익 모델이 없습니다. 여러분의 후원은 서버 운영과 지속적인 개발에 도움이 됩니다.", - "sponsorSubscriptions": "구독", - "sponsorRecommended": "추천", - "sponsorOneTime": "일회성 후원", - "sponsorPerMonth": "{price} / 월", - "sponsorRestore": "구매 복원", - "sponsorTerms": "이용약관", - "sponsorPrivacy": "개인정보 처리방침", - "sponsorRestoring": "구매를 복원하는 중…", - "sponsorRestoreUnavailable": "스토어에 연결할 수 없습니다. 나중에 다시 시도해 주세요.", - "commonClose": "닫기", + "mapLayerSatelliteNdvi": "히마와리 NDVI", + "typhoonLegendForecast": "예보 경로", + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "강수량", + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "지도에서 마커를 눌러 상세 보기", + "onboardingSkipLeave": "그래도 건너뛰기", + "onboardingBack": "이전", + "aedPlaceDesc": "설치 위치", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "권한이 허용되지 않았습니다", + "restroomTypeFamily": "가족 화장실", + "typhoonValueKm": "{n} km", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "기압", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "배터리 최적화 제외", + "typhoonLabelNw": "NW", + "dpmDisasterFlood": "홍수", + "moonPhaseWaxingCrescent": "Waxing crescent", + "restroomCategoryLeisure": "휴양·오락 시설", "mapLayerTemperature": "기온", - "trendRange24h": "24시간", - "trendRange7d": "7일", - "trendNoData": "추세 데이터 없음", - "trendCumulativeTotal": "누적 {total} mm", - "chartHourLabel": "{hour}시", - "mapLayerHumidity": "습도", - "mapLayerPressure": "기압", + "aedCategory": "분류", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "Channels", + "monitorWaiting": "데이터 대기 중…", + "typhoonOverlayForecastCallouts": "Forecast tooltips", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "진앙 좌표", + "meshtasticVoltage": "Voltage", + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "바람", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "강수량", - "rainIntervalMenu": "누적 구간", - "rainIntervalNow": "오늘", - "rainInterval10m": "10분", - "rainInterval1h": "1시간", - "rainInterval3h": "3시간", - "rainInterval6h": "6시간", + "reportDetailMagnitude": "지진 규모", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "지역별 진도", "rainInterval12h": "12시간", - "rainInterval24h": "24시간", - "rainInterval2d": "2일", - "rainInterval3d": "3일", - "mapLayerTyphoon": "태풍", - "typhoonNoActive": "활성 태풍 없음", - "typhoonWind": "풍속", - "typhoonGust": "순간최대풍속", - "typhoonPressure": "기압", - "typhoonMotion": "이동", - "mapLayerMonitor": "실시간 지진 모니터", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "방재 지도", - "disasterMapOverlayMenuTooltip": "방재 지도 레이어", - "disasterMapOverlaySectionLayers": "레이어", - "disasterMapOverlayAedTooltip": "AED 위치 표시", - "aedAddress": "주소", - "aedRegion": "지역", - "aedCategory": "분류", - "aedType": "유형", - "aedPlaceDesc": "설치 위치", - "aedDescription": "비고", - "aedHoursWeekday": "평일 운영시간", - "aedHoursSaturday": "토요일 운영시간", - "aedHoursSunday": "일요일 운영시간", - "aedOpenRemark": "운영시간 비고", - "aedEmergencyPhone": "비상 연락처", - "mapLayerRestroom": "공중화장실", - "mapLayerShelter": "대피소", - "disasterMapOverlayRestroomTooltip": "공중화장실 표시", - "disasterMapOverlayShelterTooltip": "대피소 표시", - "dpmOpenInMaps": "지도에서 열기", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "산사태", + "notifyMonitor": "강진 감시기", + "onboardingStart": "시작하기", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / 월", + "mapLayerPressure": "기압", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "히마와리 근적외(B04)", + "mapLayerSatelliteTransparentZero": "차이 0 = 투명(신호 없음)", + "shelterIndoorLabel": "실내 수용", + "notifyOptOff": "끄기", + "reportFilterSortTime": "시간", + "mapLayerSatelliteCloudProbablyClear": "아마 맑음", + "weatherModeThunderstorm": "뇌우", + "homeViewOnMap": "지도에서 보기", + "reportFilterIntensityInfoLegacyTitle": "구제(2020년 이전)", + "typhoonLabelSpeed": "Past movement speed", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "{app}을(를) 열 수 없습니다", + "mapLayerSatelliteRgbComposite": "RGB 합성(JMA 레시피)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "오늘 최저", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "히마와리 하층 수증기(B10)", + "mapLayerSatelliteCloudProbablyCloudy": "아마 구름", + "shelterCategoryLabel": "적용 재해", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = 투명(수역 없음)", + "meshtasticStateConnecting": "Connecting…", + "moonTitle": "Moon", + "weatherRankingGust": "돌풍", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "대피소 재해 유형", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "서버 상태", + "notifySectionWeather": "날씨", + "meshtasticPreset": "Modem preset", + "dataSectionSeismic": "지진", + "changelogBodyEmpty": "이 릴리스에 대한 설명이 없습니다.", + "radarGlobalOutline": "국경", + "notifyEew": "긴급 지진 경보", + "regionNationwide": "전국", + "moreNotifyLog": "DPIP 알림 발송 기록", + "regionCurrent": "현재 위치", + "dpmFilterSectionRestroom": "시설 유형", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "눈", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "디버그 정보", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "히마와리 장파 적외(B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "번개", + "homeForecastEmpty": "예보 데이터가 없습니다", + "sponsorOneTime": "일회성 후원", + "mapLayerSatelliteBtdSplit": "히마와리 스플릿 윈도우", + "onboardingPermBackground": "백그라운드 위치", + "aedEmergencyPhone": "비상 연락처", + "dpmOpenInMaps": "지도에서 열기", + "meshtasticNotifyNodes": "Notify on new nodes", + "onboardingPermCriticalDesc": "생명을 위협하는 지진 경보가 무음 모드나 방해 금지 모드에서도 소리를 낼 수 있도록 합니다.", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "맑음(고온부) = 투명,배경 지도 표시", + "meshtasticSent": "Sent", + "homeForecastTitle": "24시간 예보", + "typhoonLegendWarningAreas": "경보 지역", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "현재 위치 진도 1 이상", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "과거", + "restroomTypeFemale": "여자 화장실", + "reportListToday": "오늘", + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "불러오는 중…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "Moderate typhoon", + "typhoonWind": "풍속", + "mapLayerSatelliteAsh": "히마와리 화산재", + "rainInterval3h": "3시간", + "reportListSearch": "조회", + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "위성", + "reportFilterLocation": "위치", + "mapLayerSatelliteNightmicrophysics": "히마와리 야간 미세물리", + "typhoonIntensityTd": "Tropical depression", + "reportFilterDate": "날짜", + "sponsorRestoreUnavailable": "스토어에 연결할 수 없습니다. 나중에 다시 시도해 주세요.", + "homeForecastPop": "{pop}%", + "regionEmpty": "저장된 지역이 없습니다", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "DPIP가 백그라운드에서 계속 실행되어 경보가 지연되거나 누락되지 않도록 허용합니다.", + "mapNavDisaster": "방재", + "radarScanRangeSubtitle": "레이더 4기가 실제로 관측하는 범위를 표시합니다.", + "aedHoursSunday": "일요일 운영시간", + "reportDetailOriginTime": "발생 시각", + "trendNoData": "추세 데이터 없음", + "onboardingPermLocation": "위치", + "moreDiscord": "Discord 커뮤니티", + "mapNavPressure": "기압", + "mapLayerSatelliteB13": "히마와리 적외(B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "아직 릴리스 노트가 없습니다", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "시작일: 당일 00:00(타이베이)", + "eewTitle": "지진 조기경보", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "ko", + "regionSelectCount": "{count}/{max} 선택됨", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "히마와리 이산화황/구름상", + "meshtasticStateError": "Error", + "weatherModeOvercast": "흐림", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "진원 깊이", + "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", + "reportFilterDatePick": "날짜 선택", + "onboardingSkipStay": "돌아가기", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "실외 수용", + "meshtasticStateConnected": "Connected", + "mapNavRadar": "레이더", + "mapLayerSatelliteCloudClear": "맑음", + "eewSummary": "규모 {magnitude} · 깊이 {depth} km", + "locationBannerPermission": "위치 권한이 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.", + "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", + "radarCountyOutlineHint": "에코 위에 표시", + "windForecastCountyOutlineHint": "바람장 위에 표시", + "homeRainTrendTitle": "향후 1시간 강수", + "moonPhaseFirstQuarter": "First quarter", + "mapLayerCategoryTyphoon": "태풍", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "Airtime (24h)", + "restroomTypeMixed": "남녀 공용 화장실", + "restroomGradeGood": "우수", + "notifyTsunami": "지진해일 정보", + "navData": "자료", + "mapLayerSatelliteBtdWvirw": "히마와리 오버슈팅 탑", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "이 기기에서는 전화를 걸 수 없습니다", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "전체", + "weatherRankingMergeTo": "병합", + "notifyIntensity": "진도 속보", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "누적 구간", + "reportDetailLocalFelt": "국지적 유감지진", + "meshtasticDevice": "Device", + "onboardingGrant": "허용", + "weatherModeRain": "비", + "shelterVulnerableOkLabel": "취약계층 수용 가능", + "stationSheetEmpty": "관측소를 눌러 관측값 보기", + "typhoonLegendProbability": "내습 확률", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "규모", + "skyTimeMorning": "오전", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "실험적 기능", + "onboardingTermsBody": "DPIP를 사용하기 전에 다음 유의 사항을 반드시 읽어 주세요:\n\n• 모든 정보는 중앙기상청(CWA)에서 발표한 내용을 기준으로 합니다.\n\n• 네트워크, 서버, 애플리케이션, 상위 데이터 출처의 상태에 따라 정보를 받지 못할 수 있습니다. 이러한 상황을 방지하기 위해 최선을 다하고 있으나, 절대 발생하지 않는다고 보장할 수는 없습니다.\n\n• 강한 흔들림이 알림보다 먼저 귀하의 위치에 도달할 수 있습니다.\n\n• 지진 조기경보는 빠르게 계산된 결과로 상당한 오차가 있을 수 있으므로, 이를 이해하고 신중하게 사용하시기 바랍니다.\n\n• 당국이 승인하지 않은 모든 행위는 법적 위험을 수반할 수 있으니, 관련 규정을 반드시 준수해 주세요.\n\n또한 지역 맞춤형 경보를 제공하기 위해, 본 서비스는 귀하에게 어떤 경보를 보낼지 결정하기 위한 목적으로만 포그라운드 및 백그라운드에서 귀하의 대략적인 위치와 푸시 식별자를 수집하여 업로드합니다.\n\n하단의 “동의하고 계속”을 누르면 위 사항을 읽고 이해했으며 이에 동의함을 확인하는 것입니다.", + "reportFilterTitle": "필터", + "onboardingPermCritical": "중요 알림", + "trendCumulativeTotal": "누적 {total} mm", + "languageName": "한국어", + "reportListEmptyFiltered": "조건에 맞는 지진 보고서가 없습니다", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "태풍", + "weatherModeSand": "황사", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "위성", + "@dpmOpenInMaps": {}, + "notifyReport": "지진 보고", + "mapAppCoordinatesCopied": "좌표가 복사되었습니다", + "skyTimeNight": "밤", + "sponsorRecommended": "추천", + "mapLayerSatelliteB15": "히마와리 장파 적외(B15)", + "weatherRankingWind": "풍속", + "feedStale": "데이터가 오래되었을 수 있습니다", + "homeForecastWind": "{direction} · 풍력 {level}", + "navHome": "홈", + "meshtasticRegionLabel": "Region", + "mapLayerSatelliteCloudtop": "히마와리 운정 온도", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "오픈소스 라이선스", + "weatherRankingLowest": "최저", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "깊이", + "mapTimelineDataTime": "데이터 시간 {time}", + "radarScanRange": "스캔 범위 표시", + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "일교차 {value}°C", + "weatherRankingExtremeHigh": "오늘 최고", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "릴리스 상세", + "sponsorPrivacy": "개인정보 처리방침", + "reportDetailLocalIntensity": "내 위치의 진도", + "mapLayerSatelliteNaturalcolor": "히마와리 내추럴컬러", + "meshtasticAirtime": "Air time (TX)", + "shelterCapacityValue": "{n} 명", + "lightningLegendCc": "구름 사이 · {minutes}분 이내", + "meshtasticSendHint": "Message to broadcast", + "monitorDelay": "지연 {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "아니요", + "mapLayerSatelliteB08": "히마와리 상층 수증기(B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "레이더 에코 아래에서도 읍·면·동 경계가 보이도록 합니다.", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", + "radarScanRangeHint": "범위 밖 공백은 미관측", + "typhoonPickerTd": "Tropical depression TD {no}", + "mapLayerSatelliteWatervapor": "히마와리 수증기", + "regionAddButton": "지역 추가", + "displaySettings": "화면", + "restroomGradePoor": "불합격", + "restroomCategoryTourist": "관광 지역·경치 구역", + "locationBannerServiceOff": "위치 서비스가 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.", + "mapLayerStyleTooltip": "색상 스타일", + "lightningLegendCg": "대지로 · {minutes}분 이내", + "skyTimeAuto": "자동", + "appLogs": "앱 로그", + "feedConnecting": "연결 중…", + "notifyBannerDisabled": "알림이 꺼져 있어 재난 경보를 받을 수 없습니다.", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "습도", + "typhoonValueMs": "{n} m/s", + "homeForecastHumidity": "습도 {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "교통", + "reportFilterLocationHint": "예: 화롄, 해역", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "거리", + "meshtasticSnrTrend": "신호 추이 (SNR)", + "meshtasticBatteryTrend": "배터리 추이", + "typhoonOverlayMenuTooltip": "Typhoon overlay options", + "mapLayerSatelliteBtdOzone": "히마와리 대류권계면", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "지진", + "mapLayerDisasterMap": "방재 지도", + "weatherModeFog": "안개", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "기상청 적외 영상 관례:온도가 낮을수록 흰색", + "moreAnnouncements": "공지사항", + "mapLayerSatelliteTransparentNoData": "데이터 없음(육지) = 투명", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "민원 업무 시설", + "typhoonLegendCurrent": "현재 중심", + "aedAddress": "주소", + "mapLayerAed": "AED", + "changelogTypePrerelease": "베타", + "reportFilterIntensityInfoModernBody": "진도는 0–4, 5약, 5강, 6약, 6강, 7입니다. 필터는 신제를 따르며, 이전 지진은 목록에서 구제 표기로 표시됩니다.", + "typhoonOverlayWeatherNone": "None", + "mapLayerStyleGray": "그레이스케일(JMA)", + "weatherModeAuto": "자동", + "typhoonLabelProbCircle": "70% probability circle", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "전체 수신", + "displayTheme": "테마", + "mapLayerSatelliteB07": "히마와리 단파 적외(B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "Past movement direction", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "저장한 지역", + "typhoonLegendCone": "예보 원추", + "moreCwaEew": "중앙기상청(CWA) 지진 조기경보", + "onboardingPermsTitle": "권한 허용", + "mapLayerStyleJma": "운정 강조(JMA)", + "rainInterval10m": "10분", + "weatherRankingAnalysisLow": "최저 {value}", + "meshtasticConnectAnyway": "Connect anyway", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "히마와리 근적외(B06)", + "mapLayerSatelliteTransparentReflectance": "낮은 반사율/야간 = 투명,배경 지도 표시", + "chartHourLabel": "{hour}시", + "mapLayerShelter": "대피소", + "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", + "mapLayerSatelliteNdwi": "히마와리 NDWI", + "disasterMapOverlayShelterTooltip": "대피소 표시", + "mapNavHumidity": "습도", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "진도순 정렬", + "homeRainTrendNoData": "데이터 없음", + "mapLayerCategoryRadar": "레이더", + "meshtasticShortName": "Short name", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "히마와리 에어매스", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "경로 상세", + "dataSectionWeather": "기상", + "aedHoursWeekday": "평일 운영시간", + "homeActiveEventsTitle": "발효 중 이벤트", + "weatherRankingAnalysisHigh": "최고 {value}", + "faq": "자주 묻는 질문", + "typhoonHistoryLive": "실시간", + "eewSerial": "제 {serial} 보", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "정렬", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "지진 보고서", + "typhoonNoActive": "활성 태풍 없음", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "히마와리 이산화황/구름상(B11)", + "navEvents": "이벤트", + "onboardingTermsTitle": "서비스 약관", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "읍면동 이름", + "notifySetFailed": "설정을 저장하지 못했습니다. 다시 시도해 주세요.", + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "notifyAnnouncement": "공지사항", + "onboardingIntroTitle": "DPIP에 오신 것을 환영합니다", + "regionCurrentUnavailable": "현재 위치를 가져올 수 없습니다", + "languageSystem": "시스템 기본값", + "skyTimeSunset": "일몰", + "mapLayerSatelliteDust": "히마와리 황사", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "수정", + "weatherDynamicState": "날씨 애니메이션", + "mapPlaceholderDisabled": "지도 (일시 사용 중지)", + "moonNow": "지금", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "겉모습", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "월출·월몰", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "다음 위상", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "달력", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "거리", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "km", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "시직경", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "월출", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "월몰", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "다음 삭", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "종일 지평선 위", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "해당 없음", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "태양", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "일출·박명·절기", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "일조", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "박명", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "빛", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "해시계", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "절기", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "일출", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "일몰", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "남중", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "낮 길이", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "시민", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "항해", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "천문", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "아침 골든아워", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "저녁 골든아워", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "블루아워", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "균시차", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "분", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "다음 절기", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "행성", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "오늘 밤 위치와 밝기", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "현재", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "지평선 위", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "지평선 아래", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "태양에 근접", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "등급", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "이각", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "시간대", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "초저녁", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "새벽", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "거리", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "고도", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "수성", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "금성", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "화성", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "목성", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "토성", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "천왕성", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "해왕성", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "춘분", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "청명", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "곡우", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "입하", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "소만", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "망종", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "하지", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "소서", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "대서", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "입추", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "처서", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "백로", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "추분", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "한로", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "상강", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "입동", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "소설", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "대설", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "동지", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "소한", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "대한", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "입춘", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "우수", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "경칩", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "오늘 밤", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "무엇을 언제 볼 수 있는가", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "관측 가능 시간", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "천문박명 종료", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "완전히 어두워지지 않음", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "암흑 시간대", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "달이 밤새 떠 있음", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "총 암흑 시간", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "달빛", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "유성우", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "복사점이 뜨지 않음", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "개/시", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "위성 통과", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "지금 볼 수 있는 천체", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "사분의자리", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "거문고자리", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "물병자리 에타", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "물병자리 델타", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "페르세우스자리", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "오리온자리", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "황소자리 남", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "사자자리", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "쌍둥이자리", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "작은곰자리", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "산개성단", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "구상성단", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "나선은하", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "타원은하", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "불규칙은하", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "행성상성운", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "초신성 잔해", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "발광성운", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "반사성운", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "성군", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "역법", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "음력과 앞으로의 일식·월식", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "오늘", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "양력", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "음력", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "세차", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app} (기본)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "월 대소", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "좌표 복사", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "30일", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "좌표가 복사되었습니다", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "29일", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "{app}을(를) 열 수 없습니다", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "윤", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "이 기기에서는 전화를 걸 수 없습니다", - - "mapOverlaySectionReference": "참조 레이어", - "mapLayerCategoryEarthquake": "지진", - "mapLayerCategoryTyphoon": "태풍", - "mapLayerCategoryWeather": "기상 관측", - "mapLayerCategorySatellite": "위성", - "mapLayerCategoryRadar": "레이더", - "mapLayerCategoryLife": "생활", - "mapLayerCategoryForecast": "수치 예보", "mapOverlaySectionMap": "지도", - "rainIntervalSection": "집계 시간", - - "mapTownLabels": "읍면동 이름", - "mapTownLabelsHint": "확대하면 읍면동 이름 표시", - - "mapTerrainRelief": "지형 입체감", - "mapTerrainReliefHint": "기본 지도에 지형 음영 표시", - - "dpmSheetEmpty": "지도에서 마커를 눌러 상세 보기", - "dpmAddress": "주소", - "restroomTypeLabel": "유형", - "restroomCategoryLabel": "구분", - "restroomGradeLabel": "등급", - "restroomTypeFemale": "여자 화장실", - "restroomTypeMale": "남자 화장실", - "restroomTypeMixed": "남녀 공용 화장실", - "restroomTypeAccessible": "장애인 화장실", - "restroomTypeGenderNeutral": "성중립 화장실", - "restroomTypeFamily": "가족 화장실", - "restroomTypeUnspecified": "미설정", - "restroomCategoryTransport": "교통", - "restroomCategoryPark": "공원", - "restroomCategoryCommercial": "상업·영업 시설", - "restroomCategoryReligious": "종교·의례 시설", - "restroomCategoryCultural": "문화·여가 시설", - "restroomCategoryGovernment": "민원 업무 시설", - "restroomCategoryWelfare": "사회복지 기관·집회 시설", - "restroomCategoryTourist": "관광 지역·경치 구역", - "restroomCategoryLeisure": "휴양·오락 시설", - "restroomCategoryOther": "기타", - "restroomGradeExcellent": "최우수", - "restroomGradeGood": "우수", - "restroomGradeAverage": "보통", - "restroomGradePoor": "불합격", - "shelterAddressLabel": "주소", - "shelterCapacityLabel": "수용 인원", - "shelterCapacityValue": "{n} 명", - "shelterCategoryLabel": "적용 재해", - "shelterIndoorLabel": "실내 수용", - "shelterOutdoorLabel": "실외 수용", - "shelterVulnerableOkLabel": "취약계층 수용 가능", - "dpmYes": "예", - "dpmNo": "아니요", - "stationSheetEmpty": "관측소를 눌러 관측값 보기", - "monitorDelay": "지연 {value} s", - "monitorWaiting": "데이터 대기 중…", - "mapLegendUnit": "단위: {unit}", - "typhoonLegendPast": "실황 경로", - "typhoonLegendForecast": "예보 경로", - "typhoonLegendForecastPoint": "예보 지점", - "typhoonLegendCurrent": "현재 중심", - "typhoonLegendCone": "예보 원추", - "mapLegendExpand": "범례", - "mapLegendCollapse": "범례 숨기기", - "mapMyLocation": "내 위치", - "mapResetNorth": "북쪽으로 되돌리기", - "typhoonLegendCircle15": "강풍권 (7급)", - "typhoonLegendCircle25": "폭풍권 (10급)", - "typhoonLegendProbability": "내습 확률", - "typhoonLegendWarningAreas": "경보 지역", - "typhoonWarningTitle": "태풍 경보", - "typhoonWarningAreas": "대상 지역: {areas}", - "typhoonTrackDetail": "경로 상세", - "typhoonHistoryTitle": "자료 시각", - "typhoonHistoryLive": "실시간", - "typhoonSatelliteTitle": "위성", - "typhoonDataTime": "Data time\n{time}", - "typhoonForecastLead": "Forecast +{hours} h", - "typhoonIntensityIntense": "Intense typhoon", - "typhoonIntensityMild": "Mild typhoon", - "typhoonIntensityModerate": "Moderate typhoon", - "typhoonIntensityTd": "Tropical depression", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "Tropical depression TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "Past movement direction", - "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", - "typhoonLabelGust": "Peak gust", - "typhoonLabelNe": "NE", - "typhoonLabelNw": "NW", - "typhoonLabelPosition": "Centre location", - "typhoonLabelPressure": "Central pressure", - "typhoonLabelProbCircle": "70% probability circle", - "typhoonLabelSe": "SE", - "typhoonLabelSpeed": "Past movement speed", - "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", - "typhoonLabelSw": "SW", - "typhoonLabelWind": "Max. sustained wind near centre", - "typhoonLegendCircleAvg": "Average circle", - "typhoonOverlayMenuTooltip": "Typhoon overlay options", - "typhoonOverlayProbabilityHint": "Hides the forecast cone", - "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", - "typhoonOverlaySectionExtra": "Overlays", - "typhoonOverlaySectionStorm": "Storm wind", - "typhoonOverlaySectionWeather": "Weather underlay", - "typhoonOverlayStormBandSubtitle": "With average circle", - "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", - "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", - "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", - "typhoonOverlayWeatherHint": "Aligned to bulletin time", - "typhoonOverlayWeatherNone": "None", - "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", - "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", - "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} hPa", - "typhoonValueKm": "{n} km", - "typhoonValueLat": "{lat}°N", - "typhoonValueLon": "{lon}°E", - "typhoonValueMs": "{n} m/s", - "typhoonOverlayForecastCallouts": "Forecast tooltips", - "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", - "dpmFilterSectionRestroom": "시설 유형", - "dpmFilterSectionRestroomType": "화장실 유형", - "dpmFilterSectionShelter": "대피소 재해 유형", - "dpmDisasterFlood": "홍수", - "dpmDisasterEarthquake": "지진", - "dpmDisasterLandslide": "산사태", - "dpmDisasterTsunami": "쓰나미", - "dpmDisasterSlope": "사면 재해", - "dpmDisasterNuclear": "핵 사고", - "skyTime": "하늘 시각", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "월식", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "자동", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "일식", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "여명", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "범위 내 없음", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "일출", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "개기", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "오전", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "부분", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "정오", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "금환", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "오후", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "반영", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "골든아워", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "쥐", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "일몰", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "소", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "땅거미", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "호랑이", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "밤", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "토끼", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "구름 많음", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "용", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "흐림", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "뱀", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "눈", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "말", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "황사", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "양", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "스캔 범위 표시", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "원숭이", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "레이더 4기가 실제로 관측하는 범위를 표시합니다.", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "닭", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "범위 밖 공백은 미관측", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "개", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "레이더 레이어 옵션", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "돼지", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "시·군 경계", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "조석", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "국경", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "사리·조금과 달의 인력", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "각국 국경선", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "천문 기조력만이며 항만 조석표가 아닙니다. 수위는 기상청 발표를 참고하세요.", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "에코 위에 표시", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "현재", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "레이더 에코 아래에서도 경계가 보이도록 합니다.", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "주기", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "읍·면·동 경계", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "사리", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "더 세밀한 구획", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "조금", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "레이더 에코 아래에서도 읍·면·동 경계가 보이도록 합니다.", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "중조", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "정량 강수 예보 레이어 옵션", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "달의 인력", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "바람 예보 레이어 옵션", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "평형 조위", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "바람장 위에 표시", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "m", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "각국 국경선", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "다음 근지점 사리", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "더 촘촘한 망", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "전환점", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "제 {serial} 보", - "eewMaxIntensity": "최대 진도", - "eewLocalIntensity": "현재 위치 예상", - "eewSWave": "S파", - "eewArrived": "도달", - "eewCountdown": "{seconds}초" + "tideHigh": "고", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "저", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "성도", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "머리 위 맨눈으로 보이는 하늘", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "북", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "동", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "남", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "서", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "궤도 요소 {days}일 전", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}{month}월 {day}일", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "진행 중인 유성우 없음", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "48시간 내 가시 통과 없음", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "궤도 데이터를 읽을 수 없음", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "충분히 높은 천체 없음", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "성표를 읽을 수 없음", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 15978aa7d..77b79e2ab 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -1,679 +1,1739 @@ { - "@@locale": "th", - "languageName": "ไทย", - "navHome": "หน้าแรก", - "navEvents": "เหตุการณ์", - "navMap": "แผนที่", - "navData": "ข้อมูล", - "navEarthquake": "แผ่นดินไหว", - "dataSectionSeismic": "แผ่นดินไหว", - "dataEarthquakeSubtitle": "รายงานแผ่นดินไหว", - "dataSectionWeather": "อากาศ", - "dataWeatherRankingSubtitle": "อันดับสถานีแบบเรียลไทม์", - "weatherRankingTitle": "อันดับการสังเกต", - "weatherRankingMeta": "เวลาข้อมูล: {time}\n{count} สถานี", - "weatherRankingEmpty": "ไม่มีข้อมูลให้จัดอันดับ", - "weatherRankingBy": "เรียง", - "weatherRankingHighest": "สูงสุด", - "weatherRankingLowest": "ต่ำสุด", - "weatherRankingMergeTo": "รวม", - "weatherRankingMergeTown": "ตำบล", - "weatherRankingMergeCounty": "อำเภอ/เมือง", - "weatherRankingWind": "ความเร็วลม", - "weatherRankingGust": "ลมกระโชก", + "typhoonValueLat": "{lat}°N", + "onboardingSkipBody": "หากไม่อนุญาตตำแหน่งและการแจ้งเตือน DPIP จะไม่สามารถแจ้งเตือนแผ่นดินไหวและภัยพิบัติใกล้คุณแบบเรียลไทม์ได้ คุณยังสามารถเปิดใช้ภายหลังได้ในการตั้งค่า", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 ชม.", + "homeRainTrendHeavyStopping": "คาดว่าฝนตกหนักจะหยุดในอีก {minutes} นาที", + "mapTimelineObserved": "เวลาตรวจวัด", + "regionSelectTitle": "เลือกพื้นที่", + "skyTimeNoon": "เที่ยงวัน", + "radarCountyOutlineSubtitle": "ทำให้เส้นแบ่งเขตยังอ่านออกใต้ภาพเอคโคเรดาร์", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "ประเภทห้องน้ำ", + "mapLayerSatelliteB03": "Himawari Red (B03)", + "reportFilterIntensity": "ความเข้ม", + "mapLayerLightning": "ฟ้าผ่า", + "restroomTypeMale": "ห้องน้ำชาย", + "meshtasticLastReceived": "Last received", + "reportDetailSortByCounty": "เรียงตามพื้นที่", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "อาจมีฝนตกประปราย", + "meshtasticUptime": "Uptime", "weatherRankingTempExtremes": "ค่าสุดขั้วอุณหภูมิ", - "weatherRankingExtremeHigh": "สูงสุดวันนี้", - "weatherRankingExtremeLow": "ต่ำสุดวันนี้", + "themeLight": "สว่าง", + "mapTerrainReliefHint": "แสดงความนูนของภูมิประเทศบนแผนที่ฐาน", + "meshtasticEmptyMessage": "(empty message)", + "moreSectionRegion": "พื้นที่", + "dpmDisasterEarthquake": "แผ่นดินไหว", + "mapLayerSatellite": "Himawari Infrared (B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "เวลาวันเสาร์", + "dpmDisasterSlope": "ภัยพิบัติลาดชัน", + "moonPhaseNew": "New moon", + "notifySectionEew": "การเตือนแผ่นดินไหวล่วงหน้า", + "mapResetNorth": "กลับไปทางเหนือ", + "rainInterval2d": "2 วัน", + "mapTownLabelsHint": "แสดงชื่อตำบลเมื่อขยายแผนที่", + "commonCancel": "Cancel", + "notifyOptTsunamiWarning": "เฉพาะการเตือนภัยสึนามิ", + "mapLayerSatelliteBtdFog": "Himawari Night Fog", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "ขั้นสูง", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "ช่วงวัน", + "notifySettingsMenu": "การตั้งค่าการแจ้งเตือน", + "typhoonHistoryTitle": "เวลาข้อมูล", + "mapAppDefault": "{app} (ค่าเริ่มต้น)", + "trendRange24h": "24 ชม.", + "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", "weatherRankingRecordedAt": "บันทึกเมื่อ {time}", - "weatherRankingAnalysisCurrent": "ปัจจุบัน {value}°C", - "weatherRankingAnalysisHigh": "สูง {value}", - "weatherRankingAnalysisLow": "ต่ำ {value}", - "weatherRankingAnalysisRange": "ช่วง {value}°C", - "reportListEmpty": "ไม่มีรายงานแผ่นดินไหว", - "reportListEmptyFiltered": "ไม่มีรายงานที่ตรงกับเงื่อนไข", - "reportListMeta": "M{magnitude} · {depth} km", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "km", - "reportListLocalFelt": "รู้สึกในพื้นที่", - "reportListToday": "วันนี้", - "reportListYesterday": "เมื่อวาน", - "reportListDayCount": "{count}", - "reportListEnd": "สิ้นสุดรายการ", - "reportFilterTitle": "ตัวกรอง", - "reportFilterSort": "เรียงลำดับ", - "reportFilterSortTime": "เวลา", - "reportFilterSortIntensity": "ความเข้ม", - "reportFilterSortMagnitude": "ขนาด", - "reportFilterSortDepth": "ความลึก", + "mapLayerRain": "ปริมาณฝน", + "mapLayerQpesums": "พยากรณ์ฝน 1 ชั่วโมงข้างหน้า", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "แผนที่", + "mapTerrainRelief": "ความนูนของภูมิประเทศ", + "eewMaxIntensity": "ความรุนแรงสูงสุด", + "mapLegendCollapse": "ซ่อนคำอธิบาย", + "changelogTitle": "บันทึกการอัปเดต", "reportFilterOrderDesc": "มาก→น้อย", - "reportFilterOrderAsc": "น้อย→มาก", - "reportFilterIntensity": "ความเข้ม", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", "reportFilterIntensityInfoTitle": "มาตรวัดความรุนแรงแบบใหม่/เก่า", - "reportFilterIntensityInfoIntro": "CWA เปลี่ยนมาตรวัดเมื่อ 1 ม.ค. 2020 (เวลาไทเป)", - "reportFilterIntensityInfoLegacyTitle": "แบบเก่า (ก่อน 2020)", - "reportFilterIntensityInfoLegacyBody": "มีระดับ 0–7 เท่านั้น ไม่แยก 5−/5+/6−/6+", - "reportFilterIntensityInfoModernTitle": "แบบใหม่ (ตั้งแต่ 2020)", - "reportFilterIntensityInfoModernBody": "ระดับ 0–4, 5−, 5+, 6−, 6+, 7 แถบตัวกรองใช้แบบใหม่ เหตุการณ์เก่าในรายการยังแสดงป้ายแบบเก่า", - "reportFilterMagnitude": "ขนาด", - "reportFilterDepth": "ความลึก", - "reportFilterDepthKm": "{depth} km", - "reportFilterDate": "วันที่", - "reportFilterDatePick": "เลือกวันที่", - "reportFilterDateStartNote": "วันเริ่ม: 00:00 ของวันนั้น(ไทเป)", + "mapLayerTyphoon": "ไต้ฝุ่น", + "radarOverlayMenuTooltip": "ตัวเลือกชั้นเรดาร์", + "mapMyLocation": "ตำแหน่งของฉัน", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "Nodes", + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", + "aedType": "ประเภท", + "termsOfService": "ข้อกำหนดในการให้บริการ", + "typhoonLegendCircle25": "วงพายุ (รุนแรง)", + "sponsorTitle": "สนับสนุน DPIP", + "mapNavSatellite": "ดาวเทียม", + "homeRainTrendUpdated": "อัปเดต {time}", + "onboardingNext": "ถัดไป", + "weatherRankingMergeTown": "ตำบล", + "mapLayerMonitor": "เครื่องตรวจแผ่นดินไหว", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "แบบสมัครสมาชิก", + "typhoonValueLon": "{lon}°E", + "skyTime": "เวลาท้องฟ้า", + "weatherModeCloudy": "มีเมฆมาก", + "skyTimeDusk": "สนธยา", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "วันสิ้นสุด: 24:00 ของวันนั้น(ไทเป)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "สถานที่", - "reportFilterLocationHint": "เช่น ฮวาเหลียน", - "reportFilterAny": "ทั้งหมด", - "reportFilterApply": "ใช้", - "reportFilterReset": "รีเซ็ต", - "reportListSearch": "ค้นหา", - "reportDetailTitle": "รายงานแผ่นดินไหว", - "reportDetailNumbered": "แผ่นดินไหวรู้สึกได้อย่างมีนัยสำคัญ หมายเลข {number}", - "reportDetailLocalFelt": "แผ่นดินไหวรู้สึกได้เฉพาะพื้นที่", - "reportDetailInfo": "รายละเอียด", - "reportDetailOriginTime": "เวลาเกิดเหตุ", - "reportDetailEpicenter": "พิกัดศูนย์กลาง", - "reportDetailMagnitude": "ขนาดแผ่นดินไหว", - "reportDetailDepth": "ความลึกจุดศูนย์กลาง", - "reportDetailAreaIntensity": "ความเข้มแยกตามพื้นที่", - "reportDetailLocalIntensity": "ความเข้มที่ตำแหน่งของคุณ", - "reportDetailLocalIntensityUnavailable": "ไม่มีข้อมูลความเข้ม", - "reportDetailSortByIntensity": "เรียงตามความเข้ม", - "reportDetailSortByCounty": "เรียงตามพื้นที่", - "reportDetailImage": "ภาพรายงานแผ่นดินไหว", - "reportDetailImageUnavailable": "ยังไม่มีภาพรายงาน", - "reportDetailOpenReport": "หน้ารายงาน", - "reportDetailReplay": "เล่นย้อนหลัง", - "navMore": "เพิ่มเติม", - "appLogs": "บันทึกแอป", - "changelogTitle": "บันทึกการอัปเดต", - "changelogEmpty": "ยังไม่มีบันทึกการเผยแพร่", - "changelogTypePrerelease": "เบต้า", - "changelogTypeStable": "ทางการ", - "changelogCurrentVersion": "ปัจจุบัน", - "changelogVersionDetails": "รายละเอียดเวอร์ชัน", - "changelogBodyEmpty": "ไม่มีคำอธิบายสำหรับรุ่นนี้", - "mapPlaceholderDisabled": "แผนที่ (ปิดใช้งานชั่วคราว)", - "moreSectionRegion": "พื้นที่", - "moreSectionNotify": "การแจ้งเตือน", - "moreSectionDisplay": "การแสดงผล", - "regionManageTitle": "พื้นที่ที่ใช้บ่อย", - "regionAddButton": "เพิ่มพื้นที่", - "regionEmpty": "ยังไม่มีพื้นที่ที่บันทึกไว้", - "regionSelectTitle": "เลือกพื้นที่", - "regionSelectCount": "เลือกแล้ว {count}/{max}", - "regionSelectFull": "บันทึกได้สูงสุด {max} พื้นที่", - "regionEdit": "แก้ไข", - "moreSectionAdvanced": "ขั้นสูง", - "moreDeveloper": "ข้อมูลดีบัก", - "experimentalFeatures": "ฟีเจอร์ทดลอง", - "moreSectionLinks": "ลิงก์ที่เกี่ยวข้อง", - "moreCwaEew": "การเตือนแผ่นดินไหวล่วงหน้าของกรมอุตุนิยมวิทยากลาง (CWA)", - "moreTremReport": "รายงานการตรวจจับ TREM", - "moreServerStatus": "สถานะเซิร์ฟเวอร์", - "moreAnnouncements": "ประกาศ", - "moreDiscord": "ชุมชน Discord", - "moreNotifyLog": "บันทึกการส่งการแจ้งเตือนของ DPIP", - "moreLinkOpenFailed": "ไม่สามารถเปิดลิงก์ได้", - "weatherDynamicState": "แอนิเมชันสภาพอากาศ", - "weatherDynamicStateSubtitle": "แทนที่สภาพอากาศพื้นหลังหน้าแรก", - "weatherModeAuto": "อัตโนมัติ", - "weatherModeClear": "ท้องฟ้าแจ่มใส", - "weatherModeRain": "ฝนตก", - "weatherModeFog": "หมอกหนา", - "weatherModeThunderstorm": "พายุฝนฟ้าคะนอง", - "commonLoading": "กำลังโหลด…", - "commonRetry": "ลองอีกครั้ง", - "commonError": "เกิดข้อผิดพลาด", - "commonFetchFailed": "ไม่สามารถโหลดข้อมูลได้ โปรดลองอีกครั้ง", - "commonEmpty": "ไม่มีข้อมูล", - "feedConnecting": "กำลังเชื่อมต่อ…", - "feedStale": "ข้อมูลอาจล้าสมัย", - "feedOffline": "การเชื่อมต่อขาดหาย", - "eewTitle": "การเตือนแผ่นดินไหวล่วงหน้า", - "eewNone": "ขณะนี้ไม่มีการเตือนแผ่นดินไหวล่วงหน้า", - "eewSummary": "ขนาด {magnitude} · ความลึก {depth} กม.", - "regionNationwide": "ทั่วประเทศ", - "regionCurrent": "ตำแหน่งปัจจุบัน", - "regionCurrentUnavailable": "ไม่สามารถระบุตำแหน่งปัจจุบันได้", - "weatherPrecipitation": "ปริมาณน้ำฝน", - "weatherHumidity": "ความชื้น", - "weatherDataTime": "{station} · เวลาข้อมูล {time}", - "homeViewOnMap": "ดูบนแผนที่", - "homeForecastTitle": "พยากรณ์ 24 ชั่วโมง", + "meshtasticSilent": "Silent", + "reportFilterSortMagnitude": "ขนาด", + "mapLayerCategoryEarthquake": "แผ่นดินไหว", + "mapLayerSatelliteB12": "Himawari Ozone (B12)", + "typhoonLegendPast": "เส้นทางจริง", + "restroomCategoryOther": "อื่น ๆ", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "สูง {high}° · ต่ำ {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "รู้สึกเหมือน {temp}°", - "homeForecastHumidity": "ความชื้น {value}%", - "homeForecastWind": "{direction} · แรง {level}", - "homeForecastUnavailable": "เลือกพื้นที่เพื่อดูพยากรณ์", - "homeForecastEmpty": "ไม่มีข้อมูลพยากรณ์", - "homeActiveEventsTitle": "เหตุการณ์ที่ยังมีผล", - "homeActiveEventsEmpty": "ไม่มีเหตุการณ์ที่ยังมีผล", - "homeRainTrendTitle": "ฝนชั่วโมงถัดไป", - "homeRainTrendMinute": "{minute} นาที", - "homeRainTrendUpdated": "อัปเดต {time}", - "homeRainTrendNoData": "ไม่มีข้อมูล", - - "homeRainTrendScattered": "อาจมีฝนตกประปราย", - "homeRainTrendLightSustained": "ฝนตกเล็กน้อยต่อเนื่องตลอดชั่วโมงหน้า", - "homeRainTrendLightStopping": "คาดว่าฝนจะหยุดในอีก {minutes} นาที", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "เปิดการตั้งค่า", + "mapLegendExpand": "คำอธิบาย", + "eewNone": "ขณะนี้ไม่มีการเตือนแผ่นดินไหวล่วงหน้า", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "ข่าวสารและการเตือนภัยสึนามิ", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "Node options", + "onboardingAgreeContinue": "ยอมรับและดำเนินการต่อ", + "meshtasticNodeId": "Node ID", + "commonRetry": "ลองอีกครั้ง", + "reportDetailNumbered": "แผ่นดินไหวรู้สึกได้อย่างมีนัยสำคัญ หมายเลข {number}", + "typhoonOverlayStormBandSubtitle": "With average circle", + "disasterMapOverlayRestroomTooltip": "แสดงห้องน้ำสาธารณะ", + "weatherRankingTitle": "อันดับการสังเกต", "homeRainTrendHeavySustained": "ฝนตกหนักต่อเนื่องตลอดชั่วโมงหน้า", - "homeRainTrendHeavyStopping": "คาดว่าฝนตกหนักจะหยุดในอีก {minutes} นาที", - "mapLayers": "ชั้นข้อมูล", - "mapLayerOrderTitle": "จัดเรียงเลเยอร์", - "mapLayerOrderReset": "รีเซ็ตลำดับ", - "mapLayerRadar": "เรดาร์สะท้อนสังเคราะห์", - "mapLayerSatellite": "Himawari Infrared (B13)", - "mapLayerSatelliteB01": "Himawari Blue (B01)", - "mapLayerSatelliteB02": "Himawari Green (B02)", - "mapLayerSatelliteB03": "Himawari Red (B03)", - "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "notifySectionTsunami": "สึนามิ", + "restroomCategoryPark": "สวนสาธารณะ", + "moreLinkOpenFailed": "ไม่สามารถเปิดลิงก์ได้", + "themeDark": "มืด", + "sponsorRestore": "กู้คืนการซื้อ", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "Traffic", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", + "disasterMapOverlayAedTooltip": "แสดงตำแหน่ง AED", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "ความชื้น", + "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", + "meshtasticScanning": "Scanning…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "บันทึกได้สูงสุด {max} พื้นที่", + "meshtasticTitle": "Meshtastic", + "navMore": "เพิ่มเติม", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "ชั้น", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "Himawari Near-Infrared (B05)", - "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", - "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", - "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", - "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", - "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", - "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", - "mapLayerSatelliteB12": "Himawari Ozone (B12)", - "mapLayerSatelliteB13": "Himawari Infrared (B13)", - "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", - "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", - "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "NE", + "meshtasticCopied": "Message copied", + "reportListEmpty": "ไม่มีรายงานแผ่นดินไหว", + "reportListEnd": "สิ้นสุดรายการ", "mapLayerSatelliteTruecolor": "Himawari True Color", - "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", - "mapLayerSatelliteAsh": "Himawari Ash", - "mapLayerSatelliteDust": "Himawari Dust", - "mapLayerSatelliteAirmass": "Himawari Airmass", - "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", - "mapLayerSatelliteWatervapor": "Himawari Water Vapour", - "mapLayerSatelliteBtdSplit": "Himawari Split Window", - "mapLayerSatelliteBtdFog": "Himawari Night Fog", - "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", - "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", - "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", - "mapLayerSatelliteBtdOzone": "Himawari Tropopause", - "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", - "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", - "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", - "mapLayerSatelliteNdvi": "Himawari NDVI", - "mapLayerSatelliteNdwi": "Himawari NDWI", - "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionExtra": "Overlays", + "eewSWave": "คลื่น S", + "meshtasticBusyTitle": "Another app is using this radio", + "restroomCategoryCultural": "สถานที่ทางวัฒนธรรม", + "typhoonLabelWind": "Max. sustained wind near centre", + "radarGlobalOutlineHint": "กรอบนอกของทุกประเทศ", + "notifyEvacuation": "ข้อมูลภัยพิบัติ", + "typhoonLegendCircle15": "วงพายุ (แรง)", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "Astronomy", + "homeRainTrendLightSustained": "ฝนตกเล็กน้อยต่อเนื่องตลอดชั่วโมงหน้า", + "commonError": "เกิดข้อผิดพลาด", + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "ตอนนี้", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "หน้ารายงาน", + "trendRange7d": "7 วัน", + "typhoonWarningAreas": "พื้นที่: {areas}", + "rainIntervalSection": "ช่วงเวลา", + "notifyTitle": "การแจ้งเตือน", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "หมวดหมู่", + "sponsorRestoring": "กำลังกู้คืนการซื้อ…", + "sponsorIntro": "DPIP มุ่งมั่นให้ข้อมูลการป้องกันภัยพิบัติแบบเรียลไทม์ โดยไม่มีโฆษณาหรือรูปแบบหารายได้อื่น การสนับสนุนของคุณช่วยให้เรารักษาเซิร์ฟเวอร์และพัฒนาต่อไปได้", + "shelterAddressLabel": "ที่อยู่", + "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "สถานประกอบการพาณิชย์", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "พื้นที่", + "homeRainTrendLightStopping": "คาดว่าฝนจะหยุดในอีก {minutes} นาที", + "reportDetailInfo": "รายละเอียด", + "mapNavWind": "ทิศลม", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "ตัวเลือกชั้นพยากรณ์ลม", + "dataWeatherRankingSubtitle": "อันดับสถานีแบบเรียลไทม์", + "rainInterval6h": "6 ชม.", + "homeRainTrendMinute": "{minute} นาที", + "restroomTypeUnspecified": "ไม่ระบุ", + "typhoonOverlayProbabilityHint": "Hides the forecast cone", "mapLayerSatelliteGlobalOutline": "Country border", - "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", - "mapLayerSatelliteCloudClear": "Clear", - "mapLayerSatelliteCloudProbablyClear": "Probably clear", - "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "mapNavTemperature": "อุณหภูมิ", + "typhoonLegendForecastPoint": "จุดพยากรณ์", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "เมื่อวาน", + "moreSectionLinks": "ลิงก์ที่เกี่ยวข้อง", + "feedOffline": "การเชื่อมต่อขาดหาย", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "การแสดงผล", + "rainInterval3d": "3 วัน", + "defaultMapLayerSubtitle": "แท็บแผนที่จะเปิดชั้นนี้ ไอคอนและป้ายนำทางด้านล่างจะเปลี่ยนตาม", + "aedDescription": "หมายเหตุ", + "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", + "onboardingPermLocationDesc": "ส่งการเตือนภัยตามตำแหน่งที่คุณอยู่", + "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "ไม่มีเหตุการณ์ที่ยังมีผล", + "typhoonLabelPosition": "Centre location", + "weatherRankingBy": "เรียง", + "typhoonIntensityMild": "Mild typhoon", + "windForecastGlobalOutlineHint": "กรอบนอกของทุกประเทศ", + "rainInterval1h": "1 ชม.", + "eewLocalIntensity": "ประมาณ ณ ตำแหน่ง", + "mapLayerRadar": "เรดาร์สะท้อนสังเคราะห์", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "สถานที่ทางศาสนา", + "meshtasticRole": "Role", "mapLayerSatelliteCloudCloudy": "Cloudy", - "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", - "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", - "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", - "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", - "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", - "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", - "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "skyTimeSunrise": "พระอาทิตย์ขึ้น", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "No messages yet", + "onboardingPermNotifyDesc": "ส่งการเตือนแผ่นดินไหว สภาพอากาศ และภัยพิบัติทันทีที่เกิดขึ้น", + "radarTownOutline": "เส้นแบ่งเขตอำเภอ", "mapLayerStyleSection": "Colour style", - "mapLayerStyleTooltip": "Colour style", - "mapLayerStyleGray": "Grayscale (JMA)", - "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", - "mapLayerStyleJma": "Cloud-top enhancement (JMA)", - "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", - "mapLayerQpesums": "พยากรณ์ฝน 1 ชั่วโมงข้างหน้า", - "mapLayerLightning": "ฟ้าผ่า", - "lightningLegendCg": "เมฆสู่พื้น · {minutes} นาที", - "lightningLegendCc": "เมฆสู่เมฆ · {minutes} นาที", - "mapTimelineNow": "ตอนนี้", - "mapTimelinePast": "อดีต", - "mapTimelineFuture": "อนาคต", - "mapTimelineObserved": "เวลาตรวจวัด", - "mapTimelineForecast": "พยากรณ์", - "mapTimelineDataTime": "เวลาข้อมูล {time}", - "notifySettingsMenu": "การตั้งค่าการแจ้งเตือน", - "notifyTitle": "การแจ้งเตือน", - "notifyUnavailable": "การแจ้งเตือนแบบพุชยังไม่พร้อม — โปรดลองอีกครั้งในภายหลัง", - "notifySetFailed": "ไม่สามารถบันทึกการตั้งค่าได้ โปรดลองอีกครั้ง", - "notifySectionEew": "การเตือนแผ่นดินไหวล่วงหน้า", - "notifySectionEarthquake": "แผ่นดินไหว", - "notifySectionWeather": "สภาพอากาศ", - "notifySectionTsunami": "สึนามิ", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "ชั้นแผนที่ป้องกันภัย", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "SW", + "typhoonForecastLead": "Forecast +{hours} h", + "dpmDisasterTsunami": "สึนามิ", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "ทางการ", + "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "เลเยอร์อ้างอิง", + "mapLayerSatelliteB02": "Himawari Green (B02)", + "reportListLocalFelt": "รู้สึกในพื้นที่", + "weatherRankingEmpty": "ไม่มีข้อมูลให้จัดอันดับ", "notifySectionOther": "อื่น ๆ", - "notifyEew": "การเตือนแผ่นดินไหวฉุกเฉิน", - "notifyMonitor": "เครื่องเฝ้าระวังการสั่นสะเทือนรุนแรง", - "notifyReport": "รายงานแผ่นดินไหว", - "notifyIntensity": "รายงานความรุนแรงแผ่นดินไหว", - "notifyThunderstorm": "การแจ้งเตือนพายุฝนฟ้าคะนอง", - "notifyAdvisory": "การแจ้งเตือนและประกาศสภาพอากาศ", - "notifyEvacuation": "ข้อมูลภัยพิบัติ", - "notifyTsunami": "ข้อมูลสึนามิ", - "notifyAnnouncement": "ประกาศ", - "notifyOptOff": "ปิด", - "notifyOptAll": "รับทั้งหมด", + "weatherRankingMeta": "เวลาข้อมูล: {time}\n{count} สถานี", + "onboardingTermsAgree": "ฉันได้อ่านและยอมรับข้อกำหนดการให้บริการแล้ว", + "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", "notifyOptLocalIntensity4": "ความรุนแรงในพื้นที่ระดับ 4 ขึ้นไป", - "notifyOptLocalIntensity1": "ความรุนแรงในพื้นที่ระดับ 1 ขึ้นไป", - "notifyOptWeatherLocal": "เฉพาะตำแหน่งปัจจุบัน", - "notifyOptTsunamiWarning": "เฉพาะการเตือนภัยสึนามิ", - "notifyOptTsunamiAll": "ข่าวสารและการเตือนภัยสึนามิ", - "onboardingNext": "ถัดไป", - "onboardingBack": "ย้อนกลับ", + "eewArrived": "มาถึงแล้ว", + "meshtasticNoDevices": "No Meshtastic devices found", + "mapLayerCategoryLife": "ชีวิตประจำวัน", + "reportFilterSortIntensity": "ความเข้ม", + "typhoonMotion": "เคลื่อนที่", + "meshtasticStateDisconnected": "Disconnected", + "typhoonIntensityIntense": "Intense typhoon", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "จัดเรียงเลเยอร์", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "ใช่", + "meshtasticNoHistory": "Not enough history yet", + "reportDetailLocalIntensityUnavailable": "ไม่มีข้อมูลความเข้ม", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "ความลึก", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "เลื่อนลงเพื่อดำเนินการต่อ", - "onboardingIntroTitle": "ยินดีต้อนรับสู่ DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "พยากรณ์", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "แผนที่", + "notifyAdvisory": "การแจ้งเตือนและประกาศสภาพอากาศ", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "รีเซ็ต", + "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionStorm": "Storm wind", + "moonPhaseFull": "Full moon", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "weatherDynamicStateSubtitle": "แทนที่สภาพอากาศพื้นหลังหน้าแรก", + "reportFilterIntensityInfoModernTitle": "แบบใหม่ (ตั้งแต่ 2020)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "Data time\n{time}", + "restroomTypeAccessible": "ห้องน้ำคนพิการ", + "moreSectionAbout": "เกี่ยวกับ", + "meshtasticSelectDevice": "Select a radio", "onboardingIntroBody": "DPIP คือเพื่อนคู่ใจด้านการป้องกันภัยพิบัติของคุณ รวมการเตือนแผ่นดินไหวล่วงหน้า รายงานแผ่นดินไหว สภาพอากาศ และข้อมูลภัยพิบัติต่าง ๆ ไว้ในที่เดียว และแจ้งเตือนคุณในช่วงเวลาสำคัญ\n\n• แผ่นดินไหว: การเตือนล่วงหน้า รายงานความรุนแรง และรายงานฉบับสมบูรณ์\n• สภาพอากาศ: ข้อความพายุฝนฟ้าคะนองแบบเรียลไทม์ และการแจ้งเตือนสภาพอากาศ\n• ข้อมูลสึนามิและภัยพิบัติ\n\nต่อไป เราจะขอให้คุณอ่านข้อกำหนดการให้บริการ และอนุญาตสิทธิ์บางอย่างเพื่อให้ DPIP สามารถปกป้องคุณได้แบบเรียลไทม์", - "onboardingTermsTitle": "ข้อกำหนดการให้บริการ", - "onboardingTermsBody": "โปรดอ่านข้อควรทราบต่อไปนี้ก่อนใช้งาน DPIP:\n\n• ข้อมูลทั้งหมดควรยึดตามเนื้อหาที่เผยแพร่โดยกรมอุตุนิยมวิทยากลาง (CWA) เป็นหลัก\n\n• ขึ้นอยู่กับสภาพเครือข่าย เซิร์ฟเวอร์ แอปพลิเคชัน และแหล่งข้อมูลต้นทาง อาจมีความเป็นไปได้ที่จะไม่ได้รับข้อมูล เราพยายามอย่างเต็มที่เพื่อหลีกเลี่ยงกรณีเช่นนี้ แต่ไม่สามารถรับประกันได้ว่าจะไม่เกิดขึ้น\n\n• การสั่นสะเทือนอย่างรุนแรงอาจมาถึงตำแหน่งของคุณก่อนการแจ้งเตือน\n\n• การเตือนแผ่นดินไหวล่วงหน้าเป็นผลจากการคำนวณอย่างรวดเร็ว ซึ่งอาจมีความคลาดเคลื่อนสูง โปรดทำความเข้าใจและใช้งานด้วยความระมัดระวัง\n\n• พฤติกรรมใด ๆ ที่ไม่ได้รับการรับรองจากหน่วยงานราชการอาจมีความเสี่ยงทางกฎหมาย โปรดปฏิบัติตามระเบียบที่เกี่ยวข้องทั้งหมด\n\nนอกจากนี้ เพื่อให้บริการการเตือนภัยเฉพาะพื้นที่ บริการนี้จะเก็บรวบรวมและอัปโหลดตำแหน่งโดยประมาณและตัวระบุการแจ้งเตือนแบบพุชของคุณ — ทั้งขณะทำงานเบื้องหน้าและเบื้องหลัง — เพื่อใช้ตัดสินว่าจะส่งการเตือนใดให้คุณเท่านั้น\n\nการแตะ \"ยอมรับและดำเนินการต่อ\" ถือว่าคุณได้อ่าน เข้าใจ และยอมรับข้อความข้างต้นแล้ว", - "onboardingTermsAgree": "ฉันได้อ่านและยอมรับข้อกำหนดการให้บริการแล้ว", - "onboardingAgreeContinue": "ยอมรับและดำเนินการต่อ", - "onboardingPermsTitle": "การอนุญาตสิทธิ์", - "onboardingPermsBody": "เพื่อให้ DPIP แจ้งเตือนคุณได้ในทันทีที่เกิดภัยพิบัติ โปรดอนุญาตสิทธิ์ต่อไปนี้ คุณสามารถเปลี่ยนแปลงได้ทุกเมื่อในการตั้งค่าระบบ", + "shelterCapacityLabel": "ความจุ", + "reportDetailImage": "ภาพรายงานแผ่นดินไหว", + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", "onboardingPermNotify": "การแจ้งเตือน", - "onboardingPermNotifyDesc": "ส่งการเตือนแผ่นดินไหว สภาพอากาศ และภัยพิบัติทันทีที่เกิดขึ้น", - "onboardingPermCritical": "การแจ้งเตือนสำคัญ", - "onboardingPermCriticalDesc": "ให้การเตือนแผ่นดินไหวที่เป็นอันตรายถึงชีวิตส่งเสียงได้ แม้อยู่ในโหมดเงียบหรือโหมดห้ามรบกวน", - "onboardingPermLocation": "ตำแหน่งที่ตั้ง", - "onboardingPermLocationDesc": "ส่งการเตือนภัยตามตำแหน่งที่คุณอยู่", - "onboardingPermBackground": "ตำแหน่งที่ตั้งเบื้องหลัง", - "onboardingPermBackgroundDesc": "อนุญาต \"ทุกครั้ง\" เพื่อให้การเตือนภัยยังส่งถึงคุณได้แม้ปิดแอป", - "onboardingPermBattery": "ยกเว้นการประหยัดแบตเตอรี่", - "onboardingPermBatteryDesc": "อนุญาตให้ DPIP ทำงานเบื้องหลังอย่างต่อเนื่อง เพื่อไม่ให้การเตือนภัยล่าช้าหรือพลาดไป", - "onboardingGrant": "อนุญาต", - "onboardingGranted": "อนุญาตแล้ว", - "onboardingStart": "เริ่มใช้งาน", - "language": "ภาษา", - "languageSettings": "ภาษา", - "languageSystem": "ค่าเริ่มต้นของระบบ", - "locationBannerServiceOff": "บริการระบุตำแหน่งถูกปิด — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้", - "locationBannerPermission": "ยังไม่ได้อนุญาตสิทธิ์ตำแหน่งที่ตั้ง — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้", - "locationBannerFix": "เปิดการตั้งค่า", - "notifyBannerDisabled": "ปิดการแจ้งเตือนอยู่ — คุณจะไม่ได้รับการเตือนภัยพิบัติ", - "onboardingSkipTitle": "ยังไม่ได้ให้สิทธิ์", - "onboardingSkipBody": "หากไม่อนุญาตตำแหน่งและการแจ้งเตือน DPIP จะไม่สามารถแจ้งเตือนแผ่นดินไหวและภัยพิบัติใกล้คุณแบบเรียลไทม์ได้ คุณยังสามารถเปิดใช้ภายหลังได้ในการตั้งค่า", - "onboardingSkipStay": "กลับไปให้สิทธิ์", - "onboardingSkipLeave": "ข้ามไปก่อน", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "Clear messages", + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "ชั้นแผนที่เริ่มต้น", + "moreSectionNotify": "การแจ้งเตือน", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "การแจ้งเตือนแบบพุชยังไม่พร้อม — โปรดลองอีกครั้งในภายหลัง", + "mapLayerOrderReset": "รีเซ็ตลำดับ", + "dpmAddress": "ที่อยู่", + "weatherRankingMergeCounty": "อำเภอ/เมือง", + "moreSectionApp": "ดาวน์โหลดแอป", + "reportFilterIntensityInfoLegacyBody": "มีระดับ 0–7 เท่านั้น ไม่แยก 5−/5+/6−/6+", + "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", + "qpesumsOverlayMenuTooltip": "ตัวเลือกชั้นพยากรณ์น้ำฝน", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "อนาคต", + "typhoonLegendCircleAvg": "Average circle", + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "SE", + "radarTownOutlineHint": "เส้นแบ่งย่อยกว่า", + "eewCountdown": "{seconds} วินาที", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "Peak gust", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "ข้อกำหนดการใช้งาน", + "restroomTypeGenderNeutral": "ห้องน้ำเป็นกลางทางเพศ", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "การแจ้งเตือนพายุฝนฟ้าคะนอง", + "skyTimeGolden": "ช่วงเวลาทอง", + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "ปัจจุบัน {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "เลือกพื้นที่เพื่อดูพยากรณ์", + "mapLayers": "ชั้นข้อมูล", + "meshtasticHardware": "Hardware", + "languageSettings": "ภาษา", + "dpmDisasterNuclear": "อุบัติเหตุนิวเคลียร์", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "ภาษา", + "homeForecastFeelsLike": "รู้สึกเหมือน {temp}°", + "typhoonOverlayWeatherHint": "Aligned to bulletin time", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "รุ่งอรุณ", + "skyTimeAfternoon": "ตอนบ่าย", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "ประกาศเตือนไต้ฝุ่น", "moreSourceCode": "ซอร์สโค้ด", - "moreSectionApp": "ดาวน์โหลดแอป", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "การแสดงผล", - "defaultMapLayerSettings": "ชั้นแผนที่เริ่มต้น", - "defaultMapLayerSubtitle": "แท็บแผนที่จะเปิดชั้นนี้ ไอคอนและป้ายนำทางด้านล่างจะเปลี่ยนตาม", - "mapNavRadar": "เรดาร์", - "mapNavQpesums": "พยากรณ์", - "mapNavSatellite": "ดาวเทียม", - "mapNavLightning": "ฟ้าผ่า", - "mapNavTyphoon": "ไต้ฝุ่น", + "mapLayerCategoryWeather": "การสังเกตสภาพอากาศ", + "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", + "windForecastTownOutlineHint": "ตาข่ายที่ละเอียดกว่า", + "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", + "mapAppCopyCoordinates": "คัดลอกพิกัด", + "reportFilterIntensityInfoIntro": "CWA เปลี่ยนมาตรวัดเมื่อ 1 ม.ค. 2020 (เวลาไทเป)", "mapNavEarthquake": "แผ่นดินไหว", - "mapNavTemperature": "อุณหภูมิ", - "mapNavHumidity": "ความชื้น", - "mapNavPressure": "ความกดอากาศ", - "mapNavWind": "ทิศลม", + "typhoonGust": "ลมกระโชก", + "restroomGradeAverage": "ปานกลาง", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", + "onboardingPermBackgroundDesc": "อนุญาต \"ทุกครั้ง\" เพื่อให้การเตือนภัยยังส่งถึงคุณได้แม้ปิดแอป", + "mapTimelineForecast": "พยากรณ์", + "restroomTypeLabel": "ประเภท", + "navEarthquake": "แผ่นดินไหว", + "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "รายงานแผ่นดินไหว", + "moreTremReport": "รายงานการตรวจจับ TREM", + "weatherDataTime": "{station} · เวลาข้อมูล {time}", + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "radarCountyOutline": "เส้นแบ่งเขตจังหวัด", + "onboardingGranted": "อนุญาตแล้ว", + "@mapAppCopyCoordinates": {}, + "commonClose": "ปิด", + "restroomGradeLabel": "ระดับ", + "rainIntervalNow": "วันนี้", + "changelogCurrentVersion": "ปัจจุบัน", + "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", + "typhoonLabelPressure": "Central pressure", + "aedOpenRemark": "หมายเหตุเวลาเปิด", + "onboardingPermsBody": "เพื่อให้ DPIP แจ้งเตือนคุณได้ในทันทีที่เกิดภัยพิบัติ โปรดอนุญาตสิทธิ์ต่อไปนี้ คุณสามารถเปลี่ยนแปลงได้ทุกเมื่อในการตั้งค่าระบบ", + "typhoonOverlaySectionWeather": "Weather underlay", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "เฉพาะตำแหน่งปัจจุบัน", "mapNavRain": "ฝน", - "mapNavDisaster": "ป้องกันภัย", - "displayTheme": "ธีม", + "moonDays": "days", + "mapLegendUnit": "หน่วย: {unit}", + "weatherModeClear": "ท้องฟ้าแจ่มใส", + "meshtasticRadio": "Radio", + "commonEmpty": "ไม่มีข้อมูล", + "mapLayerSatelliteB01": "Himawari Blue (B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "น้อย→มาก", + "reportFilterApply": "ใช้", + "reportDetailImageUnavailable": "ยังไม่มีภาพรายงาน", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "สูงสุด", + "reportDetailReplay": "เล่นย้อนหลัง", + "mapLayerRestroom": "ห้องน้ำสาธารณะ", + "restroomCategoryWelfare": "สถานสงเคราะห์", + "restroomGradeExcellent": "ดีเยี่ยม", + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "การพยากรณ์เชิงตัวเลข", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", "themeSystem": "ระบบ", - "themeLight": "สว่าง", - "themeDark": "มืด", - "moreSectionAbout": "เกี่ยวกับ", - "termsOfService": "ข้อกำหนดในการให้บริการ", - "faq": "คำถามที่พบบ่อย", - "openSourceLicenses": "ใบอนุญาตโอเพนซอร์ส", - "sponsorTitle": "สนับสนุน DPIP", - "sponsorIntro": "DPIP มุ่งมั่นให้ข้อมูลการป้องกันภัยพิบัติแบบเรียลไทม์ โดยไม่มีโฆษณาหรือรูปแบบหารายได้อื่น การสนับสนุนของคุณช่วยให้เรารักษาเซิร์ฟเวอร์และพัฒนาต่อไปได้", - "sponsorSubscriptions": "แบบสมัครสมาชิก", - "sponsorRecommended": "แนะนำ", - "sponsorOneTime": "สนับสนุนครั้งเดียว", - "sponsorPerMonth": "{price} / เดือน", - "sponsorRestore": "กู้คืนการซื้อ", - "sponsorTerms": "ข้อกำหนดการใช้งาน", - "sponsorPrivacy": "นโยบายความเป็นส่วนตัว", - "sponsorRestoring": "กำลังกู้คืนการซื้อ…", - "sponsorRestoreUnavailable": "ไม่สามารถเชื่อมต่อร้านค้าได้ โปรดลองอีกครั้งภายหลัง", - "commonClose": "ปิด", + "mapLayerSatelliteNdvi": "Himawari NDVI", + "typhoonLegendForecast": "เส้นทางพยากรณ์", + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "ปริมาณน้ำฝน", + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "แตะเครื่องหมายบนแผนที่เพื่อดูรายละเอียด", + "onboardingSkipLeave": "ข้ามไปก่อน", + "onboardingBack": "ย้อนกลับ", + "aedPlaceDesc": "ตำแหน่งติดตั้ง", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "ยังไม่ได้ให้สิทธิ์", + "restroomTypeFamily": "ห้องน้ำครอบครัว", + "typhoonValueKm": "{n} km", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "ความกดอากาศ", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "ยกเว้นการประหยัดแบตเตอรี่", + "typhoonLabelNw": "NW", + "dpmDisasterFlood": "น้ำท่วม", + "moonPhaseWaxingCrescent": "Waxing crescent", + "restroomCategoryLeisure": "สถานที่พักผ่อนหย่อนใจ", "mapLayerTemperature": "อุณหภูมิ", - "trendRange24h": "24 ชม.", - "trendRange7d": "7 วัน", - "trendNoData": "ไม่มีข้อมูลแนวโน้ม", - "trendCumulativeTotal": "สะสม {total} มม.", - "chartHourLabel": "{hour}น.", - "mapLayerHumidity": "ความชื้น", - "mapLayerPressure": "ความกดอากาศ", + "aedCategory": "หมวดหมู่", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "Channels", + "monitorWaiting": "กำลังรอข้อมูล…", + "typhoonOverlayForecastCallouts": "Forecast tooltips", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "พิกัดศูนย์กลาง", + "meshtasticVoltage": "Voltage", + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "ลม", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "ปริมาณฝน", - "rainIntervalMenu": "ช่วงสะสม", - "rainIntervalNow": "วันนี้", - "rainInterval10m": "10 นาที", - "rainInterval1h": "1 ชม.", - "rainInterval3h": "3 ชม.", - "rainInterval6h": "6 ชม.", + "reportDetailMagnitude": "ขนาดแผ่นดินไหว", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "ความเข้มแยกตามพื้นที่", "rainInterval12h": "12 ชม.", - "rainInterval24h": "24 ชม.", - "rainInterval2d": "2 วัน", - "rainInterval3d": "3 วัน", - "mapLayerTyphoon": "ไต้ฝุ่น", - "typhoonNoActive": "ไม่มีไต้ฝุ่น", - "typhoonWind": "ความเร็วลม", - "typhoonGust": "ลมกระโชก", - "typhoonPressure": "ความกดอากาศ", - "typhoonMotion": "เคลื่อนที่", - "mapLayerMonitor": "เครื่องตรวจแผ่นดินไหว", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "แผนที่ป้องกันภัย", - "disasterMapOverlayMenuTooltip": "ชั้นแผนที่ป้องกันภัย", - "disasterMapOverlaySectionLayers": "ชั้น", - "disasterMapOverlayAedTooltip": "แสดงตำแหน่ง AED", - "aedAddress": "ที่อยู่", - "aedRegion": "พื้นที่", - "aedCategory": "หมวดหมู่", - "aedType": "ประเภท", - "aedPlaceDesc": "ตำแหน่งติดตั้ง", - "aedDescription": "หมายเหตุ", - "aedHoursWeekday": "เวลาวันธรรมดา", - "aedHoursSaturday": "เวลาวันเสาร์", - "aedHoursSunday": "เวลาวันอาทิตย์", - "aedOpenRemark": "หมายเหตุเวลาเปิด", - "aedEmergencyPhone": "โทรศัพท์ฉุกเฉิน", - "mapLayerRestroom": "ห้องน้ำสาธารณะ", - "mapLayerShelter": "ศูนย์อพยพ", - "disasterMapOverlayRestroomTooltip": "แสดงห้องน้ำสาธารณะ", - "disasterMapOverlayShelterTooltip": "แสดงศูนย์อพยพ", - "dpmOpenInMaps": "เปิดในแผนที่", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "ดินถล่ม", + "notifyMonitor": "เครื่องเฝ้าระวังการสั่นสะเทือนรุนแรง", + "onboardingStart": "เริ่มใช้งาน", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / เดือน", + "mapLayerPressure": "ความกดอากาศ", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", + "shelterIndoorLabel": "การอพยพในอาคาร", + "notifyOptOff": "ปิด", + "reportFilterSortTime": "เวลา", + "mapLayerSatelliteCloudProbablyClear": "Probably clear", + "weatherModeThunderstorm": "พายุฝนฟ้าคะนอง", + "homeViewOnMap": "ดูบนแผนที่", + "reportFilterIntensityInfoLegacyTitle": "แบบเก่า (ก่อน 2020)", + "typhoonLabelSpeed": "Past movement speed", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "ไม่สามารถเปิด {app} ได้", + "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "ต่ำสุดวันนี้", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", + "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "shelterCategoryLabel": "ประเภทภัยพิบัติ", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", + "meshtasticStateConnecting": "Connecting…", + "moonTitle": "Moon", + "weatherRankingGust": "ลมกระโชก", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "ประเภทภัยพิบัติของศูนย์อพยพ", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "สถานะเซิร์ฟเวอร์", + "notifySectionWeather": "สภาพอากาศ", + "meshtasticPreset": "Modem preset", + "dataSectionSeismic": "แผ่นดินไหว", + "changelogBodyEmpty": "ไม่มีคำอธิบายสำหรับรุ่นนี้", + "radarGlobalOutline": "เส้นแบ่งเขตประเทศ", + "notifyEew": "การเตือนแผ่นดินไหวฉุกเฉิน", + "regionNationwide": "ทั่วประเทศ", + "moreNotifyLog": "บันทึกการส่งการแจ้งเตือนของ DPIP", + "regionCurrent": "ตำแหน่งปัจจุบัน", + "dpmFilterSectionRestroom": "ประเภทสถานที่", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "หิมะตก", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "ข้อมูลดีบัก", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "ฟ้าผ่า", + "homeForecastEmpty": "ไม่มีข้อมูลพยากรณ์", + "sponsorOneTime": "สนับสนุนครั้งเดียว", + "mapLayerSatelliteBtdSplit": "Himawari Split Window", + "onboardingPermBackground": "ตำแหน่งที่ตั้งเบื้องหลัง", + "aedEmergencyPhone": "โทรศัพท์ฉุกเฉิน", + "dpmOpenInMaps": "เปิดในแผนที่", + "meshtasticNotifyNodes": "Notify on new nodes", + "onboardingPermCriticalDesc": "ให้การเตือนแผ่นดินไหวที่เป็นอันตรายถึงชีวิตส่งเสียงได้ แม้อยู่ในโหมดเงียบหรือโหมดห้ามรบกวน", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", + "meshtasticSent": "Sent", + "homeForecastTitle": "พยากรณ์ 24 ชั่วโมง", + "typhoonLegendWarningAreas": "พื้นที่เตือนภัย", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "ความรุนแรงในพื้นที่ระดับ 1 ขึ้นไป", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "อดีต", + "restroomTypeFemale": "ห้องน้ำหญิง", + "reportListToday": "วันนี้", + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "กำลังโหลด…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "Moderate typhoon", + "typhoonWind": "ความเร็วลม", + "mapLayerSatelliteAsh": "Himawari Ash", + "rainInterval3h": "3 ชม.", + "reportListSearch": "ค้นหา", + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "ดาวเทียม", + "reportFilterLocation": "สถานที่", + "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", + "typhoonIntensityTd": "Tropical depression", + "reportFilterDate": "วันที่", + "sponsorRestoreUnavailable": "ไม่สามารถเชื่อมต่อร้านค้าได้ โปรดลองอีกครั้งภายหลัง", + "homeForecastPop": "{pop}%", + "regionEmpty": "ยังไม่มีพื้นที่ที่บันทึกไว้", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "อนุญาตให้ DPIP ทำงานเบื้องหลังอย่างต่อเนื่อง เพื่อไม่ให้การเตือนภัยล่าช้าหรือพลาดไป", + "mapNavDisaster": "ป้องกันภัย", + "radarScanRangeSubtitle": "แสดงพื้นที่ที่เรดาร์ทั้งสี่ตรวจวัดได้จริง", + "aedHoursSunday": "เวลาวันอาทิตย์", + "reportDetailOriginTime": "เวลาเกิดเหตุ", + "trendNoData": "ไม่มีข้อมูลแนวโน้ม", + "onboardingPermLocation": "ตำแหน่งที่ตั้ง", + "moreDiscord": "ชุมชน Discord", + "mapNavPressure": "ความกดอากาศ", + "mapLayerSatelliteB13": "Himawari Infrared (B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "ยังไม่มีบันทึกการเผยแพร่", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "วันเริ่ม: 00:00 ของวันนั้น(ไทเป)", + "eewTitle": "การเตือนแผ่นดินไหวล่วงหน้า", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "th", + "regionSelectCount": "เลือกแล้ว {count}/{max}", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", + "meshtasticStateError": "Error", + "weatherModeOvercast": "ฟ้าปิด", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "ความลึกจุดศูนย์กลาง", + "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", + "reportFilterDatePick": "เลือกวันที่", + "onboardingSkipStay": "กลับไปให้สิทธิ์", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "ไม่สามารถโหลดข้อมูลได้ โปรดลองอีกครั้ง", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "การอพยพกลางแจ้ง", + "meshtasticStateConnected": "Connected", + "mapNavRadar": "เรดาร์", + "mapLayerSatelliteCloudClear": "Clear", + "eewSummary": "ขนาด {magnitude} · ความลึก {depth} กม.", + "locationBannerPermission": "ยังไม่ได้อนุญาตสิทธิ์ตำแหน่งที่ตั้ง — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้", + "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", + "radarCountyOutlineHint": "วาดทับภาพเอคโค", + "windForecastCountyOutlineHint": "วาดทับบนสนามลม", + "homeRainTrendTitle": "ฝนชั่วโมงถัดไป", + "moonPhaseFirstQuarter": "First quarter", + "mapLayerCategoryTyphoon": "พายุไต้ฝุ่น", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "Airtime (24h)", + "restroomTypeMixed": "ห้องน้ำรวม", + "restroomGradeGood": "ดี", + "notifyTsunami": "ข้อมูลสึนามิ", + "navData": "ข้อมูล", + "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "อุปกรณ์นี้ไม่สามารถโทรออกได้", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "ทั้งหมด", + "weatherRankingMergeTo": "รวม", + "notifyIntensity": "รายงานความรุนแรงแผ่นดินไหว", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "ช่วงสะสม", + "reportDetailLocalFelt": "แผ่นดินไหวรู้สึกได้เฉพาะพื้นที่", + "meshtasticDevice": "Device", + "onboardingGrant": "อนุญาต", + "weatherModeRain": "ฝนตก", + "shelterVulnerableOkLabel": "เหมาะกับผู้เปราะบาง", + "stationSheetEmpty": "แตะสถานีเพื่อดูค่าที่วัดได้", + "typhoonLegendProbability": "โอกาสกระทบ", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "ขนาด", + "skyTimeMorning": "ตอนเช้า", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "ฟีเจอร์ทดลอง", + "onboardingTermsBody": "โปรดอ่านข้อควรทราบต่อไปนี้ก่อนใช้งาน DPIP:\n\n• ข้อมูลทั้งหมดควรยึดตามเนื้อหาที่เผยแพร่โดยกรมอุตุนิยมวิทยากลาง (CWA) เป็นหลัก\n\n• ขึ้นอยู่กับสภาพเครือข่าย เซิร์ฟเวอร์ แอปพลิเคชัน และแหล่งข้อมูลต้นทาง อาจมีความเป็นไปได้ที่จะไม่ได้รับข้อมูล เราพยายามอย่างเต็มที่เพื่อหลีกเลี่ยงกรณีเช่นนี้ แต่ไม่สามารถรับประกันได้ว่าจะไม่เกิดขึ้น\n\n• การสั่นสะเทือนอย่างรุนแรงอาจมาถึงตำแหน่งของคุณก่อนการแจ้งเตือน\n\n• การเตือนแผ่นดินไหวล่วงหน้าเป็นผลจากการคำนวณอย่างรวดเร็ว ซึ่งอาจมีความคลาดเคลื่อนสูง โปรดทำความเข้าใจและใช้งานด้วยความระมัดระวัง\n\n• พฤติกรรมใด ๆ ที่ไม่ได้รับการรับรองจากหน่วยงานราชการอาจมีความเสี่ยงทางกฎหมาย โปรดปฏิบัติตามระเบียบที่เกี่ยวข้องทั้งหมด\n\nนอกจากนี้ เพื่อให้บริการการเตือนภัยเฉพาะพื้นที่ บริการนี้จะเก็บรวบรวมและอัปโหลดตำแหน่งโดยประมาณและตัวระบุการแจ้งเตือนแบบพุชของคุณ — ทั้งขณะทำงานเบื้องหน้าและเบื้องหลัง — เพื่อใช้ตัดสินว่าจะส่งการเตือนใดให้คุณเท่านั้น\n\nการแตะ \"ยอมรับและดำเนินการต่อ\" ถือว่าคุณได้อ่าน เข้าใจ และยอมรับข้อความข้างต้นแล้ว", + "reportFilterTitle": "ตัวกรอง", + "onboardingPermCritical": "การแจ้งเตือนสำคัญ", + "trendCumulativeTotal": "สะสม {total} มม.", + "languageName": "ไทย", + "reportListEmptyFiltered": "ไม่มีรายงานที่ตรงกับเงื่อนไข", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "ไต้ฝุ่น", + "weatherModeSand": "ฝุ่นทราย", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "ดาวเทียม", + "@dpmOpenInMaps": {}, + "notifyReport": "รายงานแผ่นดินไหว", + "mapAppCoordinatesCopied": "คัดลอกพิกัดแล้ว", + "skyTimeNight": "กลางคืน", + "sponsorRecommended": "แนะนำ", + "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", + "weatherRankingWind": "ความเร็วลม", + "feedStale": "ข้อมูลอาจล้าสมัย", + "homeForecastWind": "{direction} · แรง {level}", + "navHome": "หน้าแรก", + "meshtasticRegionLabel": "Region", + "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "ใบอนุญาตโอเพนซอร์ส", + "weatherRankingLowest": "ต่ำสุด", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "ความลึก", + "mapTimelineDataTime": "เวลาข้อมูล {time}", + "radarScanRange": "แสดงขอบเขตการสแกน", + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "ช่วง {value}°C", + "weatherRankingExtremeHigh": "สูงสุดวันนี้", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "รายละเอียดเวอร์ชัน", + "sponsorPrivacy": "นโยบายความเป็นส่วนตัว", + "reportDetailLocalIntensity": "ความเข้มที่ตำแหน่งของคุณ", + "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", + "meshtasticAirtime": "Air time (TX)", + "shelterCapacityValue": "{n} คน", + "lightningLegendCc": "เมฆสู่เมฆ · {minutes} นาที", + "meshtasticSendHint": "Message to broadcast", + "monitorDelay": "หน่วงเวลา {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "ไม่ใช่", + "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "ทำให้เส้นแบ่งเขตอำเภอยังอ่านออกใต้ภาพเอคโคเรดาร์", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", + "radarScanRangeHint": "นอกกรอบคือไม่ได้ตรวจวัด", + "typhoonPickerTd": "Tropical depression TD {no}", + "mapLayerSatelliteWatervapor": "Himawari Water Vapour", + "regionAddButton": "เพิ่มพื้นที่", + "displaySettings": "การแสดงผล", + "restroomGradePoor": "ต่ำกว่ามาตรฐาน", + "restroomCategoryTourist": "แหล่งท่องเที่ยว", + "locationBannerServiceOff": "บริการระบุตำแหน่งถูกปิด — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้", + "mapLayerStyleTooltip": "Colour style", + "lightningLegendCg": "เมฆสู่พื้น · {minutes} นาที", + "skyTimeAuto": "อัตโนมัติ", + "appLogs": "บันทึกแอป", + "feedConnecting": "กำลังเชื่อมต่อ…", + "notifyBannerDisabled": "ปิดการแจ้งเตือนอยู่ — คุณจะไม่ได้รับการเตือนภัยพิบัติ", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "ความชื้น", + "typhoonValueMs": "{n} m/s", + "homeForecastHumidity": "ความชื้น {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "การคมนาคม", + "reportFilterLocationHint": "เช่น ฮวาเหลียน", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "ระยะทาง", + "meshtasticSnrTrend": "แนวโน้มสัญญาณ (SNR)", + "meshtasticBatteryTrend": "แนวโน้มแบตเตอรี่", + "typhoonOverlayMenuTooltip": "Typhoon overlay options", + "mapLayerSatelliteBtdOzone": "Himawari Tropopause", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "แผ่นดินไหว", + "mapLayerDisasterMap": "แผนที่ป้องกันภัย", + "weatherModeFog": "หมอกหนา", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", + "moreAnnouncements": "ประกาศ", + "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "สำนักงานราชการ", + "typhoonLegendCurrent": "ศูนย์กลางปัจจุบัน", + "aedAddress": "ที่อยู่", + "mapLayerAed": "AED", + "changelogTypePrerelease": "เบต้า", + "reportFilterIntensityInfoModernBody": "ระดับ 0–4, 5−, 5+, 6−, 6+, 7 แถบตัวกรองใช้แบบใหม่ เหตุการณ์เก่าในรายการยังแสดงป้ายแบบเก่า", + "typhoonOverlayWeatherNone": "None", + "mapLayerStyleGray": "Grayscale (JMA)", + "weatherModeAuto": "อัตโนมัติ", + "typhoonLabelProbCircle": "70% probability circle", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "รับทั้งหมด", + "displayTheme": "ธีม", + "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "Past movement direction", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "พื้นที่ที่ใช้บ่อย", + "typhoonLegendCone": "กรวยพยากรณ์", + "moreCwaEew": "การเตือนแผ่นดินไหวล่วงหน้าของกรมอุตุนิยมวิทยากลาง (CWA)", + "onboardingPermsTitle": "การอนุญาตสิทธิ์", + "mapLayerStyleJma": "Cloud-top enhancement (JMA)", + "rainInterval10m": "10 นาที", + "weatherRankingAnalysisLow": "ต่ำ {value}", + "meshtasticConnectAnyway": "Connect anyway", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", + "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", + "chartHourLabel": "{hour}น.", + "mapLayerShelter": "ศูนย์อพยพ", + "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", + "mapLayerSatelliteNdwi": "Himawari NDWI", + "disasterMapOverlayShelterTooltip": "แสดงศูนย์อพยพ", + "mapNavHumidity": "ความชื้น", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "เรียงตามความเข้ม", + "homeRainTrendNoData": "ไม่มีข้อมูล", + "mapLayerCategoryRadar": "เรดาร์", + "meshtasticShortName": "Short name", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "Himawari Airmass", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "รายละเอียดเส้นทาง", + "dataSectionWeather": "อากาศ", + "aedHoursWeekday": "เวลาวันธรรมดา", + "homeActiveEventsTitle": "เหตุการณ์ที่ยังมีผล", + "weatherRankingAnalysisHigh": "สูง {value}", + "faq": "คำถามที่พบบ่อย", + "typhoonHistoryLive": "สด", + "eewSerial": "รายงาน {serial}", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "เรียงลำดับ", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "รายงานแผ่นดินไหว", + "typhoonNoActive": "ไม่มีไต้ฝุ่น", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", + "navEvents": "เหตุการณ์", + "onboardingTermsTitle": "ข้อกำหนดการให้บริการ", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "ชื่อตำบล", + "notifySetFailed": "ไม่สามารถบันทึกการตั้งค่าได้ โปรดลองอีกครั้ง", + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "notifyAnnouncement": "ประกาศ", + "onboardingIntroTitle": "ยินดีต้อนรับสู่ DPIP", + "regionCurrentUnavailable": "ไม่สามารถระบุตำแหน่งปัจจุบันได้", + "languageSystem": "ค่าเริ่มต้นของระบบ", + "skyTimeSunset": "พระอาทิตย์ตก", + "mapLayerSatelliteDust": "Himawari Dust", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "แก้ไข", + "weatherDynamicState": "แอนิเมชันสภาพอากาศ", + "mapPlaceholderDisabled": "แผนที่ (ปิดใช้งานชั่วคราว)", + "moonNow": "ตอนนี้", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "ลักษณะ", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "จันทร์ขึ้นและตก", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "ถัดไป", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "ปฏิทิน", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "ระยะทาง", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "กม.", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "ขนาดปรากฏ", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "จันทร์ขึ้น", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "จันทร์ตก", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "นิวมูนครั้งถัดไป", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "อยู่เหนือขอบฟ้าทั้งวัน", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "ไม่มีในวันนี้", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "ดวงอาทิตย์", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "พระอาทิตย์ขึ้น สนธยา และปักษ์", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "แสงกลางวัน", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "สนธยา", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "แสง", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "นาฬิกาแดด", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "ปักษ์", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "พระอาทิตย์ขึ้น", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "พระอาทิตย์ตก", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "เที่ยงสุริยะ", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "ความยาววัน", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "พลเรือน", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "เดินเรือ", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "ดาราศาสตร์", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "โกลเดนอาวร์เช้า", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "โกลเดนอาวร์เย็น", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "บลูอาวร์", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "สมการเวลา", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "นาที", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "ปักษ์ถัดไป", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "ดาวเคราะห์", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "คืนนี้อยู่ไหน สว่างแค่ไหน", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "ขณะนี้", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "เหนือขอบฟ้า", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "ใต้ขอบฟ้า", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "ใกล้ดวงอาทิตย์", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "โชติมาตร", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "มุมห่าง", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "ช่วงเวลา", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "หัวค่ำ", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "ก่อนรุ่ง", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "ระยะทาง", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "มุมเงย", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "พุธ", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "ศุกร์", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "อังคาร", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "พฤหัสบดี", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "เสาร์", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "ยูเรนัส", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "เนปจูน", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "วสันตวิษุวัต", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "เช็งเม้ง", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "ฝนธัญพืช", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "เริ่มฤดูร้อน", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "ธัญพืชเต็ม", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "ธัญพืชออกรวง", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "ครีษมายัน", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "ร้อนน้อย", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "ร้อนมาก", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "เริ่มฤดูใบไม้ร่วง", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "สิ้นสุดความร้อน", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "น้ำค้างขาว", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "ศารทวิษุวัต", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "น้ำค้างเย็น", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "น้ำค้างแข็ง", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "เริ่มฤดูหนาว", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "หิมะน้อย", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "หิมะมาก", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "เหมายัน", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "หนาวน้อย", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "หนาวมาก", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "เริ่มฤดูใบไม้ผลิ", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "ฝนน้ำ", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "แมลงตื่น", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "คืนนี้", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "มีอะไรให้ดู และเมื่อไร", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "ช่วงสังเกตการณ์", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "กลางคืนทางดาราศาสตร์", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "ไม่มืดสนิท", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "ช่วงมืด", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "ดวงจันทร์อยู่ทั้งคืน", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "เวลามืดรวม", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "แสงจันทร์", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "ฝนดาวตก", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "จุดกระจายไม่ขึ้น", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "ดวง/ชม.", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "การผ่านของดาวเทียม", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "เป้าหมายที่เห็นได้ตอนนี้", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "ควอดรานติดส์", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "ไลริดส์", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "อีตาอควาริดส์", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "เดลตาอควาริดส์", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "เพอร์เซอิดส์", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "โอไรออนิดส์", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "เทาริดส์ใต้", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "ลีโอนิดส์", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "เจมินิดส์", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "เออร์ซิดส์", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "กระจุกดาวเปิด", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "กระจุกดาวทรงกลม", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "ดาราจักรกังหัน", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "ดาราจักรรี", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "ดาราจักรไร้รูปแบบ", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "เนบิวลาดาวเคราะห์", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "ซากซูเปอร์โนวา", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "เนบิวลาเปล่งแสง", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "เนบิวลาสะท้อนแสง", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "กลุ่มดาวย่อย", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "ปฏิทิน", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "ปฏิทินจันทรคติและอุปราคาข้างหน้า", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "วันนี้", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "สุริยคติ", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "จันทรคติ", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "ปีนักษัตร", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app} (ค่าเริ่มต้น)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "ความยาวเดือน", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "คัดลอกพิกัด", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "30 วัน", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "คัดลอกพิกัดแล้ว", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "29 วัน", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "ไม่สามารถเปิด {app} ได้", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "อธิกมาส ", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "อุปกรณ์นี้ไม่สามารถโทรออกได้", - - "mapOverlaySectionReference": "เลเยอร์อ้างอิง", - "mapLayerCategoryEarthquake": "แผ่นดินไหว", - "mapLayerCategoryTyphoon": "พายุไต้ฝุ่น", - "mapLayerCategoryWeather": "การสังเกตสภาพอากาศ", - "mapLayerCategorySatellite": "ดาวเทียม", - "mapLayerCategoryRadar": "เรดาร์", - "mapLayerCategoryLife": "ชีวิตประจำวัน", - "mapLayerCategoryForecast": "การพยากรณ์เชิงตัวเลข", "mapOverlaySectionMap": "แผนที่", - "rainIntervalSection": "ช่วงเวลา", - - "mapTownLabels": "ชื่อตำบล", - "mapTownLabelsHint": "แสดงชื่อตำบลเมื่อขยายแผนที่", - - "mapTerrainRelief": "ความนูนของภูมิประเทศ", - "mapTerrainReliefHint": "แสดงความนูนของภูมิประเทศบนแผนที่ฐาน", - - "dpmSheetEmpty": "แตะเครื่องหมายบนแผนที่เพื่อดูรายละเอียด", - "dpmAddress": "ที่อยู่", - "restroomTypeLabel": "ประเภท", - "restroomCategoryLabel": "หมวดหมู่", - "restroomGradeLabel": "ระดับ", - "restroomTypeFemale": "ห้องน้ำหญิง", - "restroomTypeMale": "ห้องน้ำชาย", - "restroomTypeMixed": "ห้องน้ำรวม", - "restroomTypeAccessible": "ห้องน้ำคนพิการ", - "restroomTypeGenderNeutral": "ห้องน้ำเป็นกลางทางเพศ", - "restroomTypeFamily": "ห้องน้ำครอบครัว", - "restroomTypeUnspecified": "ไม่ระบุ", - "restroomCategoryTransport": "การคมนาคม", - "restroomCategoryPark": "สวนสาธารณะ", - "restroomCategoryCommercial": "สถานประกอบการพาณิชย์", - "restroomCategoryReligious": "สถานที่ทางศาสนา", - "restroomCategoryCultural": "สถานที่ทางวัฒนธรรม", - "restroomCategoryGovernment": "สำนักงานราชการ", - "restroomCategoryWelfare": "สถานสงเคราะห์", - "restroomCategoryTourist": "แหล่งท่องเที่ยว", - "restroomCategoryLeisure": "สถานที่พักผ่อนหย่อนใจ", - "restroomCategoryOther": "อื่น ๆ", - "restroomGradeExcellent": "ดีเยี่ยม", - "restroomGradeGood": "ดี", - "restroomGradeAverage": "ปานกลาง", - "restroomGradePoor": "ต่ำกว่ามาตรฐาน", - "shelterAddressLabel": "ที่อยู่", - "shelterCapacityLabel": "ความจุ", - "shelterCapacityValue": "{n} คน", - "shelterCategoryLabel": "ประเภทภัยพิบัติ", - "shelterIndoorLabel": "การอพยพในอาคาร", - "shelterOutdoorLabel": "การอพยพกลางแจ้ง", - "shelterVulnerableOkLabel": "เหมาะกับผู้เปราะบาง", - "dpmYes": "ใช่", - "dpmNo": "ไม่ใช่", - "stationSheetEmpty": "แตะสถานีเพื่อดูค่าที่วัดได้", - "monitorDelay": "หน่วงเวลา {value} s", - "monitorWaiting": "กำลังรอข้อมูล…", - "mapLegendUnit": "หน่วย: {unit}", - "typhoonLegendPast": "เส้นทางจริง", - "typhoonLegendForecast": "เส้นทางพยากรณ์", - "typhoonLegendForecastPoint": "จุดพยากรณ์", - "typhoonLegendCurrent": "ศูนย์กลางปัจจุบัน", - "typhoonLegendCone": "กรวยพยากรณ์", - "mapLegendExpand": "คำอธิบาย", - "mapLegendCollapse": "ซ่อนคำอธิบาย", - "mapMyLocation": "ตำแหน่งของฉัน", - "mapResetNorth": "กลับไปทางเหนือ", - "typhoonLegendCircle15": "วงพายุ (แรง)", - "typhoonLegendCircle25": "วงพายุ (รุนแรง)", - "typhoonLegendProbability": "โอกาสกระทบ", - "typhoonLegendWarningAreas": "พื้นที่เตือนภัย", - "typhoonWarningTitle": "ประกาศเตือนไต้ฝุ่น", - "typhoonWarningAreas": "พื้นที่: {areas}", - "typhoonTrackDetail": "รายละเอียดเส้นทาง", - "typhoonHistoryTitle": "เวลาข้อมูล", - "typhoonHistoryLive": "สด", - "typhoonSatelliteTitle": "ดาวเทียม", - "typhoonDataTime": "Data time\n{time}", - "typhoonForecastLead": "Forecast +{hours} h", - "typhoonIntensityIntense": "Intense typhoon", - "typhoonIntensityMild": "Mild typhoon", - "typhoonIntensityModerate": "Moderate typhoon", - "typhoonIntensityTd": "Tropical depression", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "Tropical depression TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "Past movement direction", - "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", - "typhoonLabelGust": "Peak gust", - "typhoonLabelNe": "NE", - "typhoonLabelNw": "NW", - "typhoonLabelPosition": "Centre location", - "typhoonLabelPressure": "Central pressure", - "typhoonLabelProbCircle": "70% probability circle", - "typhoonLabelSe": "SE", - "typhoonLabelSpeed": "Past movement speed", - "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", - "typhoonLabelSw": "SW", - "typhoonLabelWind": "Max. sustained wind near centre", - "typhoonLegendCircleAvg": "Average circle", - "typhoonOverlayMenuTooltip": "Typhoon overlay options", - "typhoonOverlayProbabilityHint": "Hides the forecast cone", - "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", - "typhoonOverlaySectionExtra": "Overlays", - "typhoonOverlaySectionStorm": "Storm wind", - "typhoonOverlaySectionWeather": "Weather underlay", - "typhoonOverlayStormBandSubtitle": "With average circle", - "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", - "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", - "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", - "typhoonOverlayWeatherHint": "Aligned to bulletin time", - "typhoonOverlayWeatherNone": "None", - "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", - "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", - "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} hPa", - "typhoonValueKm": "{n} km", - "typhoonValueLat": "{lat}°N", - "typhoonValueLon": "{lon}°E", - "typhoonValueMs": "{n} m/s", - "typhoonOverlayForecastCallouts": "Forecast tooltips", - "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", - "dpmFilterSectionRestroom": "ประเภทสถานที่", - "dpmFilterSectionRestroomType": "ประเภทห้องน้ำ", - "dpmFilterSectionShelter": "ประเภทภัยพิบัติของศูนย์อพยพ", - "dpmDisasterFlood": "น้ำท่วม", - "dpmDisasterEarthquake": "แผ่นดินไหว", - "dpmDisasterLandslide": "ดินถล่ม", - "dpmDisasterTsunami": "สึนามิ", - "dpmDisasterSlope": "ภัยพิบัติลาดชัน", - "dpmDisasterNuclear": "อุบัติเหตุนิวเคลียร์", - "skyTime": "เวลาท้องฟ้า", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "จันทรุปราคา", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "อัตโนมัติ", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "สุริยุปราคา", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "รุ่งอรุณ", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "ไม่มีในช่วงนี้", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "พระอาทิตย์ขึ้น", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "เต็มดวง", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "ตอนเช้า", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "บางส่วน", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "เที่ยงวัน", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "วงแหวน", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "ตอนบ่าย", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "เงามัว", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "ช่วงเวลาทอง", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "ชวด", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "พระอาทิตย์ตก", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "ฉลู", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "สนธยา", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "ขาล", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "กลางคืน", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "เถาะ", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "มีเมฆมาก", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "มะโรง", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "ฟ้าปิด", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "มะเส็ง", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "หิมะตก", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "มะเมีย", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "ฝุ่นทราย", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "มะแม", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "แสดงขอบเขตการสแกน", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "วอก", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "แสดงพื้นที่ที่เรดาร์ทั้งสี่ตรวจวัดได้จริง", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "ระกา", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "นอกกรอบคือไม่ได้ตรวจวัด", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "จอ", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "ตัวเลือกชั้นเรดาร์", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "กุน", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "เส้นแบ่งเขตจังหวัด", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "น้ำขึ้นน้ำลง", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "เส้นแบ่งเขตประเทศ", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "น้ำเกิด น้ำตาย และแรงดึงดูดของดวงจันทร์", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "กรอบนอกของทุกประเทศ", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "แรงดาราศาสตร์เท่านั้น ไม่ใช่ตารางน้ำท่า ระดับน้ำโปรดดูตารางที่กรมอุตุนิยมวิทยาเผยแพร่", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "วาดทับภาพเอคโค", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "ขณะนี้", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "ทำให้เส้นแบ่งเขตยังอ่านออกใต้ภาพเอคโคเรดาร์", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "วัฏจักร", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "เส้นแบ่งเขตอำเภอ", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "น้ำเกิด", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "เส้นแบ่งย่อยกว่า", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "น้ำตาย", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "ทำให้เส้นแบ่งเขตอำเภอยังอ่านออกใต้ภาพเอคโคเรดาร์", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "ปานกลาง", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "ตัวเลือกชั้นพยากรณ์น้ำฝน", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "แรงดึงดวงจันทร์", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "ตัวเลือกชั้นพยากรณ์ลม", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "ระดับสมดุล", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "วาดทับบนสนามลม", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "ม.", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "กรอบนอกของทุกประเทศ", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "น้ำเกิดใกล้โลกครั้งถัดไป", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "ตาข่ายที่ละเอียดกว่า", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "จุดเปลี่ยน", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "รายงาน {serial}", - "eewMaxIntensity": "ความรุนแรงสูงสุด", - "eewLocalIntensity": "ประมาณ ณ ตำแหน่ง", - "eewSWave": "คลื่น S", - "eewArrived": "มาถึงแล้ว", - "eewCountdown": "{seconds} วินาที" + "tideHigh": "สูง", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "ต่ำ", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "แผนที่ดาว", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "ท้องฟ้าที่ตาเปล่ามองเห็น", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "N", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "E", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "S", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "W", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "ข้อมูลวงโคจร {days} วันก่อน", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}เดือน {month} วันที่ {day}", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "ไม่มีฝนดาวตก", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "ไม่มีการผ่านที่มองเห็นใน 48 ชม.", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "อ่านข้อมูลวงโคจรไม่ได้", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "ไม่มีเป้าหมายที่สูงพอ", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "อ่านแคตตาล็อกดาวไม่ได้", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 5eda21767..195420563 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -1,679 +1,1739 @@ { - "@@locale": "vi", - "languageName": "Tiếng Việt", - "navHome": "Trang chủ", - "navEvents": "Sự kiện", - "navMap": "Bản đồ", - "navData": "Dữ liệu", - "navEarthquake": "Động đất", - "dataSectionSeismic": "Địa chấn", - "dataEarthquakeSubtitle": "Báo cáo động đất", - "dataSectionWeather": "Thời tiết", - "dataWeatherRankingSubtitle": "Xếp hạng trạm trực tiếp", - "weatherRankingTitle": "Xếp hạng quan trắc", - "weatherRankingMeta": "Thời gian: {time}\n{count} trạm", - "weatherRankingEmpty": "Không có quan trắc để xếp hạng", - "weatherRankingBy": "Theo", - "weatherRankingHighest": "Cao nhất", - "weatherRankingLowest": "Thấp nhất", - "weatherRankingMergeTo": "Gộp", - "weatherRankingMergeTown": "Xã/trấn", - "weatherRankingMergeCounty": "Huyện/thành", - "weatherRankingWind": "Tốc độ gió", - "weatherRankingGust": "Gió giật", + "typhoonValueLat": "{lat}°N", + "onboardingSkipBody": "Nếu không có quyền vị trí và thông báo, DPIP không thể cảnh báo tức thời về động đất và thiên tai gần bạn. Bạn vẫn có thể cấp quyền sau trong Cài đặt.", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 giờ", + "homeRainTrendHeavyStopping": "Mưa lớn có thể tạnh trong {minutes} phút nữa", + "mapTimelineObserved": "Quan trắc", + "regionSelectTitle": "Chọn khu vực", + "skyTimeNoon": "Buổi trưa", + "radarCountyOutlineSubtitle": "Giữ ranh giới rõ ràng dưới lớp phản hồi radar.", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "Loại nhà vệ sinh", + "mapLayerSatelliteB03": "Himawari Red (B03)", + "reportFilterIntensity": "Cường độ", + "mapLayerLightning": "Sét", + "restroomTypeMale": "Nhà vệ sinh nam", + "meshtasticLastReceived": "Last received", + "reportDetailSortByCounty": "Sắp xếp theo khu vực", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "Có thể có mưa rào nhẹ", + "meshtasticUptime": "Uptime", "weatherRankingTempExtremes": "Cực trị nhiệt độ", - "weatherRankingExtremeHigh": "Cao nhất ngày", - "weatherRankingExtremeLow": "Thấp nhất ngày", + "themeLight": "Sáng", + "mapTerrainReliefHint": "Hiển thị địa hình nổi trên bản đồ nền", + "meshtasticEmptyMessage": "(empty message)", + "moreSectionRegion": "Khu vực", + "dpmDisasterEarthquake": "Động đất", + "mapLayerSatellite": "Himawari Infrared (B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "Giờ thứ Bảy", + "dpmDisasterSlope": "Thiên tai sườn dốc", + "moonPhaseNew": "New moon", + "notifySectionEew": "Cảnh báo sớm động đất", + "mapResetNorth": "Về hướng bắc", + "rainInterval2d": "2 ngày", + "mapTownLabelsHint": "Hiển thị tên hương trấn khi phóng to", + "commonCancel": "Cancel", + "notifyOptTsunamiWarning": "Chỉ cảnh báo sóng thần", + "mapLayerSatelliteBtdFog": "Himawari Night Fog", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "Nâng cao", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "Biên độ ngày", + "notifySettingsMenu": "Cài đặt thông báo", + "typhoonHistoryTitle": "Thời điểm dữ liệu", + "mapAppDefault": "{app} (mặc định)", + "trendRange24h": "24 giờ", + "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", "weatherRankingRecordedAt": "Ghi nhận lúc {time}", - "weatherRankingAnalysisCurrent": "Hiện tại {value}°C", - "weatherRankingAnalysisHigh": "Cao {value}", - "weatherRankingAnalysisLow": "Thấp {value}", - "weatherRankingAnalysisRange": "Biên độ {value}°C", - "reportListEmpty": "Không có báo cáo động đất", - "reportListEmptyFiltered": "Không có báo cáo khớp bộ lọc", - "reportListMeta": "M{magnitude} · {depth} km", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "km", - "reportListLocalFelt": "Cảm nhận cục bộ", - "reportListToday": "Hôm nay", - "reportListYesterday": "Hôm qua", - "reportListDayCount": "{count}", - "reportListEnd": "Hết danh sách", - "reportFilterTitle": "Bộ lọc", - "reportFilterSort": "Sắp xếp", - "reportFilterSortTime": "Thời gian", - "reportFilterSortIntensity": "Cường độ", - "reportFilterSortMagnitude": "Độ lớn", - "reportFilterSortDepth": "Độ sâu", + "mapLayerRain": "Lượng mưa", + "mapLayerQpesums": "Dự báo mưa 1 giờ tới", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "Bản đồ", + "mapTerrainRelief": "Độ nổi địa hình", + "eewMaxIntensity": "Cường độ tối đa", + "mapLegendCollapse": "Ẩn chú giải", + "changelogTitle": "Nhật ký cập nhật", "reportFilterOrderDesc": "Giảm dần", - "reportFilterOrderAsc": "Tăng dần", - "reportFilterIntensity": "Cường độ", + "meshtasticExcludeMqttSubtitle": "Nodes bridged over the internet, not heard by radio", "reportFilterIntensityInfoTitle": "Thang cường độ mới và cũ", - "reportFilterIntensityInfoIntro": "CWA đổi thang cường độ từ 1/1/2020 (giờ Đài Bắc).", - "reportFilterIntensityInfoLegacyTitle": "Cũ (trước 2020)", - "reportFilterIntensityInfoLegacyBody": "Chỉ có mức 0–7, không tách 5−/5+/6−/6+.", - "reportFilterIntensityInfoModernTitle": "Mới (từ 2020)", - "reportFilterIntensityInfoModernBody": "Các mức 0–4, 5−, 5+, 6−, 6+, 7. Thanh lọc dùng thang mới; sự kiện cũ vẫn hiện nhãn cũ trong danh sách.", - "reportFilterMagnitude": "Độ lớn", - "reportFilterDepth": "Độ sâu", - "reportFilterDepthKm": "{depth} km", - "reportFilterDate": "Ngày", - "reportFilterDatePick": "Chọn ngày", - "reportFilterDateStartNote": "Ngày bắt đầu: từ 00:00(Đài Bắc)", + "mapLayerTyphoon": "Bão", + "radarOverlayMenuTooltip": "Tùy chọn lớp radar", + "mapMyLocation": "Vị trí của tôi", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "Nodes", + "meshtasticSend": "Send", + "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", + "aedType": "Loại", + "termsOfService": "Điều khoản dịch vụ", + "typhoonLegendCircle25": "Vòng bão", + "sponsorTitle": "Ủng hộ DPIP", + "mapNavSatellite": "Vệ tinh", + "homeRainTrendUpdated": "Cập nhật {time}", + "onboardingNext": "Tiếp theo", + "weatherRankingMergeTown": "Xã/trấn", + "mapLayerMonitor": "Giám sát địa chấn", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "Gói đăng ký", + "typhoonValueLon": "{lon}°E", + "skyTime": "Thời gian bầu trời", + "weatherModeCloudy": "Nhiều mây", + "skyTimeDusk": "Chạng vạng", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "Firmware", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "Ngày kết thúc: đến 24:00(Đài Bắc)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "Địa điểm", - "reportFilterLocationHint": "vd: Hoa Liên, ngoài khơi", - "reportFilterAny": "Tất cả", - "reportFilterApply": "Áp dụng", - "reportFilterReset": "Đặt lại", - "reportListSearch": "Tìm", - "reportDetailTitle": "Báo cáo động đất", - "reportDetailNumbered": "Động đất có cảm nhận đáng kể số {number}", - "reportDetailLocalFelt": "Động đất cảm nhận cục bộ", - "reportDetailInfo": "Chi tiết", - "reportDetailOriginTime": "Thời gian xảy ra", - "reportDetailEpicenter": "Tọa độ tâm chấn", - "reportDetailMagnitude": "Độ lớn", - "reportDetailDepth": "Độ sâu chấn tiêu", - "reportDetailAreaIntensity": "Cường độ theo khu vực", - "reportDetailLocalIntensity": "Cường độ tại vị trí của bạn", - "reportDetailLocalIntensityUnavailable": "Không có dữ liệu cường độ", - "reportDetailSortByIntensity": "Sắp xếp theo cường độ", - "reportDetailSortByCounty": "Sắp xếp theo khu vực", - "reportDetailImage": "Hình ảnh báo cáo", - "reportDetailImageUnavailable": "Hình ảnh báo cáo chưa có sẵn", - "reportDetailOpenReport": "Trang báo cáo", - "reportDetailReplay": "Phát lại", - "navMore": "Thêm", - "appLogs": "Nhật ký ứng dụng", - "changelogTitle": "Nhật ký cập nhật", - "changelogEmpty": "Chưa có ghi chú phát hành", - "changelogTypePrerelease": "Thử nghiệm", - "changelogTypeStable": "Chính thức", - "changelogCurrentVersion": "Hiện tại", - "changelogVersionDetails": "Chi tiết phiên bản", - "changelogBodyEmpty": "Không có ghi chú cho bản phát hành này.", - "mapPlaceholderDisabled": "Bản đồ (tạm thời vô hiệu hóa)", - "moreSectionRegion": "Khu vực", - "moreSectionNotify": "Thông báo", - "moreSectionDisplay": "Hiển thị", - "regionManageTitle": "Khu vực đã lưu", - "regionAddButton": "Thêm khu vực", - "regionEmpty": "Chưa có khu vực nào được lưu", - "regionSelectTitle": "Chọn khu vực", - "regionSelectCount": "Đã chọn {count}/{max}", - "regionSelectFull": "Bạn chỉ có thể lưu tối đa {max} khu vực", - "regionEdit": "Sửa", - "moreSectionAdvanced": "Nâng cao", - "moreDeveloper": "Thông tin gỡ lỗi", - "experimentalFeatures": "Tính năng thử nghiệm", - "moreSectionLinks": "Liên kết", - "moreCwaEew": "Cảnh báo sớm động đất của CWA", - "moreTremReport": "Báo cáo phát hiện TREM", - "moreServerStatus": "Trạng thái máy chủ", - "moreAnnouncements": "Thông báo", - "moreDiscord": "Cộng đồng Discord", - "moreNotifyLog": "Nhật ký thông báo DPIP", - "moreLinkOpenFailed": "Không thể mở liên kết", - "weatherDynamicState": "Hoạt ảnh thời tiết", - "weatherDynamicStateSubtitle": "Ghi đè thời tiết nền của trang chủ", - "weatherModeAuto": "Tự động", - "weatherModeClear": "Trời quang", - "weatherModeRain": "Mưa", - "weatherModeFog": "Sương mù", - "weatherModeThunderstorm": "Mưa dông", - "commonLoading": "Đang tải…", - "commonRetry": "Thử lại", - "commonError": "Đã xảy ra lỗi", - "commonFetchFailed": "Không thể tải dữ liệu. Vui lòng thử lại.", - "commonEmpty": "Không có dữ liệu", - "feedConnecting": "Đang kết nối…", - "feedStale": "Dữ liệu có thể đã lỗi thời", - "feedOffline": "Mất kết nối", - "eewTitle": "Cảnh báo sớm động đất", - "eewNone": "Hiện không có cảnh báo sớm động đất", - "eewSummary": "M{magnitude} · độ sâu {depth} km", - "regionNationwide": "Toàn quốc", - "regionCurrent": "Vị trí hiện tại", - "regionCurrentUnavailable": "Không thể lấy vị trí hiện tại", - "weatherPrecipitation": "Lượng mưa", - "weatherHumidity": "Độ ẩm", - "weatherDataTime": "{station} · Thời gian dữ liệu {time}", - "homeViewOnMap": "Xem trên bản đồ", - "homeForecastTitle": "Dự báo 24 giờ", + "meshtasticSilent": "Silent", + "reportFilterSortMagnitude": "Độ lớn", + "mapLayerCategoryEarthquake": "Động đất", + "mapLayerSatelliteB12": "Himawari Ozone (B12)", + "typhoonLegendPast": "Quỹ đạo thực tế", + "restroomCategoryOther": "Khác", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "Cao {high}° · Thấp {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "Cảm giác {temp}°", - "homeForecastHumidity": "Độ ẩm {value}%", - "homeForecastWind": "{direction} · Cấp {level}", - "homeForecastUnavailable": "Chọn khu vực để xem dự báo", - "homeForecastEmpty": "Không có dữ liệu dự báo", - "homeActiveEventsTitle": "Sự kiện đang hiệu lực", - "homeActiveEventsEmpty": "Không có sự kiện đang hiệu lực", - "homeRainTrendTitle": "Mưa 1 giờ tới", - "homeRainTrendMinute": "{minute} phút", - "homeRainTrendUpdated": "Cập nhật {time}", - "homeRainTrendNoData": "Không có dữ liệu", - - "homeRainTrendScattered": "Có thể có mưa rào nhẹ", - "homeRainTrendLightSustained": "Mưa nhỏ tiếp diễn trong 1 giờ tới", - "homeRainTrendLightStopping": "Mưa nhỏ có thể tạnh trong {minutes} phút nữa", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "Mở cài đặt", + "mapLegendExpand": "Chú giải", + "eewNone": "Hiện không có cảnh báo sớm động đất", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "Tin và cảnh báo sóng thần", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "Node options", + "onboardingAgreeContinue": "Đồng ý và tiếp tục", + "meshtasticNodeId": "Node ID", + "commonRetry": "Thử lại", + "reportDetailNumbered": "Động đất có cảm nhận đáng kể số {number}", + "typhoonOverlayStormBandSubtitle": "With average circle", + "disasterMapOverlayRestroomTooltip": "Hiển thị nhà vệ sinh công cộng", + "weatherRankingTitle": "Xếp hạng quan trắc", "homeRainTrendHeavySustained": "Mưa lớn tiếp diễn trong 1 giờ tới", - "homeRainTrendHeavyStopping": "Mưa lớn có thể tạnh trong {minutes} phút nữa", - "mapLayers": "Lớp bản đồ", - "mapLayerOrderTitle": "Sắp xếp thứ tự lớp", - "mapLayerOrderReset": "Đặt lại thứ tự mặc định", - "mapLayerRadar": "Radar phản xạ tổng hợp", - "mapLayerSatellite": "Himawari Infrared (B13)", - "mapLayerSatelliteB01": "Himawari Blue (B01)", - "mapLayerSatelliteB02": "Himawari Green (B02)", - "mapLayerSatelliteB03": "Himawari Red (B03)", - "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "notifySectionTsunami": "Sóng thần", + "restroomCategoryPark": "Công viên", + "moreLinkOpenFailed": "Không thể mở liên kết", + "themeDark": "Tối", + "sponsorRestore": "Khôi phục giao dịch", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "Setting up the DPIP channel…", + "meshtasticRegionSwitch": "Switch to TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "Traffic", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", + "disasterMapOverlayAedTooltip": "Hiện vị trí AED", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "Độ ẩm", + "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", + "meshtasticScanning": "Scanning…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "Bạn chỉ có thể lưu tối đa {max} khu vực", + "meshtasticTitle": "Meshtastic", + "navMore": "Thêm", + "meshtasticDpipChannel": "DPIP channel", + "disasterMapOverlaySectionLayers": "Lớp", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "Himawari Near-Infrared (B05)", - "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", - "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", - "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", - "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", - "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", - "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", - "mapLayerSatelliteB12": "Himawari Ozone (B12)", - "mapLayerSatelliteB13": "Himawari Infrared (B13)", - "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", - "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", - "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "NE", + "meshtasticCopied": "Message copied", + "reportListEmpty": "Không có báo cáo động đất", + "reportListEnd": "Hết danh sách", "mapLayerSatelliteTruecolor": "Himawari True Color", - "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", - "mapLayerSatelliteAsh": "Himawari Ash", - "mapLayerSatelliteDust": "Himawari Dust", - "mapLayerSatelliteAirmass": "Himawari Airmass", - "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", - "mapLayerSatelliteWatervapor": "Himawari Water Vapour", - "mapLayerSatelliteBtdSplit": "Himawari Split Window", - "mapLayerSatelliteBtdFog": "Himawari Night Fog", - "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", - "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", - "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", - "mapLayerSatelliteBtdOzone": "Himawari Tropopause", - "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", - "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", - "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", - "mapLayerSatelliteNdvi": "Himawari NDVI", - "mapLayerSatelliteNdwi": "Himawari NDWI", - "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionExtra": "Overlays", + "eewSWave": "Sóng S", + "meshtasticBusyTitle": "Another app is using this radio", + "restroomCategoryCultural": "Địa điểm văn hóa giải trí", + "typhoonLabelWind": "Max. sustained wind near centre", + "radarGlobalOutlineHint": "Khung ngoài của mỗi quốc gia", + "notifyEvacuation": "Thông tin thảm họa", + "typhoonLegendCircle15": "Vòng gió mạnh", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "Astronomy", + "homeRainTrendLightSustained": "Mưa nhỏ tiếp diễn trong 1 giờ tới", + "commonError": "Đã xảy ra lỗi", + "moonPhaseWaningCrescent": "Waning crescent", + "meshtasticPower": "Power", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "Bây giờ", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "Trang báo cáo", + "trendRange7d": "7 ngày", + "typhoonWarningAreas": "Khu vực: {areas}", + "rainIntervalSection": "Khoảng thời gian", + "notifyTitle": "Thông báo", + "meshtasticTxPower": "TX power", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "Hạng mục", + "sponsorRestoring": "Đang khôi phục giao dịch…", + "sponsorIntro": "DPIP cam kết cung cấp thông tin phòng chống thiên tai theo thời gian thực, không có quảng cáo hay mô hình lợi nhuận nào khác. Sự ủng hộ của bạn giúp chúng tôi duy trì máy chủ và tiếp tục phát triển.", + "shelterAddressLabel": "Địa chỉ", + "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "Cơ sở thương mại", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "Khu vực", + "homeRainTrendLightStopping": "Mưa nhỏ có thể tạnh trong {minutes} phút nữa", + "reportDetailInfo": "Chi tiết", + "mapNavWind": "Gió", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "Tùy chọn lớp dự báo gió", + "dataWeatherRankingSubtitle": "Xếp hạng trạm trực tiếp", + "rainInterval6h": "6 giờ", + "homeRainTrendMinute": "{minute} phút", + "restroomTypeUnspecified": "Không xác định", + "typhoonOverlayProbabilityHint": "Hides the forecast cone", "mapLayerSatelliteGlobalOutline": "Country border", - "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", - "mapLayerSatelliteCloudClear": "Clear", - "mapLayerSatelliteCloudProbablyClear": "Probably clear", - "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "mapNavTemperature": "Nhiệt độ", + "typhoonLegendForecastPoint": "Điểm dự báo", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "Hôm qua", + "moreSectionLinks": "Liên kết", + "feedOffline": "Mất kết nối", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "Hiển thị", + "rainInterval3d": "3 ngày", + "defaultMapLayerSubtitle": "Tab Bản đồ mở lớp này. Biểu tượng và nhãn thanh điều hướng dưới cũng theo lựa chọn.", + "aedDescription": "Ghi chú", + "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", + "onboardingPermLocationDesc": "Gửi cảnh báo phù hợp với nơi bạn đang ở.", + "mapLayerSatelliteB16": "Himawari CO₂ (B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "Không có sự kiện đang hiệu lực", + "typhoonLabelPosition": "Centre location", + "weatherRankingBy": "Theo", + "typhoonIntensityMild": "Mild typhoon", + "windForecastGlobalOutlineHint": "Khung ngoài của mỗi quốc gia", + "rainInterval1h": "1 giờ", + "eewLocalIntensity": "Ước tính tại vị trí", + "mapLayerRadar": "Radar phản xạ tổng hợp", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "Nơi tôn giáo", + "meshtasticRole": "Role", "mapLayerSatelliteCloudCloudy": "Cloudy", - "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", - "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", - "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", - "mapLayerSatelliteTransparentNight": "Night = transparent, the basemap shows", - "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", - "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", - "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "skyTimeSunrise": "Bình minh", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "No messages yet", + "onboardingPermNotifyDesc": "Gửi cảnh báo động đất, thời tiết và thảm họa ngay khi chúng xảy ra.", + "radarTownOutline": "Ranh giới xã phường", "mapLayerStyleSection": "Colour style", - "mapLayerStyleTooltip": "Colour style", - "mapLayerStyleGray": "Grayscale (JMA)", - "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", - "mapLayerStyleJma": "Cloud-top enhancement (JMA)", - "mapLayerStyleJmaTooltip": "Grayscale base, tinted below −40 °C to highlight cloud-top height", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis", - "mapLayerQpesums": "Dự báo mưa 1 giờ tới", - "mapLayerLightning": "Sét", - "lightningLegendCg": "Mây–đất · {minutes} phút", - "lightningLegendCc": "Mây–mây · {minutes} phút", - "mapTimelineNow": "Bây giờ", - "mapTimelinePast": "Quá khứ", - "mapTimelineFuture": "Tương lai", - "mapTimelineObserved": "Quan trắc", - "mapTimelineForecast": "Dự báo", - "mapTimelineDataTime": "Thời gian dữ liệu {time}", - "notifySettingsMenu": "Cài đặt thông báo", - "notifyTitle": "Thông báo", - "notifyUnavailable": "Thông báo đẩy chưa sẵn sàng — vui lòng thử lại sau giây lát.", - "notifySetFailed": "Không thể lưu cài đặt. Vui lòng thử lại.", - "notifySectionEew": "Cảnh báo sớm động đất", - "notifySectionEarthquake": "Động đất", - "notifySectionWeather": "Thời tiết", - "notifySectionTsunami": "Sóng thần", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "Lớp bản đồ phòng chống", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "Heard recently", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "SW", + "typhoonForecastLead": "Forecast +{hours} h", + "dpmDisasterTsunami": "Sóng thần", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "Chính thức", + "mapLayerSatelliteTransparentClear": "Clear sky = transparent, the basemap shows", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "Lớp tham chiếu", + "mapLayerSatelliteB02": "Himawari Green (B02)", + "reportListLocalFelt": "Cảm nhận cục bộ", + "weatherRankingEmpty": "Không có quan trắc để xếp hạng", "notifySectionOther": "Khác", - "notifyEew": "Cảnh báo động đất khẩn cấp", - "notifyMonitor": "Giám sát rung chấn mạnh", - "notifyReport": "Báo cáo động đất", - "notifyIntensity": "Báo cáo cường độ chấn động", - "notifyThunderstorm": "Cảnh báo mưa dông", - "notifyAdvisory": "Tin cảnh báo thời tiết", - "notifyEvacuation": "Thông tin thảm họa", - "notifyTsunami": "Thông tin sóng thần", - "notifyAnnouncement": "Thông báo", - "notifyOptOff": "Tắt", - "notifyOptAll": "Nhận tất cả", + "weatherRankingMeta": "Thời gian: {time}\n{count} trạm", + "onboardingTermsAgree": "Tôi đã đọc và đồng ý với Điều khoản Dịch vụ", + "mapLayerSatelliteTransparentNoVegetation": "Below 0.1 = transparent (no vegetation)", "notifyOptLocalIntensity4": "Cường độ tại chỗ từ 4 trở lên", - "notifyOptLocalIntensity1": "Cường độ tại chỗ từ 1 trở lên", - "notifyOptWeatherLocal": "Chỉ vị trí hiện tại", - "notifyOptTsunamiWarning": "Chỉ cảnh báo sóng thần", - "notifyOptTsunamiAll": "Tin và cảnh báo sóng thần", - "onboardingNext": "Tiếp theo", - "onboardingBack": "Quay lại", + "eewArrived": "Đã đến", + "meshtasticNoDevices": "No Meshtastic devices found", + "mapLayerCategoryLife": "Đời sống", + "reportFilterSortIntensity": "Cường độ", + "typhoonMotion": "Di chuyển", + "meshtasticStateDisconnected": "Disconnected", + "typhoonIntensityIntense": "Intense typhoon", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "Sắp xếp thứ tự lớp", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "Có", + "meshtasticNoHistory": "Not enough history yet", + "reportDetailLocalIntensityUnavailable": "Không có dữ liệu cường độ", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "km", + "reportFilterDepth": "Độ sâu", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "Cuộn xuống để tiếp tục", - "onboardingIntroTitle": "Chào mừng đến với DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "Dự báo", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "Bản đồ", + "notifyAdvisory": "Tin cảnh báo thời tiết", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "Đặt lại", + "mapLayerSatelliteMndwi": "Himawari MNDWI", + "typhoonOverlaySectionStorm": "Storm wind", + "moonPhaseFull": "Full moon", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "Waning gibbous", + "weatherDynamicStateSubtitle": "Ghi đè thời tiết nền của trang chủ", + "reportFilterIntensityInfoModernTitle": "Mới (từ 2020)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "Data time\n{time}", + "restroomTypeAccessible": "Nhà vệ sinh tiếp cận được", + "moreSectionAbout": "Giới thiệu", + "meshtasticSelectDevice": "Select a radio", "onboardingIntroBody": "DPIP là người bạn đồng hành phòng chống thiên tai của bạn. Ứng dụng tích hợp cảnh báo sớm động đất, báo cáo động đất, thời tiết và thông tin về hiểm họa, đồng thời cảnh báo bạn ngay tại thời điểm quan trọng.\n\n• Động đất: cảnh báo sớm, báo cáo cường độ và báo cáo chi tiết\n• Thời tiết: tin nhắn mưa dông theo thời gian thực và cảnh báo thời tiết\n• Thông tin sóng thần và thảm họa\n\nTiếp theo, chúng tôi sẽ mời bạn xem lại Điều khoản Dịch vụ và cấp một vài quyền để DPIP có thể bảo vệ bạn theo thời gian thực.", - "onboardingTermsTitle": "Điều khoản Dịch vụ", - "onboardingTermsBody": "Vui lòng đọc kỹ các lưu ý sau đây trước khi sử dụng DPIP:\n\n• Mọi thông tin phải căn cứ theo nội dung do Cục Khí tượng Trung ương Đài Loan (CWA) công bố.\n\n• Tùy thuộc vào tình trạng mạng, máy chủ, ứng dụng và nguồn dữ liệu đầu nguồn, có khả năng không nhận được thông tin; chúng tôi nỗ lực hết sức để tránh điều này nhưng không thể bảo đảm rằng nó không bao giờ xảy ra.\n\n• Rung lắc mạnh có thể lan đến vị trí của bạn trước khi thông báo được gửi tới.\n\n• Cảnh báo sớm động đất là kết quả được tính toán nhanh nên có thể chứa sai số đáng kể — hãy hiểu rõ điều này và sử dụng một cách thận trọng.\n\n• Bất kỳ hành vi nào không được cơ quan chức năng cho phép đều có thể mang rủi ro pháp lý; vui lòng tuân thủ mọi quy định hiện hành.\n\nNgoài ra, để cung cấp cảnh báo theo khu vực, dịch vụ này thu thập và tải lên vị trí gần đúng cùng mã định danh thông báo đẩy của bạn — cả ở nền trước lẫn nền sau — chỉ nhằm quyết định những cảnh báo nào sẽ gửi cho bạn.\n\nBằng việc nhấn \"Đồng ý và tiếp tục\", bạn xác nhận rằng đã đọc, hiểu và đồng ý với những điều trên.", - "onboardingTermsAgree": "Tôi đã đọc và đồng ý với Điều khoản Dịch vụ", - "onboardingAgreeContinue": "Đồng ý và tiếp tục", - "onboardingPermsTitle": "Quyền truy cập", - "onboardingPermsBody": "Để DPIP có thể cảnh báo bạn ngay khi thảm họa xảy ra, vui lòng cấp các quyền sau. Bạn có thể thay đổi chúng bất cứ lúc nào trong cài đặt hệ thống.", + "shelterCapacityLabel": "Sức chứa", + "reportDetailImage": "Hình ảnh báo cáo", + "meshtasticStateConfiguring": "Configuring…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", "onboardingPermNotify": "Thông báo", - "onboardingPermNotifyDesc": "Gửi cảnh báo động đất, thời tiết và thảm họa ngay khi chúng xảy ra.", - "onboardingPermCritical": "Cảnh báo quan trọng", - "onboardingPermCriticalDesc": "Cho phép các cảnh báo động đất nguy hiểm đến tính mạng phát âm thanh ngay cả khi ở chế độ im lặng hoặc Không làm phiền.", - "onboardingPermLocation": "Vị trí", - "onboardingPermLocationDesc": "Gửi cảnh báo phù hợp với nơi bạn đang ở.", - "onboardingPermBackground": "Vị trí chạy nền", - "onboardingPermBackgroundDesc": "Cho phép \"Luôn luôn\" để cảnh báo vẫn nhắm đúng vị trí của bạn ngay cả khi đã đóng ứng dụng.", - "onboardingPermBattery": "Miễn trừ tối ưu hóa pin", - "onboardingPermBatteryDesc": "Cho phép DPIP tiếp tục chạy ở chế độ nền để cảnh báo không bị trì hoãn hay bỏ lỡ.", - "onboardingGrant": "Cấp quyền", - "onboardingGranted": "Đã cấp", - "onboardingStart": "Bắt đầu", - "language": "Ngôn ngữ", - "languageSettings": "Ngôn ngữ", - "languageSystem": "Mặc định hệ thống", - "locationBannerServiceOff": "Dịch vụ vị trí đang tắt — cảnh báo khu vực không thể nhắm đúng vùng của bạn.", - "locationBannerPermission": "Chưa cấp quyền vị trí — cảnh báo khu vực không thể nhắm đúng vùng của bạn.", - "locationBannerFix": "Mở cài đặt", - "notifyBannerDisabled": "Thông báo đã tắt — bạn sẽ không nhận được cảnh báo thiên tai.", - "onboardingSkipTitle": "Chưa cấp quyền", - "onboardingSkipBody": "Nếu không có quyền vị trí và thông báo, DPIP không thể cảnh báo tức thời về động đất và thiên tai gần bạn. Bạn vẫn có thể cấp quyền sau trong Cài đặt.", - "onboardingSkipStay": "Quay lại", - "onboardingSkipLeave": "Vẫn bỏ qua", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "Clear messages", + "meshtasticNotifyMessages": "Notify on new messages", + "defaultMapLayerSettings": "Lớp bản đồ mặc định", + "moreSectionNotify": "Thông báo", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "Thông báo đẩy chưa sẵn sàng — vui lòng thử lại sau giây lát.", + "mapLayerOrderReset": "Đặt lại thứ tự mặc định", + "dpmAddress": "Địa chỉ", + "weatherRankingMergeCounty": "Huyện/thành", + "moreSectionApp": "Tải ứng dụng", + "reportFilterIntensityInfoLegacyBody": "Chỉ có mức 0–7, không tách 5−/5+/6−/6+.", + "mapLayerSatelliteSst": "Himawari Sea Surface Temperature", + "qpesumsOverlayMenuTooltip": "Tùy chọn lớp dự báo mưa định lượng", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "Tương lai", + "typhoonLegendCircleAvg": "Average circle", + "reportFilterDepthKm": "{depth} km", + "typhoonLabelSe": "SE", + "radarTownOutlineHint": "Lưới chi tiết hơn", + "eewCountdown": "{seconds} giây", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "Peak gust", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "Điều khoản sử dụng", + "restroomTypeGenderNeutral": "Nhà vệ sinh trung tính giới", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "Cảnh báo mưa dông", + "skyTimeGolden": "Giờ vàng", + "moonAge": "Age", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "Hiện tại {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "Chọn khu vực để xem dự báo", + "mapLayers": "Lớp bản đồ", + "meshtasticHardware": "Hardware", + "languageSettings": "Ngôn ngữ", + "dpmDisasterNuclear": "Sự cố hạt nhân", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "Ngôn ngữ", + "homeForecastFeelsLike": "Cảm giác {temp}°", + "typhoonOverlayWeatherHint": "Aligned to bulletin time", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "Rạng đông", + "skyTimeAfternoon": "Buổi chiều", + "meshtasticLastHeard": "Last heard", + "typhoonWarningTitle": "Cảnh báo bão", "moreSourceCode": "Mã nguồn", - "moreSectionApp": "Tải ứng dụng", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "Hiển thị", - "defaultMapLayerSettings": "Lớp bản đồ mặc định", - "defaultMapLayerSubtitle": "Tab Bản đồ mở lớp này. Biểu tượng và nhãn thanh điều hướng dưới cũng theo lựa chọn.", - "mapNavRadar": "Radar", - "mapNavQpesums": "Dự báo", - "mapNavSatellite": "Vệ tinh", - "mapNavLightning": "Sét", - "mapNavTyphoon": "Bão", + "mapLayerCategoryWeather": "Quan sát thời tiết", + "mapLayerSatelliteB09": "Himawari Mid Water Vapour (B09)", + "windForecastTownOutlineHint": "Lưới mịn hơn", + "mapLayerSatelliteCloudmask": "Himawari Cloud Mask", + "mapAppCopyCoordinates": "Sao chép tọa độ", + "reportFilterIntensityInfoIntro": "CWA đổi thang cường độ từ 1/1/2020 (giờ Đài Bắc).", "mapNavEarthquake": "Động đất", - "mapNavTemperature": "Nhiệt độ", - "mapNavHumidity": "Độ ẩm", - "mapNavPressure": "Khí áp", - "mapNavWind": "Gió", + "typhoonGust": "Gió giật", + "restroomGradeAverage": "Trung bình", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "Himawari Cirrus / Cloud Height", + "onboardingPermBackgroundDesc": "Cho phép \"Luôn luôn\" để cảnh báo vẫn nhắm đúng vị trí của bạn ngay cả khi đã đóng ứng dụng.", + "mapTimelineForecast": "Dự báo", + "restroomTypeLabel": "Loại", + "navEarthquake": "Động đất", + "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", + "moonPhaseWaxingGibbous": "Waxing gibbous", + "reportDetailTitle": "Báo cáo động đất", + "moreTremReport": "Báo cáo phát hiện TREM", + "weatherDataTime": "{station} · Thời gian dữ liệu {time}", + "meshtasticNoNodes": "No nodes heard yet", + "meshtasticViaMqtt": "Via MQTT (internet)", + "radarCountyOutline": "Ranh giới huyện thị", + "onboardingGranted": "Đã cấp", + "@mapAppCopyCoordinates": {}, + "commonClose": "Đóng", + "restroomGradeLabel": "Hạng", + "rainIntervalNow": "Hôm nay", + "changelogCurrentVersion": "Hiện tại", + "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", + "typhoonLabelPressure": "Central pressure", + "aedOpenRemark": "Ghi chú giờ mở", + "onboardingPermsBody": "Để DPIP có thể cảnh báo bạn ngay khi thảm họa xảy ra, vui lòng cấp các quyền sau. Bạn có thể thay đổi chúng bất cứ lúc nào trong cài đặt hệ thống.", + "typhoonOverlaySectionWeather": "Weather underlay", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "Chỉ vị trí hiện tại", "mapNavRain": "Mưa", - "mapNavDisaster": "Phòng thảm", - "displayTheme": "Giao diện", + "moonDays": "days", + "mapLegendUnit": "Đơn vị: {unit}", + "weatherModeClear": "Trời quang", + "meshtasticRadio": "Radio", + "commonEmpty": "Không có dữ liệu", + "mapLayerSatelliteB01": "Himawari Blue (B01)", + "meshtasticExternalPower": "External power", + "moonPhaseLastQuarter": "Last quarter", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "Tăng dần", + "reportFilterApply": "Áp dụng", + "reportDetailImageUnavailable": "Hình ảnh báo cáo chưa có sẵn", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "Cao nhất", + "reportDetailReplay": "Phát lại", + "mapLayerRestroom": "Nhà vệ sinh công cộng", + "restroomCategoryWelfare": "Cơ sở phúc lợi", + "restroomGradeExcellent": "Xuất sắc", + "meshtasticLastSent": "Last sent", + "meshtasticName": "Name", + "meshtasticScan": "Scan", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "Dự báo số", + "meshtasticChannelFailed": "Couldn't set up the DPIP channel", "themeSystem": "Hệ thống", - "themeLight": "Sáng", - "themeDark": "Tối", - "moreSectionAbout": "Giới thiệu", - "termsOfService": "Điều khoản dịch vụ", - "faq": "Câu hỏi thường gặp", - "openSourceLicenses": "Giấy phép mã nguồn mở", - "sponsorTitle": "Ủng hộ DPIP", - "sponsorIntro": "DPIP cam kết cung cấp thông tin phòng chống thiên tai theo thời gian thực, không có quảng cáo hay mô hình lợi nhuận nào khác. Sự ủng hộ của bạn giúp chúng tôi duy trì máy chủ và tiếp tục phát triển.", - "sponsorSubscriptions": "Gói đăng ký", - "sponsorRecommended": "Đề xuất", - "sponsorOneTime": "Ủng hộ một lần", - "sponsorPerMonth": "{price} / tháng", - "sponsorRestore": "Khôi phục giao dịch", - "sponsorTerms": "Điều khoản sử dụng", - "sponsorPrivacy": "Chính sách quyền riêng tư", - "sponsorRestoring": "Đang khôi phục giao dịch…", - "sponsorRestoreUnavailable": "Không thể kết nối tới cửa hàng. Vui lòng thử lại sau.", - "commonClose": "Đóng", + "mapLayerSatelliteNdvi": "Himawari NDVI", + "typhoonLegendForecast": "Quỹ đạo dự báo", + "typhoonValueHpa": "{n} hPa", + "weatherPrecipitation": "Lượng mưa", + "moonNextFullMoon": "Next full moon", + "dpmSheetEmpty": "Chạm vào điểm đánh dấu trên bản đồ để xem chi tiết", + "onboardingSkipLeave": "Vẫn bỏ qua", + "onboardingBack": "Quay lại", + "aedPlaceDesc": "Vị trí đặt", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "Chưa cấp quyền", + "restroomTypeFamily": "Nhà vệ sinh gia đình", + "typhoonValueKm": "{n} km", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "Áp suất", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "Miễn trừ tối ưu hóa pin", + "typhoonLabelNw": "NW", + "dpmDisasterFlood": "Lũ lụt", + "moonPhaseWaxingCrescent": "Waxing crescent", + "restroomCategoryLeisure": "Địa điểm vui chơi giải trí", "mapLayerTemperature": "Nhiệt độ", - "trendRange24h": "24 giờ", - "trendRange7d": "7 ngày", - "trendNoData": "Không có dữ liệu xu hướng", - "trendCumulativeTotal": "Tổng cộng {total} mm", - "chartHourLabel": "{hour}h", - "mapLayerHumidity": "Độ ẩm", - "mapLayerPressure": "Áp suất", + "aedCategory": "Phân loại", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "Channels", + "monitorWaiting": "Đang chờ dữ liệu…", + "typhoonOverlayForecastCallouts": "Forecast tooltips", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "Tọa độ tâm chấn", + "meshtasticVoltage": "Voltage", + "mapLayerMeshtasticSubtitle": "LoRa mesh nodes heard by your radio", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "Gió", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "Lượng mưa", - "rainIntervalMenu": "Khung tích lũy", - "rainIntervalNow": "Hôm nay", - "rainInterval10m": "10 phút", - "rainInterval1h": "1 giờ", - "rainInterval3h": "3 giờ", - "rainInterval6h": "6 giờ", + "reportDetailMagnitude": "Độ lớn", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "Cường độ theo khu vực", "rainInterval12h": "12 giờ", - "rainInterval24h": "24 giờ", - "rainInterval2d": "2 ngày", - "rainInterval3d": "3 ngày", - "mapLayerTyphoon": "Bão", - "typhoonNoActive": "Không có bão", - "typhoonWind": "Sức gió", - "typhoonGust": "Gió giật", - "typhoonPressure": "Áp suất", - "typhoonMotion": "Di chuyển", - "mapLayerMonitor": "Giám sát địa chấn", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "Bản đồ phòng chống", - "disasterMapOverlayMenuTooltip": "Lớp bản đồ phòng chống", - "disasterMapOverlaySectionLayers": "Lớp", - "disasterMapOverlayAedTooltip": "Hiện vị trí AED", - "aedAddress": "Địa chỉ", - "aedRegion": "Khu vực", - "aedCategory": "Phân loại", - "aedType": "Loại", - "aedPlaceDesc": "Vị trí đặt", - "aedDescription": "Ghi chú", - "aedHoursWeekday": "Giờ ngày thường", - "aedHoursSaturday": "Giờ thứ Bảy", - "aedHoursSunday": "Giờ Chủ nhật", - "aedOpenRemark": "Ghi chú giờ mở", - "aedEmergencyPhone": "Điện thoại khẩn cấp", - "mapLayerRestroom": "Nhà vệ sinh công cộng", - "mapLayerShelter": "Nơi trú ẩn", - "disasterMapOverlayRestroomTooltip": "Hiển thị nhà vệ sinh công cộng", - "disasterMapOverlayShelterTooltip": "Hiển thị nơi trú ẩn", - "dpmOpenInMaps": "Mở trong bản đồ", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "Sạt lở đất", + "notifyMonitor": "Giám sát rung chấn mạnh", + "onboardingStart": "Bắt đầu", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / tháng", + "mapLayerPressure": "Áp suất", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "Himawari Near-Infrared (B04)", + "mapLayerSatelliteTransparentZero": "Zero difference = transparent (no signal)", + "shelterIndoorLabel": "Trú ẩn trong nhà", + "notifyOptOff": "Tắt", + "reportFilterSortTime": "Thời gian", + "mapLayerSatelliteCloudProbablyClear": "Probably clear", + "weatherModeThunderstorm": "Mưa dông", + "homeViewOnMap": "Xem trên bản đồ", + "reportFilterIntensityInfoLegacyTitle": "Cũ (trước 2020)", + "typhoonLabelSpeed": "Past movement speed", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "Không thể mở {app}", + "mapLayerSatelliteRgbComposite": "RGB composite (JMA recipe)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "Received", + "weatherRankingExtremeLow": "Thấp nhất ngày", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "Himawari Lower Water Vapour (B10)", + "mapLayerSatelliteCloudProbablyCloudy": "Probably cloudy", + "shelterCategoryLabel": "Loại thảm họa", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = transparent (no water)", + "meshtasticStateConnecting": "Connecting…", + "moonTitle": "Moon", + "weatherRankingGust": "Gió giật", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "Loại thiên tai nơi trú ẩn", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "Trạng thái máy chủ", + "notifySectionWeather": "Thời tiết", + "meshtasticPreset": "Modem preset", + "dataSectionSeismic": "Địa chấn", + "changelogBodyEmpty": "Không có ghi chú cho bản phát hành này.", + "radarGlobalOutline": "Biên giới quốc gia", + "notifyEew": "Cảnh báo động đất khẩn cấp", + "regionNationwide": "Toàn quốc", + "moreNotifyLog": "Nhật ký thông báo DPIP", + "regionCurrent": "Vị trí hiện tại", + "dpmFilterSectionRestroom": "Loại địa điểm", + "meshtasticNotConnected": "Not connected to a radio", + "weatherModeSnow": "Tuyết rơi", + "mapLayerMeshtastic": "Meshtastic nodes", + "moreDeveloper": "Thông tin gỡ lỗi", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "Himawari Longwave Infrared (B14)", + "meshtasticChannelUse": "Channel use", + "mapNavLightning": "Sét", + "homeForecastEmpty": "Không có dữ liệu dự báo", + "sponsorOneTime": "Ủng hộ một lần", + "mapLayerSatelliteBtdSplit": "Himawari Split Window", + "onboardingPermBackground": "Vị trí chạy nền", + "aedEmergencyPhone": "Điện thoại khẩn cấp", + "dpmOpenInMaps": "Mở trong bản đồ", + "meshtasticNotifyNodes": "Notify on new nodes", + "onboardingPermCriticalDesc": "Cho phép các cảnh báo động đất nguy hiểm đến tính mạng phát âm thanh ngay cả khi ở chế độ im lặng hoặc Không làm phiền.", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "Clear sky (warm end) = transparent, the basemap shows", + "meshtasticSent": "Sent", + "homeForecastTitle": "Dự báo 24 giờ", + "typhoonLegendWarningAreas": "Vùng cảnh báo", + "meshtasticExcludeMqttHidden": "{count} hidden", + "notifyOptLocalIntensity1": "Cường độ tại chỗ từ 1 trở lên", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "Quá khứ", + "restroomTypeFemale": "Nhà vệ sinh nữ", + "reportListToday": "Hôm nay", + "meshtasticTapNode": "Tap a node for details", + "commonLoading": "Đang tải…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "Moderate typhoon", + "typhoonWind": "Sức gió", + "mapLayerSatelliteAsh": "Himawari Ash", + "rainInterval3h": "3 giờ", + "reportListSearch": "Tìm", + "meshtasticChannelReady": "DPIP channel ready", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "Vệ tinh", + "reportFilterLocation": "Địa điểm", + "mapLayerSatelliteNightmicrophysics": "Himawari Night Microphysics", + "typhoonIntensityTd": "Tropical depression", + "reportFilterDate": "Ngày", + "sponsorRestoreUnavailable": "Không thể kết nối tới cửa hàng. Vui lòng thử lại sau.", + "homeForecastPop": "{pop}%", + "regionEmpty": "Chưa có khu vực nào được lưu", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "Cho phép DPIP tiếp tục chạy ở chế độ nền để cảnh báo không bị trì hoãn hay bỏ lỡ.", + "mapNavDisaster": "Phòng thảm", + "radarScanRangeSubtitle": "Đánh dấu vùng bốn radar thực sự quan trắc.", + "aedHoursSunday": "Giờ Chủ nhật", + "reportDetailOriginTime": "Thời gian xảy ra", + "trendNoData": "Không có dữ liệu xu hướng", + "onboardingPermLocation": "Vị trí", + "moreDiscord": "Cộng đồng Discord", + "mapNavPressure": "Khí áp", + "mapLayerSatelliteB13": "Himawari Infrared (B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "Chưa có ghi chú phát hành", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "Ngày bắt đầu: từ 00:00(Đài Bắc)", + "eewTitle": "Cảnh báo sớm động đất", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "vi", + "regionSelectCount": "Đã chọn {count}/{max}", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "Himawari SO₂ / Cloud Phase", + "meshtasticStateError": "Error", + "weatherModeOvercast": "Trời âm u", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "Độ sâu chấn tiêu", + "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", + "reportFilterDatePick": "Chọn ngày", + "onboardingSkipStay": "Quay lại", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "Không thể tải dữ liệu. Vui lòng thử lại.", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "Trú ẩn ngoài trời", + "meshtasticStateConnected": "Connected", + "mapNavRadar": "Radar", + "mapLayerSatelliteCloudClear": "Clear", + "eewSummary": "M{magnitude} · độ sâu {depth} km", + "locationBannerPermission": "Chưa cấp quyền vị trí — cảnh báo khu vực không thể nhắm đúng vùng của bạn.", + "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", + "radarCountyOutlineHint": "Vẽ đè lên tiếng vọng", + "windForecastCountyOutlineHint": "Vẽ trên trường gió", + "homeRainTrendTitle": "Mưa 1 giờ tới", + "moonPhaseFirstQuarter": "First quarter", + "mapLayerCategoryTyphoon": "Bão", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "Airtime (24h)", + "restroomTypeMixed": "Nhà vệ sinh chung", + "restroomGradeGood": "Tốt", + "notifyTsunami": "Thông tin sóng thần", + "navData": "Dữ liệu", + "mapLayerSatelliteBtdWvirw": "Himawari Overshooting Top", + "meshtasticReadingAge": "Reading taken", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "Thiết bị này không thể thực hiện cuộc gọi", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "Tất cả", + "weatherRankingMergeTo": "Gộp", + "notifyIntensity": "Báo cáo cường độ chấn động", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "Khung tích lũy", + "reportDetailLocalFelt": "Động đất cảm nhận cục bộ", + "meshtasticDevice": "Device", + "onboardingGrant": "Cấp quyền", + "weatherModeRain": "Mưa", + "shelterVulnerableOkLabel": "Phù hợp người yếu thế", + "stationSheetEmpty": "Chạm vào một trạm để xem số liệu", + "typhoonLegendProbability": "Xác suất đổ bộ", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "Độ lớn", + "skyTimeMorning": "Buổi sáng", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "Tính năng thử nghiệm", + "onboardingTermsBody": "Vui lòng đọc kỹ các lưu ý sau đây trước khi sử dụng DPIP:\n\n• Mọi thông tin phải căn cứ theo nội dung do Cục Khí tượng Trung ương Đài Loan (CWA) công bố.\n\n• Tùy thuộc vào tình trạng mạng, máy chủ, ứng dụng và nguồn dữ liệu đầu nguồn, có khả năng không nhận được thông tin; chúng tôi nỗ lực hết sức để tránh điều này nhưng không thể bảo đảm rằng nó không bao giờ xảy ra.\n\n• Rung lắc mạnh có thể lan đến vị trí của bạn trước khi thông báo được gửi tới.\n\n• Cảnh báo sớm động đất là kết quả được tính toán nhanh nên có thể chứa sai số đáng kể — hãy hiểu rõ điều này và sử dụng một cách thận trọng.\n\n• Bất kỳ hành vi nào không được cơ quan chức năng cho phép đều có thể mang rủi ro pháp lý; vui lòng tuân thủ mọi quy định hiện hành.\n\nNgoài ra, để cung cấp cảnh báo theo khu vực, dịch vụ này thu thập và tải lên vị trí gần đúng cùng mã định danh thông báo đẩy của bạn — cả ở nền trước lẫn nền sau — chỉ nhằm quyết định những cảnh báo nào sẽ gửi cho bạn.\n\nBằng việc nhấn \"Đồng ý và tiếp tục\", bạn xác nhận rằng đã đọc, hiểu và đồng ý với những điều trên.", + "reportFilterTitle": "Bộ lọc", + "onboardingPermCritical": "Cảnh báo quan trọng", + "trendCumulativeTotal": "Tổng cộng {total} mm", + "languageName": "Tiếng Việt", + "reportListEmptyFiltered": "Không có báo cáo khớp bộ lọc", + "meshtasticExcludeMqtt": "Hide MQTT nodes", + "mapNavTyphoon": "Bão", + "weatherModeSand": "Bụi cát", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "Vệ tinh", + "@dpmOpenInMaps": {}, + "notifyReport": "Báo cáo động đất", + "mapAppCoordinatesCopied": "Đã sao chép tọa độ", + "skyTimeNight": "Ban đêm", + "sponsorRecommended": "Đề xuất", + "mapLayerSatelliteB15": "Himawari Longwave Infrared (B15)", + "weatherRankingWind": "Tốc độ gió", + "feedStale": "Dữ liệu có thể đã lỗi thời", + "homeForecastWind": "{direction} · Cấp {level}", + "navHome": "Trang chủ", + "meshtasticRegionLabel": "Region", + "mapLayerSatelliteCloudtop": "Himawari Cloud Top Temperature", + "moonTimelineCaption": "Phase", + "reportListMeta": "M{magnitude} · {depth} km", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "Giấy phép mã nguồn mở", + "weatherRankingLowest": "Thấp nhất", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "Độ sâu", + "mapTimelineDataTime": "Thời gian dữ liệu {time}", + "radarScanRange": "Hiện phạm vi quét", + "meshtasticHopLimit": "Hop limit", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "Biên độ {value}°C", + "weatherRankingExtremeHigh": "Cao nhất ngày", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "Chi tiết phiên bản", + "sponsorPrivacy": "Chính sách quyền riêng tư", + "reportDetailLocalIntensity": "Cường độ tại vị trí của bạn", + "mapLayerSatelliteNaturalcolor": "Himawari Natural Color", + "meshtasticAirtime": "Air time (TX)", + "shelterCapacityValue": "{n} người", + "lightningLegendCc": "Mây–mây · {minutes} phút", + "meshtasticSendHint": "Message to broadcast", + "monitorDelay": "Độ trễ {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "Không", + "mapLayerSatelliteB08": "Himawari Upper Water Vapour (B08)", + "meshtasticReconnecting": "Reconnecting…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "Giữ ranh giới xã phường rõ ràng dưới lớp phản hồi radar.", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", + "radarScanRangeHint": "Ngoài khung là chưa quan trắc", + "typhoonPickerTd": "Tropical depression TD {no}", + "mapLayerSatelliteWatervapor": "Himawari Water Vapour", + "regionAddButton": "Thêm khu vực", + "displaySettings": "Hiển thị", + "restroomGradePoor": "Dưới chuẩn", + "restroomCategoryTourist": "Khu du lịch thắng cảnh", + "locationBannerServiceOff": "Dịch vụ vị trí đang tắt — cảnh báo khu vực không thể nhắm đúng vùng của bạn.", + "mapLayerStyleTooltip": "Colour style", + "lightningLegendCg": "Mây–đất · {minutes} phút", + "skyTimeAuto": "Tự động", + "appLogs": "Nhật ký ứng dụng", + "feedConnecting": "Đang kết nối…", + "notifyBannerDisabled": "Thông báo đã tắt — bạn sẽ không nhận được cảnh báo thiên tai.", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "Độ ẩm", + "typhoonValueMs": "{n} m/s", + "homeForecastHumidity": "Độ ẩm {value}%", + "meshtasticBusyBody": "Disconnect it in the other Meshtastic app first. Two apps on one radio take each other's messages, so some will go missing.", + "meshtasticChannelNoSlot": "No free channel slot — free one on the radio", + "restroomCategoryTransport": "Giao thông", + "reportFilterLocationHint": "vd: Hoa Liên, ngoài khơi", + "moonSubtitle": "Lunar phase and illumination — computed locally", + "meshtasticBattery": "Battery", + "meshtasticDistance": "Khoảng cách", + "meshtasticSnrTrend": "Xu hướng tín hiệu (SNR)", + "meshtasticBatteryTrend": "Xu hướng pin", + "typhoonOverlayMenuTooltip": "Typhoon overlay options", + "mapLayerSatelliteBtdOzone": "Himawari Tropopause", + "meshtasticRegionMismatch": "Radio region is {region} — DPIP needs TW", + "notifySectionEarthquake": "Động đất", + "mapLayerDisasterMap": "Bản đồ phòng chống", + "weatherModeFog": "Sương mù", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "JMA grayscale — colder is whiter", + "moreAnnouncements": "Thông báo", + "mapLayerSatelliteTransparentNoData": "No data (land) = transparent", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "Cơ quan công quyền", + "typhoonLegendCurrent": "Tâm hiện tại", + "aedAddress": "Địa chỉ", + "mapLayerAed": "AED", + "changelogTypePrerelease": "Thử nghiệm", + "reportFilterIntensityInfoModernBody": "Các mức 0–4, 5−, 5+, 6−, 6+, 7. Thanh lọc dùng thang mới; sự kiện cũ vẫn hiện nhãn cũ trong danh sách.", + "typhoonOverlayWeatherNone": "None", + "mapLayerStyleGray": "Grayscale (JMA)", + "weatherModeAuto": "Tự động", + "typhoonLabelProbCircle": "70% probability circle", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "Nhận tất cả", + "displayTheme": "Giao diện", + "mapLayerSatelliteB07": "Himawari Shortwave Infrared (B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "Past movement direction", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "Khu vực đã lưu", + "typhoonLegendCone": "Nón dự báo", + "moreCwaEew": "Cảnh báo sớm động đất của CWA", + "onboardingPermsTitle": "Quyền truy cập", + "mapLayerStyleJma": "Cloud-top enhancement (JMA)", + "rainInterval10m": "10 phút", + "weatherRankingAnalysisLow": "Thấp {value}", + "meshtasticConnectAnyway": "Connect anyway", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "Himawari Near-Infrared (B06)", + "mapLayerSatelliteTransparentReflectance": "Low reflectance / night = transparent, the basemap shows", + "chartHourLabel": "{hour}h", + "mapLayerShelter": "Nơi trú ẩn", + "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", + "mapLayerSatelliteNdwi": "Himawari NDWI", + "disasterMapOverlayShelterTooltip": "Hiển thị nơi trú ẩn", + "mapNavHumidity": "Độ ẩm", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "Sắp xếp theo cường độ", + "homeRainTrendNoData": "Không có dữ liệu", + "mapLayerCategoryRadar": "Ra đa", + "meshtasticShortName": "Short name", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "Himawari Airmass", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "Chi tiết quỹ đạo", + "dataSectionWeather": "Thời tiết", + "aedHoursWeekday": "Giờ ngày thường", + "homeActiveEventsTitle": "Sự kiện đang hiệu lực", + "weatherRankingAnalysisHigh": "Cao {value}", + "faq": "Câu hỏi thường gặp", + "typhoonHistoryLive": "Trực tiếp", + "eewSerial": "Bản tin {serial}", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "Sắp xếp", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.", + "dataEarthquakeSubtitle": "Báo cáo động đất", + "typhoonNoActive": "Không có bão", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "Himawari SO₂ / Cloud Phase (B11)", + "navEvents": "Sự kiện", + "onboardingTermsTitle": "Điều khoản Dịch vụ", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "Tên hương trấn", + "notifySetFailed": "Không thể lưu cài đặt. Vui lòng thử lại.", + "meshtasticDisconnect": "Disconnect", + "meshtasticUndecoded": "Not decrypted", + "notifyAnnouncement": "Thông báo", + "onboardingIntroTitle": "Chào mừng đến với DPIP", + "regionCurrentUnavailable": "Không thể lấy vị trí hiện tại", + "languageSystem": "Mặc định hệ thống", + "skyTimeSunset": "Hoàng hôn", + "mapLayerSatelliteDust": "Himawari Dust", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "Sửa", + "weatherDynamicState": "Hoạt ảnh thời tiết", + "mapPlaceholderDisabled": "Bản đồ (tạm thời vô hiệu hóa)", + "moonNow": "Bây giờ", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "Diện mạo", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "Mọc và lặn", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "Sắp tới", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "Lịch", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "Khoảng cách", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "km", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "Đường kính biểu kiến", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "Trăng mọc", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "Trăng lặn", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "Trăng non tiếp theo", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "Trên chân trời cả ngày", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "Không có hôm nay", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "Mặt Trời", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "Bình minh, hoàng hôn và tiết khí", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "Ánh sáng ban ngày", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "Hoàng hôn", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "Ánh sáng", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "Đồng hồ mặt trời", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "Tiết khí", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "Mặt Trời mọc", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "Mặt Trời lặn", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "Chính ngọ", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "Độ dài ngày", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "Dân dụng", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "Hàng hải", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "Thiên văn", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "Giờ vàng buổi sáng", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "Giờ vàng buổi chiều", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "Giờ xanh", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "Phương trình thời gian", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "phút", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "Tiết khí tiếp theo", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "Hành tinh", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "Đêm nay ở đâu, sáng bao nhiêu", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "Hiện tại", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "Trên chân trời", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "Dưới chân trời", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "Quá gần Mặt Trời", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "Cấp sao", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "Ly giác", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "Thời điểm", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "Sao Hôm", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "Sao Mai", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "Khoảng cách", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "au", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "Độ cao", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "Sao Thủy", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "Sao Kim", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "Sao Hỏa", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "Sao Mộc", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "Sao Thổ", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "Sao Thiên Vương", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "Sao Hải Vương", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "Xuân phân", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "Thanh minh", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "Cốc vũ", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "Lập hạ", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "Tiểu mãn", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "Mang chủng", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "Hạ chí", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "Tiểu thử", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "Đại thử", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "Lập thu", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "Xử thử", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "Bạch lộ", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "Thu phân", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "Hàn lộ", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "Sương giáng", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "Lập đông", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "Tiểu tuyết", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "Đại tuyết", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "Đông chí", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "Tiểu hàn", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "Đại hàn", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "Lập xuân", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "Vũ thủy", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "Kinh trập", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "Đêm nay", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "Có thể quan sát gì, và khi nào", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "Cửa sổ quan sát", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "Đêm thiên văn", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "Không bao giờ tối hẳn", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "Cửa sổ tối", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "Trăng lên suốt đêm", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "Tổng thời gian tối", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "Ánh trăng", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "Mưa sao băng", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "Tâm điểm không mọc", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "sao/giờ", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "Vệ tinh bay qua", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "Mục tiêu đang lên", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "Quadrantids", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "Lyrids", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "Eta Aquariids", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "Delta Aquariids", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "Perseids", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "Orionids", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "Nam Taurids", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "Leonids", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "Geminids", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "Ursids", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "Cụm sao mở", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "Cụm sao cầu", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "Thiên hà xoắn ốc", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "Thiên hà elip", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "Thiên hà vô định hình", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "Tinh vân hành tinh", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "Tàn dư siêu tân tinh", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "Tinh vân phát xạ", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "Tinh vân phản xạ", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "Chòm sao nhỏ", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "Lịch pháp", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "Ngày âm lịch và nhật thực, nguyệt thực sắp tới", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "Hôm nay", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "Dương lịch", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "Âm lịch", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "Can chi", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app} (mặc định)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "Độ dài tháng", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "Sao chép tọa độ", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "30 ngày", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "Đã sao chép tọa độ", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "29 ngày", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "Không thể mở {app}", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "Nhuận ", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "Thiết bị này không thể thực hiện cuộc gọi", - - "mapOverlaySectionReference": "Lớp tham chiếu", - "mapLayerCategoryEarthquake": "Động đất", - "mapLayerCategoryTyphoon": "Bão", - "mapLayerCategoryWeather": "Quan sát thời tiết", - "mapLayerCategorySatellite": "Vệ tinh", - "mapLayerCategoryRadar": "Ra đa", - "mapLayerCategoryLife": "Đời sống", - "mapLayerCategoryForecast": "Dự báo số", "mapOverlaySectionMap": "Bản đồ", - "rainIntervalSection": "Khoảng thời gian", - - "mapTownLabels": "Tên hương trấn", - "mapTownLabelsHint": "Hiển thị tên hương trấn khi phóng to", - - "mapTerrainRelief": "Độ nổi địa hình", - "mapTerrainReliefHint": "Hiển thị địa hình nổi trên bản đồ nền", - - "dpmSheetEmpty": "Chạm vào điểm đánh dấu trên bản đồ để xem chi tiết", - "dpmAddress": "Địa chỉ", - "restroomTypeLabel": "Loại", - "restroomCategoryLabel": "Hạng mục", - "restroomGradeLabel": "Hạng", - "restroomTypeFemale": "Nhà vệ sinh nữ", - "restroomTypeMale": "Nhà vệ sinh nam", - "restroomTypeMixed": "Nhà vệ sinh chung", - "restroomTypeAccessible": "Nhà vệ sinh tiếp cận được", - "restroomTypeGenderNeutral": "Nhà vệ sinh trung tính giới", - "restroomTypeFamily": "Nhà vệ sinh gia đình", - "restroomTypeUnspecified": "Không xác định", - "restroomCategoryTransport": "Giao thông", - "restroomCategoryPark": "Công viên", - "restroomCategoryCommercial": "Cơ sở thương mại", - "restroomCategoryReligious": "Nơi tôn giáo", - "restroomCategoryCultural": "Địa điểm văn hóa giải trí", - "restroomCategoryGovernment": "Cơ quan công quyền", - "restroomCategoryWelfare": "Cơ sở phúc lợi", - "restroomCategoryTourist": "Khu du lịch thắng cảnh", - "restroomCategoryLeisure": "Địa điểm vui chơi giải trí", - "restroomCategoryOther": "Khác", - "restroomGradeExcellent": "Xuất sắc", - "restroomGradeGood": "Tốt", - "restroomGradeAverage": "Trung bình", - "restroomGradePoor": "Dưới chuẩn", - "shelterAddressLabel": "Địa chỉ", - "shelterCapacityLabel": "Sức chứa", - "shelterCapacityValue": "{n} người", - "shelterCategoryLabel": "Loại thảm họa", - "shelterIndoorLabel": "Trú ẩn trong nhà", - "shelterOutdoorLabel": "Trú ẩn ngoài trời", - "shelterVulnerableOkLabel": "Phù hợp người yếu thế", - "dpmYes": "Có", - "dpmNo": "Không", - "stationSheetEmpty": "Chạm vào một trạm để xem số liệu", - "monitorDelay": "Độ trễ {value} s", - "monitorWaiting": "Đang chờ dữ liệu…", - "mapLegendUnit": "Đơn vị: {unit}", - "typhoonLegendPast": "Quỹ đạo thực tế", - "typhoonLegendForecast": "Quỹ đạo dự báo", - "typhoonLegendForecastPoint": "Điểm dự báo", - "typhoonLegendCurrent": "Tâm hiện tại", - "typhoonLegendCone": "Nón dự báo", - "mapLegendExpand": "Chú giải", - "mapLegendCollapse": "Ẩn chú giải", - "mapMyLocation": "Vị trí của tôi", - "mapResetNorth": "Về hướng bắc", - "typhoonLegendCircle15": "Vòng gió mạnh", - "typhoonLegendCircle25": "Vòng bão", - "typhoonLegendProbability": "Xác suất đổ bộ", - "typhoonLegendWarningAreas": "Vùng cảnh báo", - "typhoonWarningTitle": "Cảnh báo bão", - "typhoonWarningAreas": "Khu vực: {areas}", - "typhoonTrackDetail": "Chi tiết quỹ đạo", - "typhoonHistoryTitle": "Thời điểm dữ liệu", - "typhoonHistoryLive": "Trực tiếp", - "typhoonSatelliteTitle": "Vệ tinh", - "typhoonDataTime": "Data time\n{time}", - "typhoonForecastLead": "Forecast +{hours} h", - "typhoonIntensityIntense": "Intense typhoon", - "typhoonIntensityMild": "Mild typhoon", - "typhoonIntensityModerate": "Moderate typhoon", - "typhoonIntensityTd": "Tropical depression", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "Tropical depression TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "Past movement direction", - "typhoonLabelGaleAvg": "Avg. radius of Beaufort 7 winds", - "typhoonLabelGust": "Peak gust", - "typhoonLabelNe": "NE", - "typhoonLabelNw": "NW", - "typhoonLabelPosition": "Centre location", - "typhoonLabelPressure": "Central pressure", - "typhoonLabelProbCircle": "70% probability circle", - "typhoonLabelSe": "SE", - "typhoonLabelSpeed": "Past movement speed", - "typhoonLabelStormAvg": "Avg. radius of Beaufort 10 winds", - "typhoonLabelSw": "SW", - "typhoonLabelWind": "Max. sustained wind near centre", - "typhoonLegendCircleAvg": "Average circle", - "typhoonOverlayMenuTooltip": "Typhoon overlay options", - "typhoonOverlayProbabilityHint": "Hides the forecast cone", - "typhoonOverlayProbabilityTooltip": "Show strike probability (hides the forecast cone)", - "typhoonOverlaySectionExtra": "Overlays", - "typhoonOverlaySectionStorm": "Storm wind", - "typhoonOverlaySectionWeather": "Weather underlay", - "typhoonOverlayStormBandSubtitle": "With average circle", - "typhoonOverlayStormL10Tooltip": "Level-10 wind field + average circle (yellow)", - "typhoonOverlayStormL7Tooltip": "Level-7 wind field + average circle (purple)", - "typhoonOverlayWarningTooltip": "Highlight counties under a typhoon warning", - "typhoonOverlayWeatherHint": "Aligned to bulletin time", - "typhoonOverlayWeatherNone": "None", - "typhoonOverlayWeatherNoneTooltip": "No radar or infrared underlay", - "typhoonOverlayWeatherRadarTooltip": "Radar echo closest to the typhoon bulletin time", - "typhoonOverlayWeatherSatelliteTooltip": "Infrared closest to the typhoon bulletin time", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} hPa", - "typhoonValueKm": "{n} km", - "typhoonValueLat": "{lat}°N", - "typhoonValueLon": "{lon}°E", - "typhoonValueMs": "{n} m/s", - "typhoonOverlayForecastCallouts": "Forecast tooltips", - "typhoonOverlayForecastCalloutsTooltip": "Show forecast-point detail cards when zoomed in", - "dpmFilterSectionRestroom": "Loại địa điểm", - "dpmFilterSectionRestroomType": "Loại nhà vệ sinh", - "dpmFilterSectionShelter": "Loại thiên tai nơi trú ẩn", - "dpmDisasterFlood": "Lũ lụt", - "dpmDisasterEarthquake": "Động đất", - "dpmDisasterLandslide": "Sạt lở đất", - "dpmDisasterTsunami": "Sóng thần", - "dpmDisasterSlope": "Thiên tai sườn dốc", - "dpmDisasterNuclear": "Sự cố hạt nhân", - "skyTime": "Thời gian bầu trời", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "Nguyệt thực", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "Tự động", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "Nhật thực", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "Rạng đông", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "Không có trong phạm vi", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "Bình minh", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "Toàn phần", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "Buổi sáng", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "Một phần", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "Buổi trưa", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "Hình khuyên", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "Buổi chiều", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "Nửa tối", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "Giờ vàng", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "Tý", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "Hoàng hôn", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "Sửu", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "Chạng vạng", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "Dần", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "Ban đêm", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "Mão", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "Nhiều mây", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "Thìn", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "Trời âm u", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "Tỵ", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "Tuyết rơi", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "Ngọ", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "Bụi cát", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "Mùi", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "Hiện phạm vi quét", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "Thân", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "Đánh dấu vùng bốn radar thực sự quan trắc.", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "Dậu", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "Ngoài khung là chưa quan trắc", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "Tuất", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "Tùy chọn lớp radar", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "Hợi", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "Ranh giới huyện thị", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "Thủy triều", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "Biên giới quốc gia", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "Triều cường, triều kém và lực hút của Mặt Trăng", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "Khung ngoài của mỗi quốc gia", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "Chỉ là lực triều thiên văn, không phải bảng thủy triều cảng. Mực nước xin xem bảng do CWA công bố.", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "Vẽ đè lên tiếng vọng", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "Hiện tại", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "Giữ ranh giới rõ ràng dưới lớp phản hồi radar.", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "Chu kỳ", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "Ranh giới xã phường", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "Triều cường", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "Lưới chi tiết hơn", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "Triều kém", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "Giữ ranh giới xã phường rõ ràng dưới lớp phản hồi radar.", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "Trung bình", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "Tùy chọn lớp dự báo mưa định lượng", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "Lực hút Mặt Trăng", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "Tùy chọn lớp dự báo gió", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "Triều cân bằng", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "Vẽ trên trường gió", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "m", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "Khung ngoài của mỗi quốc gia", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "Triều cường cận điểm tới", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "Lưới mịn hơn", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "Điểm ngoặt", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "Bản tin {serial}", - "eewMaxIntensity": "Cường độ tối đa", - "eewLocalIntensity": "Ước tính tại vị trí", - "eewSWave": "Sóng S", - "eewArrived": "Đã đến", - "eewCountdown": "{seconds} giây" + "tideHigh": "Cao", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "Thấp", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "Bản đồ sao", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "Bầu trời nhìn bằng mắt thường", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "B", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "Đ", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "N", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "T", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "dữ liệu quỹ đạo {days} ngày trước", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}tháng {month} ngày {day}", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "Không có mưa sao băng", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "Không có lượt bay qua nhìn thấy trong 48 giờ", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "Không đọc được dữ liệu quỹ đạo", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "Không có mục tiêu đủ cao", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "Không đọc được danh mục sao", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8964b1e65..8b42ae25f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,679 +1,1739 @@ { - "@@locale": "zh", - "languageName": "繁體中文(臺灣)", - "navHome": "首頁", - "navEvents": "事件", - "navMap": "地圖", - "navData": "資料", - "navEarthquake": "地震", - "dataSectionSeismic": "地震", - "dataEarthquakeSubtitle": "地震報告", - "dataSectionWeather": "氣象", - "dataWeatherRankingSubtitle": "即時觀測排行", - "weatherRankingTitle": "觀測排行", - "weatherRankingMeta": "資料時間:{time}\n共 {count} 觀測點", - "weatherRankingEmpty": "目前沒有可排序的觀測", - "weatherRankingBy": "依", - "weatherRankingHighest": "最高", - "weatherRankingLowest": "最低", - "weatherRankingMergeTo": "合併至", - "weatherRankingMergeTown": "鄉鎮", - "weatherRankingMergeCounty": "縣市", - "weatherRankingWind": "風速", - "weatherRankingGust": "陣風", + "typhoonValueLat": "北緯 {lat} 度", + "onboardingSkipBody": "未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 時", + "homeRainTrendHeavyStopping": "預計 {minutes} 分鐘後停止下大雨", + "mapTimelineObserved": "觀測", + "regionSelectTitle": "選擇地區", + "skyTimeNoon": "正午", + "radarCountyOutlineSubtitle": "讓縣市界線在雷達回波下仍然清楚。", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "廁所類型", + "mapLayerSatelliteB03": "ひまわり 可見光-紅(B03)", + "reportFilterIntensity": "震度", + "mapLayerLightning": "閃電", + "restroomTypeMale": "男廁所", + "meshtasticLastReceived": "最近接收", + "reportDetailSortByCounty": "依縣市排序", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "可能會有零星降雨", + "meshtasticUptime": "運行時間", "weatherRankingTempExtremes": "溫度極值", - "weatherRankingExtremeHigh": "今日最高", - "weatherRankingExtremeLow": "今日最低", + "themeLight": "淺色", + "mapTerrainReliefHint": "在底圖上顯示立體地形陰影", + "meshtasticEmptyMessage": "(空白訊息)", + "moreSectionRegion": "地區", + "dpmDisasterEarthquake": "震災", + "mapLayerSatellite": "ひまわり 紅外線(B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "週六開放時間", + "dpmDisasterSlope": "坡地災害", + "moonPhaseNew": "新月", + "notifySectionEew": "地震速報", + "mapResetNorth": "回到北方", + "rainInterval2d": "2 日", + "mapTownLabelsHint": "放大時顯示鄉鎮名稱", + "commonCancel": "取消", + "notifyOptTsunamiWarning": "只接收海嘯警報", + "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "進階", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "日溫差", + "notifySettingsMenu": "通知設定", + "typhoonHistoryTitle": "資料時間", + "mapAppDefault": "{app}(預設)", + "trendRange24h": "24 小時", + "mapLayerStyleJmaTooltip": "灰階為底,−40 °C 以下上色,凸顯雲頂高度", "weatherRankingRecordedAt": "記錄於 {time}", - "weatherRankingAnalysisCurrent": "當下 {value}°C", - "weatherRankingAnalysisHigh": "最高 {value}", - "weatherRankingAnalysisLow": "最低 {value}", - "weatherRankingAnalysisRange": "溫差 {value}°C", - "reportListEmpty": "目前沒有地震報告", - "reportListEmptyFiltered": "沒有符合條件的地震報告", - "reportListMeta": "M{magnitude} · {depth} 公里", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "公里", - "reportListLocalFelt": "小區域有感", - "reportListToday": "今天", - "reportListYesterday": "昨天", - "reportListDayCount": "{count}", - "reportListEnd": "已到最後一頁", - "reportFilterTitle": "篩選", - "reportFilterSort": "排序方式", - "reportFilterSortTime": "時間", - "reportFilterSortIntensity": "震度", - "reportFilterSortMagnitude": "規模", - "reportFilterSortDepth": "深度", + "mapLayerRain": "雨量", + "mapLayerQpesums": "未來 1 小時降水預報", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "地圖", + "mapTerrainRelief": "地形立體感", + "eewMaxIntensity": "最大震度", + "mapLegendCollapse": "收合圖例", + "changelogTitle": "更新日誌", "reportFilterOrderDesc": "降序", - "reportFilterOrderAsc": "升序", - "reportFilterIntensity": "震度", + "meshtasticExcludeMqttSubtitle": "經網際網路橋接、並非無線電聽到的節點", "reportFilterIntensityInfoTitle": "震度新制與舊制", - "reportFilterIntensityInfoIntro": "中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。", - "reportFilterIntensityInfoLegacyTitle": "舊制(2020 以前)", - "reportFilterIntensityInfoLegacyBody": "震度僅 0–7,沒有 5弱/5強/6弱/6強。", - "reportFilterIntensityInfoModernTitle": "新制(2020 起)", - "reportFilterIntensityInfoModernBody": "震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。", - "reportFilterMagnitude": "規模", - "reportFilterDepth": "深度", - "reportFilterDepthKm": "{depth} 公里", - "reportFilterDate": "日期", - "reportFilterDatePick": "選擇日期", - "reportFilterDateStartNote": "開始日:當日 00:00(臺北時間)", + "mapLayerTyphoon": "颱風", + "radarOverlayMenuTooltip": "雷達圖層選項", + "mapMyLocation": "我的位置", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "節點", + "meshtasticSend": "傳送", + "typhoonOverlayStormL7Tooltip": "七級暴風圈+平均圓(紫色)", + "aedType": "場所類型", + "termsOfService": "服務條款", + "typhoonLegendCircle25": "十級風暴風圈", + "sponsorTitle": "支持 DPIP", + "mapNavSatellite": "衛星", + "homeRainTrendUpdated": "更新 {time}", + "onboardingNext": "下一步", + "weatherRankingMergeTown": "鄉鎮", + "mapLayerMonitor": "強震監視器", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "訂閱制", + "typhoonValueLon": "東經 {lon} 度", + "skyTime": "天空時間", + "weatherModeCloudy": "多雲", + "skyTimeDusk": "暮色", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "韌體", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "結束日:當日 24:00(臺北時間)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "地點", - "reportFilterLocationHint": "例如:花蓮、東部海域", - "reportFilterAny": "不限", - "reportFilterApply": "套用", - "reportFilterReset": "重設", - "reportListSearch": "查詢", - "reportDetailTitle": "地震報告", - "reportDetailNumbered": "編號 {number} 顯著有感地震", - "reportDetailLocalFelt": "小區域有感地震", - "reportDetailInfo": "詳細資訊", - "reportDetailOriginTime": "發震時間", - "reportDetailEpicenter": "震央座標", - "reportDetailMagnitude": "地震規模", - "reportDetailDepth": "震源深度", - "reportDetailAreaIntensity": "各地震度", - "reportDetailLocalIntensity": "所在地的震度", - "reportDetailLocalIntensityUnavailable": "沒有震度訊息", - "reportDetailSortByIntensity": "依震度排序", - "reportDetailSortByCounty": "依縣市排序", - "reportDetailImage": "地震報告圖", - "reportDetailImageUnavailable": "報告圖尚未提供", - "reportDetailOpenReport": "報告頁面", - "reportDetailReplay": "重播", - "navMore": "更多", - "appLogs": "App 日誌", - "changelogTitle": "更新日誌", - "changelogEmpty": "目前沒有更新日誌", - "changelogTypePrerelease": "公測", - "changelogTypeStable": "正式", - "changelogCurrentVersion": "目前版本", - "changelogVersionDetails": "版本資訊", - "changelogBodyEmpty": "此版本沒有說明。", - "mapPlaceholderDisabled": "地圖(暫時停用)", - "moreSectionRegion": "地區", - "moreSectionNotify": "通知", - "moreSectionDisplay": "顯示", - "regionManageTitle": "常用地區", - "regionAddButton": "新增地區", - "regionEmpty": "尚未新增常用地區", - "regionSelectTitle": "選擇地區", - "regionSelectCount": "已選 {count}/{max}", - "regionSelectFull": "最多只能選擇 {max} 個地區", - "regionEdit": "修改", - "moreSectionAdvanced": "進階", - "moreDeveloper": "除錯資訊", - "experimentalFeatures": "實驗性功能", - "moreSectionLinks": "相關連結", - "moreCwaEew": "中央氣象署強震即時警報", - "moreTremReport": "TREM 檢知報告", - "moreServerStatus": "伺服器狀態", - "moreAnnouncements": "公告", - "moreDiscord": "Discord 社群", - "moreNotifyLog": "DPIP 通知發送記錄", - "moreLinkOpenFailed": "無法開啟連結", - "weatherDynamicState": "天氣動態狀態", - "weatherDynamicStateSubtitle": "覆寫主頁背景天氣", - "weatherModeAuto": "自動", - "weatherModeClear": "晴天", - "weatherModeRain": "雨天", - "weatherModeFog": "大霧", - "weatherModeThunderstorm": "雷雨", - "commonLoading": "載入中…", - "commonRetry": "重試", - "commonError": "發生錯誤", - "commonFetchFailed": "無法獲取資料,請稍後重試", - "commonEmpty": "沒有資料", - "feedConnecting": "連線中…", - "feedStale": "資料可能已過期", - "feedOffline": "連線中斷", - "eewTitle": "地震速報", - "eewNone": "目前沒有地震速報", - "eewSummary": "規模 {magnitude}・深度 {depth} 公里", - "regionNationwide": "全國", - "regionCurrent": "所在地", - "regionCurrentUnavailable": "無法取得所在地位置資訊", - "weatherPrecipitation": "降水量", - "weatherHumidity": "濕度", - "weatherDataTime": "{station} ∙ 資料時間 {time}", - "homeViewOnMap": "前往地圖察看", - "homeForecastTitle": "24小時預報", + "meshtasticSilent": "已靜默", + "reportFilterSortMagnitude": "規模", + "mapLayerCategoryEarthquake": "地震", + "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", + "typhoonLegendPast": "實際路徑", + "restroomCategoryOther": "其他", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "高 {high}° · 低 {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "體感 {temp}°", - "homeForecastHumidity": "濕度 {value}%", - "homeForecastWind": "{direction} · {level} 級", - "homeForecastUnavailable": "選擇鄉鎮後可查看預報", - "homeForecastEmpty": "目前沒有預報資料", - "homeActiveEventsTitle": "生效中事件", - "homeActiveEventsEmpty": "目前沒有生效中的事件", - "homeRainTrendTitle": "近 1 小時降水趨勢", - "homeRainTrendMinute": "{minute}分", - "homeRainTrendUpdated": "更新 {time}", - "homeRainTrendNoData": "無資料", - - "homeRainTrendScattered": "可能會有零星降雨", - "homeRainTrendLightSustained": "未來 1 小時會有持續小雨", - "homeRainTrendLightStopping": "預計 {minutes} 分鐘後停止下小雨", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "開啟設定", + "mapLegendExpand": "圖例", + "eewNone": "目前沒有地震速報", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "海嘯消息、海嘯警報", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "節點選項", + "onboardingAgreeContinue": "同意並繼續", + "meshtasticNodeId": "節點 ID", + "commonRetry": "重試", + "reportDetailNumbered": "編號 {number} 顯著有感地震", + "typhoonOverlayStormBandSubtitle": "含平均圓", + "disasterMapOverlayRestroomTooltip": "顯示公廁", + "weatherRankingTitle": "觀測排行", "homeRainTrendHeavySustained": "未來 1 小時會有持續大雨", - "homeRainTrendHeavyStopping": "預計 {minutes} 分鐘後停止下大雨", - "mapLayers": "圖層", - "mapLayerOrderTitle": "調整圖層順序", - "mapLayerOrderReset": "回復預設順序", - "mapLayerRadar": "雷達合成回波圖", - "mapLayerSatellite": "ひまわり 紅外線(B13)", - "mapLayerSatelliteB01": "ひまわり 可見光-藍(B01)", - "mapLayerSatelliteB02": "ひまわり 可見光-綠(B02)", - "mapLayerSatelliteB03": "ひまわり 可見光-紅(B03)", - "mapLayerSatelliteB04": "ひまわり 近紅外(B04)", + "notifySectionTsunami": "海嘯", + "restroomCategoryPark": "公園", + "moreLinkOpenFailed": "無法開啟連結", + "themeDark": "深色", + "sponsorRestore": "恢復購買", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "正在設定 DPIP 頻道…", + "meshtasticRegionSwitch": "切換為 TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "流量", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階", + "disasterMapOverlayAedTooltip": "顯示 AED 位置", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "濕度", + "mapLayerSatelliteTransparentNight": "夜間 = 透明,顯示底圖", + "meshtasticScanning": "掃描中…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "最多只能選擇 {max} 個地區", + "meshtasticTitle": "Meshtastic", + "navMore": "更多", + "meshtasticDpipChannel": "DPIP 頻道", + "disasterMapOverlaySectionLayers": "圖層", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "ひまわり 近紅外(B05)", - "mapLayerSatelliteB06": "ひまわり 近紅外(B06)", - "mapLayerSatelliteB07": "ひまわり 短波紅外(B07)", - "mapLayerSatelliteB08": "ひまわり 上層水氣(B08)", - "mapLayerSatelliteB09": "ひまわり 中層水氣(B09)", - "mapLayerSatelliteB10": "ひまわり 低層水氣(B10)", - "mapLayerSatelliteB11": "ひまわり 二氧化硫/雲相(B11)", - "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", - "mapLayerSatelliteB13": "ひまわり 紅外線(B13)", - "mapLayerSatelliteB14": "ひまわり 長波紅外線(B14)", - "mapLayerSatelliteB15": "ひまわり 長波紅外線(B15)", - "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "typhoonStormRadii": "東北 {ne} · 東南 {se} · 西南 {sw} · 西北 {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "東北側", + "meshtasticCopied": "已複製訊息", + "reportListEmpty": "目前沒有地震報告", + "reportListEnd": "已到最後一頁", "mapLayerSatelliteTruecolor": "ひまわり 真彩色", - "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", - "mapLayerSatelliteAsh": "ひまわり 火山灰", - "mapLayerSatelliteDust": "ひまわり 沙塵", - "mapLayerSatelliteAirmass": "ひまわり 氣團", - "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", - "mapLayerSatelliteWatervapor": "ひまわり 水氣", - "mapLayerSatelliteBtdSplit": "ひまわり 分割視窗", - "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", - "mapLayerSatelliteBtdWvirw": "ひまわり 過衝雲頂", - "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/雲相", - "mapLayerSatelliteBtdCo2": "ひまわり 卷雲/雲高", - "mapLayerSatelliteBtdOzone": "ひまわり 對流層頂", - "mapLayerSatelliteCloudtop": "ひまわり 雲頂溫度", - "mapLayerSatelliteCloudmask": "ひまわり 雲遮罩", - "mapLayerSatelliteSst": "ひまわり 海表溫度", - "mapLayerSatelliteNdvi": "ひまわり 植生指數", - "mapLayerSatelliteNdwi": "ひまわり 水體指數", - "mapLayerSatelliteMndwi": "ひまわり 改良水體指數", + "typhoonOverlaySectionExtra": "覆蓋層", + "eewSWave": "震波", + "meshtasticBusyTitle": "另一個 App 正在使用這台裝置", + "restroomCategoryCultural": "文化育樂活動場所", + "typhoonLabelWind": "近中心最大風速", + "radarGlobalOutlineHint": "各國國界外框", + "notifyEvacuation": "防災資訊", + "typhoonLegendCircle15": "七級風暴風圈", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "天文", + "homeRainTrendLightSustained": "未來 1 小時會有持續小雨", + "commonError": "發生錯誤", + "moonPhaseWaningCrescent": "殘月", + "meshtasticPower": "電力", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "現在", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "報告頁面", + "trendRange7d": "7 天", + "typhoonWarningAreas": "警戒區域:{areas}", + "rainIntervalSection": "統計時間", + "notifyTitle": "通知", + "meshtasticTxPower": "發射功率", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "類別", + "sponsorRestoring": "正在恢復購買…", + "sponsorIntro": "DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。", + "shelterAddressLabel": "地址", + "typhoonLabelStormAvg": "十級風平均暴風半徑", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "商業營業場所", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "縣市區域", + "homeRainTrendLightStopping": "預計 {minutes} 分鐘後停止下小雨", + "reportDetailInfo": "詳細資訊", + "mapNavWind": "風向", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "風場預報圖層選項", + "dataWeatherRankingSubtitle": "即時觀測排行", + "rainInterval6h": "6 時", + "homeRainTrendMinute": "{minute}分", + "restroomTypeUnspecified": "未設定", + "typhoonOverlayProbabilityHint": "會隱藏預測圓錐", "mapLayerSatelliteGlobalOutline": "國界", - "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", - "mapLayerSatelliteCloudClear": "晴空", - "mapLayerSatelliteCloudProbablyClear": "可能晴空", - "mapLayerSatelliteCloudProbablyCloudy": "可能有雲", + "mapNavTemperature": "溫度", + "typhoonLegendForecastPoint": "預測點", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "昨天", + "moreSectionLinks": "相關連結", + "feedOffline": "連線中斷", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "顯示", + "rainInterval3d": "3 日", + "defaultMapLayerSubtitle": "開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。", + "aedDescription": "備註", + "typhoonOverlayWeatherRadarTooltip": "雷達回波(對齊颱風報文時間)", + "onboardingPermLocationDesc": "依你所在位置推送在地警報。", + "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "目前沒有生效中的事件", + "typhoonLabelPosition": "中心位置", + "weatherRankingBy": "依", + "typhoonIntensityMild": "輕度颱風", + "windForecastGlobalOutlineHint": "各國國界外框", + "rainInterval1h": "1 時", + "eewLocalIntensity": "所在地預估", + "mapLayerRadar": "雷達合成回波圖", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "宗教禮儀場所", + "meshtasticRole": "角色", "mapLayerSatelliteCloudCloudy": "有雲", - "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,顯示底圖", - "mapLayerSatelliteTransparentReflectance": "低反射率/夜間 = 透明,顯示底圖", - "mapLayerSatelliteTransparentZero": "零差值 = 透明(無訊號)", - "mapLayerSatelliteTransparentNight": "夜間 = 透明,顯示底圖", - "mapLayerSatelliteTransparentNoData": "無資料(陸地) = 透明", - "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(無植被)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(無水體)", - "mapLayerSatelliteTransparentClear": "晴空 = 透明,顯示底圖", + "skyTimeSunrise": "日出", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "尚無訊息", + "onboardingPermNotifyDesc": "在地震、天氣與災害發生時,即時傳遞警報通知。", + "radarTownOutline": "鄉鎮界線", "mapLayerStyleSection": "顯示樣式", - "mapLayerStyleTooltip": "顯示樣式", - "mapLayerStyleGray": "灰階(JMA)", - "mapLayerStyleGrayTooltip": "氣象廳灰階慣例:溫度越低越白", - "mapLayerStyleJma": "雲頂強調(JMA)", - "mapLayerStyleJmaTooltip": "灰階為底,−40 °C 以下上色,凸顯雲頂高度", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階", - "mapLayerQpesums": "未來 1 小時降水預報", - "mapLayerLightning": "閃電", - "lightningLegendCg": "對地 · {minutes} 分內", - "lightningLegendCc": "雲間 · {minutes} 分內", - "mapTimelineNow": "現在", - "mapTimelinePast": "歷史", - "mapTimelineFuture": "未來", - "mapTimelineObserved": "觀測", - "mapTimelineForecast": "預報", - "mapTimelineDataTime": "資料時間 {time}", - "notifySettingsMenu": "通知設定", - "notifyTitle": "通知", - "notifyUnavailable": "推播尚未就緒,請稍後再試。", - "notifySetFailed": "設定失敗,請稍後再試。", - "notifySectionEew": "地震速報", - "notifySectionEarthquake": "地震", - "notifySectionWeather": "天氣", - "notifySectionTsunami": "海嘯", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "防災地圖圖層", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "近期聽到", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "西南側", + "typhoonForecastLead": "預測 +{hours} 小時", + "dpmDisasterTsunami": "海嘯", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "正式", + "mapLayerSatelliteTransparentClear": "晴空 = 透明,顯示底圖", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "參考圖層", + "mapLayerSatelliteB02": "ひまわり 可見光-綠(B02)", + "reportListLocalFelt": "小區域有感", + "weatherRankingEmpty": "目前沒有可排序的觀測", "notifySectionOther": "其他", - "notifyEew": "緊急地震速報", - "notifyMonitor": "強震監視器", - "notifyReport": "地震報告", - "notifyIntensity": "震度速報", - "notifyThunderstorm": "雷雨即時訊息", - "notifyAdvisory": "天氣警特報", - "notifyEvacuation": "防災資訊", - "notifyTsunami": "海嘯資訊", - "notifyAnnouncement": "公告", - "notifyOptOff": "關閉", - "notifyOptAll": "接收全部", + "weatherRankingMeta": "資料時間:{time}\n共 {count} 觀測點", + "onboardingTermsAgree": "我已閱讀並同意服務條款", + "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(無植被)", "notifyOptLocalIntensity4": "所在地震度4以上", - "notifyOptLocalIntensity1": "所在地震度1以上", - "notifyOptWeatherLocal": "接收所在地", - "notifyOptTsunamiWarning": "只接收海嘯警報", - "notifyOptTsunamiAll": "海嘯消息、海嘯警報", - "onboardingNext": "下一步", - "onboardingBack": "上一步", + "eewArrived": "已抵達", + "meshtasticNoDevices": "找不到 Meshtastic 裝置", + "mapLayerCategoryLife": "生活", + "reportFilterSortIntensity": "震度", + "typhoonMotion": "移動", + "meshtasticStateDisconnected": "未連線", + "typhoonIntensityIntense": "強烈颱風", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "調整圖層順序", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "是", + "meshtasticNoHistory": "歷史紀錄還不夠", + "reportDetailLocalIntensityUnavailable": "沒有震度訊息", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "公里", + "reportFilterDepth": "深度", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "往下捲動以繼續", - "onboardingIntroTitle": "歡迎使用 DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "預報", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "地圖", + "notifyAdvisory": "天氣警特報", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "重設", + "mapLayerSatelliteMndwi": "ひまわり 改良水體指數", + "typhoonOverlaySectionStorm": "暴風圈", + "moonPhaseFull": "滿月", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "虧凸月", + "weatherDynamicStateSubtitle": "覆寫主頁背景天氣", + "reportFilterIntensityInfoModernTitle": "新制(2020 起)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "資料時間\n{time}", + "restroomTypeAccessible": "無障礙廁所", + "moreSectionAbout": "關於", + "meshtasticSelectDevice": "選擇裝置", "onboardingIntroBody": "DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷雨即時訊息、天氣警特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。", - "onboardingTermsTitle": "服務條款", - "onboardingTermsBody": "使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。", - "onboardingTermsAgree": "我已閱讀並同意服務條款", - "onboardingAgreeContinue": "同意並繼續", - "onboardingPermsTitle": "權限授權", - "onboardingPermsBody": "為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。", + "shelterCapacityLabel": "收容人數", + "reportDetailImage": "地震報告圖", + "meshtasticStateConfiguring": "設定中…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "七級風平均暴風半徑", "onboardingPermNotify": "通知", - "onboardingPermNotifyDesc": "在地震、天氣與災害發生時,即時傳遞警報通知。", - "onboardingPermCritical": "重大通知", - "onboardingPermCriticalDesc": "讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。", - "onboardingPermLocation": "定位", - "onboardingPermLocationDesc": "依你所在位置推送在地警報。", - "onboardingPermBackground": "背景定位", - "onboardingPermBackgroundDesc": "選擇「一律允許」,關閉 App 也能推送在地警報。", - "onboardingPermBattery": "省電白名單", - "onboardingPermBatteryDesc": "允許 DPIP 在背景持續運作,避免警報延遲或漏收。", - "onboardingGrant": "授權", - "onboardingGranted": "已授權", - "onboardingStart": "開始使用", - "language": "語言", - "languageSettings": "語言設定", - "languageSystem": "系統預設", - "locationBannerServiceOff": "定位服務已關閉,無法針對你的所在地推送警報。", - "locationBannerPermission": "尚未授權定位,無法針對你的所在地推送警報。", - "locationBannerFix": "開啟設定", - "notifyBannerDisabled": "通知已關閉,將收不到災害警報。", - "onboardingSkipTitle": "尚未完成授權", - "onboardingSkipBody": "未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。", - "onboardingSkipStay": "返回授權", - "onboardingSkipLeave": "仍要略過", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "清除訊息", + "meshtasticNotifyMessages": "新訊息通知", + "defaultMapLayerSettings": "地圖預設圖層", + "moreSectionNotify": "通知", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "推播尚未就緒,請稍後再試。", + "mapLayerOrderReset": "回復預設順序", + "dpmAddress": "地址", + "weatherRankingMergeCounty": "縣市", + "moreSectionApp": "取得 App", + "reportFilterIntensityInfoLegacyBody": "震度僅 0–7,沒有 5弱/5強/6弱/6強。", + "mapLayerSatelliteSst": "ひまわり 海表溫度", + "qpesumsOverlayMenuTooltip": "定量降水預報圖層選項", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "未來", + "typhoonLegendCircleAvg": "平均圓", + "reportFilterDepthKm": "{depth} 公里", + "typhoonLabelSe": "東南側", + "radarTownOutlineHint": "較細的分區", + "eewCountdown": "{seconds} 秒", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "瞬間最大陣風", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "使用條款", + "restroomTypeGenderNeutral": "性別友善廁所", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "雷雨即時訊息", + "skyTimeGolden": "黃金時刻", + "moonAge": "月齡", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "當下 {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "選擇鄉鎮後可查看預報", + "mapLayers": "圖層", + "meshtasticHardware": "硬體", + "languageSettings": "語言設定", + "dpmDisasterNuclear": "核子事故", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "語言", + "homeForecastFeelsLike": "體感 {temp}°", + "typhoonOverlayWeatherHint": "對齊報文時間", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "黎明", + "skyTimeAfternoon": "下午", + "meshtasticLastHeard": "最後聽到", + "typhoonWarningTitle": "颱風警報", "moreSourceCode": "原始碼", - "moreSectionApp": "取得 App", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "顯示設定", - "defaultMapLayerSettings": "地圖預設圖層", - "defaultMapLayerSubtitle": "開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。", - "mapNavRadar": "雷達", - "mapNavQpesums": "預報", - "mapNavSatellite": "衛星", - "mapNavLightning": "閃電", - "mapNavTyphoon": "颱風", + "mapLayerCategoryWeather": "氣象觀測", + "mapLayerSatelliteB09": "ひまわり 中層水氣(B09)", + "windForecastTownOutlineHint": "更細的網格", + "mapLayerSatelliteCloudmask": "ひまわり 雲遮罩", + "mapAppCopyCoordinates": "複製座標", + "reportFilterIntensityInfoIntro": "中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。", "mapNavEarthquake": "地震", - "mapNavTemperature": "溫度", - "mapNavHumidity": "濕度", - "mapNavPressure": "氣壓", - "mapNavWind": "風向", + "typhoonGust": "陣風", + "restroomGradeAverage": "普通級", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "ひまわり 卷雲/雲高", + "onboardingPermBackgroundDesc": "選擇「一律允許」,關閉 App 也能推送在地警報。", + "mapTimelineForecast": "預報", + "restroomTypeLabel": "廁所類型", + "navEarthquake": "地震", + "typhoonOverlayStormL10Tooltip": "十級暴風圈+平均圓(黃色)", + "moonPhaseWaxingGibbous": "盈凸月", + "reportDetailTitle": "地震報告", + "moreTremReport": "TREM 檢知報告", + "weatherDataTime": "{station} ∙ 資料時間 {time}", + "meshtasticNoNodes": "尚未聽到任何節點", + "meshtasticViaMqtt": "經 MQTT(網際網路)", + "radarCountyOutline": "縣市界線", + "onboardingGranted": "已授權", + "@mapAppCopyCoordinates": {}, + "commonClose": "關閉", + "restroomGradeLabel": "等級", + "rainIntervalNow": "今日", + "changelogCurrentVersion": "目前版本", + "typhoonOverlayForecastCalloutsTooltip": "放大時顯示預測點詳細卡片", + "typhoonLabelPressure": "中心氣壓", + "aedOpenRemark": "開放時間備註", + "onboardingPermsBody": "為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。", + "typhoonOverlaySectionWeather": "天氣底圖", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "接收所在地", "mapNavRain": "雨量", - "mapNavDisaster": "防災", - "displayTheme": "主題", + "moonDays": "天", + "mapLegendUnit": "單位:{unit}", + "weatherModeClear": "晴天", + "meshtasticRadio": "電台", + "commonEmpty": "沒有資料", + "mapLayerSatelliteB01": "ひまわり 可見光-藍(B01)", + "meshtasticExternalPower": "外部供電", + "moonPhaseLastQuarter": "下弦月", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "升序", + "reportFilterApply": "套用", + "reportDetailImageUnavailable": "報告圖尚未提供", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "最高", + "reportDetailReplay": "重播", + "mapLayerRestroom": "公廁", + "restroomCategoryWelfare": "社福機構、集會場所", + "restroomGradeExcellent": "特優級", + "meshtasticLastSent": "最近送出", + "meshtasticName": "名稱", + "meshtasticScan": "掃描", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "數值預報", + "meshtasticChannelFailed": "無法設定 DPIP 頻道", "themeSystem": "跟隨系統", - "themeLight": "淺色", - "themeDark": "深色", - "moreSectionAbout": "關於", - "termsOfService": "服務條款", - "faq": "常見問題", - "openSourceLicenses": "引用套件", - "sponsorTitle": "支持 DPIP", - "sponsorIntro": "DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。", - "sponsorSubscriptions": "訂閱制", - "sponsorRecommended": "推薦", - "sponsorOneTime": "單次支援", - "sponsorPerMonth": "{price} / 月", - "sponsorRestore": "恢復購買", - "sponsorTerms": "使用條款", - "sponsorPrivacy": "隱私權政策", - "sponsorRestoring": "正在恢復購買…", - "sponsorRestoreUnavailable": "無法連線至商店,請稍後再試", - "commonClose": "關閉", + "mapLayerSatelliteNdvi": "ひまわり 植生指數", + "typhoonLegendForecast": "預測路徑", + "typhoonValueHpa": "{n} 百帕", + "weatherPrecipitation": "降水量", + "moonNextFullMoon": "下次滿月", + "dpmSheetEmpty": "點選地圖上的標記查看詳情", + "onboardingSkipLeave": "仍要略過", + "onboardingBack": "上一步", + "aedPlaceDesc": "放置位置說明", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "尚未完成授權", + "restroomTypeFamily": "親子廁所", + "typhoonValueKm": "{n} 公里", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "氣壓", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "省電白名單", + "typhoonLabelNw": "西北側", + "dpmDisasterFlood": "水災", + "moonPhaseWaxingCrescent": "眉月", + "restroomCategoryLeisure": "休閒娛樂場所", "mapLayerTemperature": "溫度", - "trendRange24h": "24 小時", - "trendRange7d": "7 天", - "trendNoData": "沒有趨勢資料", - "trendCumulativeTotal": "累計 {total} mm", - "chartHourLabel": "{hour}時", - "mapLayerHumidity": "濕度", - "mapLayerPressure": "氣壓", + "aedCategory": "場所分類", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "頻道", + "monitorWaiting": "等待資料…", + "typhoonOverlayForecastCallouts": "預測點資訊", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "震央座標", + "meshtasticVoltage": "電壓", + "mapLayerMeshtasticSubtitle": "電台聽到過的 LoRa 網狀網路節點", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "風向", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "雨量", - "rainIntervalMenu": "累積時段", - "rainIntervalNow": "今日", - "rainInterval10m": "10 分", - "rainInterval1h": "1 時", - "rainInterval3h": "3 時", - "rainInterval6h": "6 時", + "reportDetailMagnitude": "地震規模", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "各地震度", "rainInterval12h": "12 時", - "rainInterval24h": "24 時", - "rainInterval2d": "2 日", - "rainInterval3d": "3 日", - "mapLayerTyphoon": "颱風", - "typhoonNoActive": "目前無颱風", - "typhoonWind": "風速", - "typhoonGust": "陣風", - "typhoonPressure": "氣壓", - "typhoonMotion": "移動", - "typhoonLabelPosition": "中心位置", - "typhoonLabelDirection": "過去移動方向", + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "土石流", + "notifyMonitor": "強震監視器", + "onboardingStart": "開始使用", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / 月", + "mapLayerPressure": "氣壓", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "ひまわり 近紅外(B04)", + "mapLayerSatelliteTransparentZero": "零差值 = 透明(無訊號)", + "shelterIndoorLabel": "室內收容", + "notifyOptOff": "關閉", + "reportFilterSortTime": "時間", + "mapLayerSatelliteCloudProbablyClear": "可能晴空", + "weatherModeThunderstorm": "雷雨", + "homeViewOnMap": "前往地圖察看", + "reportFilterIntensityInfoLegacyTitle": "舊制(2020 以前)", "typhoonLabelSpeed": "過去移動時速", - "typhoonLabelPressure": "中心氣壓", - "typhoonLabelWind": "近中心最大風速", - "typhoonLabelGust": "瞬間最大陣風", - "typhoonLabelGaleAvg": "七級風平均暴風半徑", - "typhoonLabelStormAvg": "十級風平均暴風半徑", - "typhoonLabelProbCircle": "70%機率圓", - "typhoonForecastLead": "預測 +{hours} 小時", - "typhoonLabelNw": "西北側", - "typhoonLabelNe": "東北側", - "typhoonLabelSw": "西南側", - "typhoonLabelSe": "東南側", - "typhoonValueLat": "北緯 {lat} 度", - "typhoonValueLon": "東經 {lon} 度", - "typhoonValueKm": "{n} 公里", - "typhoonValueHpa": "{n} 百帕", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "無法開啟 {app}", + "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "已接收", + "weatherRankingExtremeLow": "今日最低", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "ひまわり 低層水氣(B10)", + "mapLayerSatelliteCloudProbablyCloudy": "可能有雲", + "shelterCategoryLabel": "適用災害", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(無水體)", + "meshtasticStateConnecting": "連線中…", + "moonTitle": "月亮", + "weatherRankingGust": "陣風", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "避難所災害類型", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "伺服器狀態", + "notifySectionWeather": "天氣", + "meshtasticPreset": "調變預設", + "dataSectionSeismic": "地震", + "changelogBodyEmpty": "此版本沒有說明。", + "radarGlobalOutline": "國界", + "notifyEew": "緊急地震速報", + "regionNationwide": "全國", + "moreNotifyLog": "DPIP 通知發送記錄", + "regionCurrent": "所在地", + "dpmFilterSectionRestroom": "場所類型", + "meshtasticNotConnected": "尚未連線至裝置", + "weatherModeSnow": "下雪", + "mapLayerMeshtastic": "Meshtastic 節點", + "moreDeveloper": "除錯資訊", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "ひまわり 長波紅外線(B14)", + "meshtasticChannelUse": "頻道使用率", + "mapNavLightning": "閃電", + "homeForecastEmpty": "目前沒有預報資料", + "sponsorOneTime": "單次支援", + "mapLayerSatelliteBtdSplit": "ひまわり 分割視窗", + "onboardingPermBackground": "背景定位", + "aedEmergencyPhone": "緊急聯絡電話", + "dpmOpenInMaps": "開啟地圖", + "meshtasticNotifyNodes": "新節點通知", + "onboardingPermCriticalDesc": "讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,顯示底圖", + "meshtasticSent": "已送出", + "homeForecastTitle": "24小時預報", + "typhoonLegendWarningAreas": "警報區域", + "meshtasticExcludeMqttHidden": "已隱藏 {count} 個", + "notifyOptLocalIntensity1": "所在地震度1以上", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "歷史", + "restroomTypeFemale": "女廁所", + "reportListToday": "今天", + "meshtasticTapNode": "點選節點查看詳細資訊", + "commonLoading": "載入中…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "中度颱風", + "typhoonWind": "風速", + "mapLayerSatelliteAsh": "ひまわり 火山灰", + "rainInterval3h": "3 時", + "reportListSearch": "查詢", + "meshtasticChannelReady": "DPIP 頻道已就緒", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "衛星", + "reportFilterLocation": "地點", + "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", + "typhoonIntensityTd": "熱帶性低氣壓", + "reportFilterDate": "日期", + "sponsorRestoreUnavailable": "無法連線至商店,請稍後再試", + "homeForecastPop": "{pop}%", + "regionEmpty": "尚未新增常用地區", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "允許 DPIP 在背景持續運作,避免警報延遲或漏收。", + "mapNavDisaster": "防災", + "radarScanRangeSubtitle": "標示四座雷達實際觀測到的範圍。", + "aedHoursSunday": "週日開放時間", + "reportDetailOriginTime": "發震時間", + "trendNoData": "沒有趨勢資料", + "onboardingPermLocation": "定位", + "moreDiscord": "Discord 社群", + "mapNavPressure": "氣壓", + "mapLayerSatelliteB13": "ひまわり 紅外線(B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "目前沒有更新日誌", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "開始日:當日 00:00(臺北時間)", + "eewTitle": "地震速報", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "zh", + "regionSelectCount": "已選 {count}/{max}", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/雲相", + "meshtasticStateError": "錯誤", + "weatherModeOvercast": "陰天", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "震源深度", + "typhoonOverlayWarningTooltip": "標示警報區域縣市", + "reportFilterDatePick": "選擇日期", + "onboardingSkipStay": "返回授權", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "無法獲取資料,請稍後重試", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "室外收容", + "meshtasticStateConnected": "已連線", + "mapNavRadar": "雷達", + "mapLayerSatelliteCloudClear": "晴空", + "eewSummary": "規模 {magnitude}・深度 {depth} 公里", + "locationBannerPermission": "尚未授權定位,無法針對你的所在地推送警報。", + "typhoonOverlayWeatherNoneTooltip": "不疊雷達或紅外線", + "radarCountyOutlineHint": "畫在回波之上", + "windForecastCountyOutlineHint": "繪製於風場之上", + "homeRainTrendTitle": "近 1 小時降水趨勢", + "moonPhaseFirstQuarter": "上弦月", + "mapLayerCategoryTyphoon": "颱風", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "空中工時(24 小時)", + "restroomTypeMixed": "混合廁所", + "restroomGradeGood": "優等級", + "notifyTsunami": "海嘯資訊", + "navData": "資料", + "mapLayerSatelliteBtdWvirw": "ひまわり 過衝雲頂", + "meshtasticReadingAge": "數值時間", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "此裝置無法撥打電話", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "不限", + "weatherRankingMergeTo": "合併至", + "notifyIntensity": "震度速報", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "累積時段", + "reportDetailLocalFelt": "小區域有感地震", + "meshtasticDevice": "裝置", + "onboardingGrant": "授權", + "weatherModeRain": "雨天", + "shelterVulnerableOkLabel": "適合避難弱者安置", + "stationSheetEmpty": "點選任一測站查看觀測值", + "typhoonLegendProbability": "侵襲機率", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "規模", + "skyTimeMorning": "上午", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "實驗性功能", + "onboardingTermsBody": "使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。", + "reportFilterTitle": "篩選", + "onboardingPermCritical": "重大通知", + "trendCumulativeTotal": "累計 {total} mm", + "languageName": "繁體中文(臺灣)", + "reportListEmptyFiltered": "沒有符合條件的地震報告", + "meshtasticExcludeMqtt": "隱藏 MQTT 節點", + "mapNavTyphoon": "颱風", + "weatherModeSand": "沙塵", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "衛星雲圖", + "@dpmOpenInMaps": {}, + "notifyReport": "地震報告", + "mapAppCoordinatesCopied": "已複製座標", + "skyTimeNight": "夜晚", + "sponsorRecommended": "推薦", + "mapLayerSatelliteB15": "ひまわり 長波紅外線(B15)", + "weatherRankingWind": "風速", + "feedStale": "資料可能已過期", + "homeForecastWind": "{direction} · {level} 級", + "navHome": "首頁", + "meshtasticRegionLabel": "地區", + "mapLayerSatelliteCloudtop": "ひまわり 雲頂溫度", + "moonTimelineCaption": "月相", + "reportListMeta": "M{magnitude} · {depth} 公里", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "引用套件", + "weatherRankingLowest": "最低", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "深度", + "mapTimelineDataTime": "資料時間 {time}", + "radarScanRange": "顯示掃描範圍", + "meshtasticHopLimit": "跳數上限", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "溫差 {value}°C", + "weatherRankingExtremeHigh": "今日最高", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "版本資訊", + "sponsorPrivacy": "隱私權政策", + "reportDetailLocalIntensity": "所在地的震度", + "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", + "meshtasticAirtime": "發射佔空比", + "shelterCapacityValue": "{n} 人", + "lightningLegendCc": "雲間 · {minutes} 分內", + "meshtasticSendHint": "要廣播的訊息", + "monitorDelay": "延遲 {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "否", + "mapLayerSatelliteB08": "ひまわり 上層水氣(B08)", + "meshtasticReconnecting": "重新連線中…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "讓鄉鎮界線在雷達回波下仍然清楚。", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "紅外線(對齊颱風報文時間)", + "radarScanRangeHint": "框外空白代表未觀測", + "typhoonPickerTd": "熱帶性低氣壓 TD {no}", + "mapLayerSatelliteWatervapor": "ひまわり 水氣", + "regionAddButton": "新增地區", + "displaySettings": "顯示設定", + "restroomGradePoor": "不合格", + "restroomCategoryTourist": "觀光地區及風景區", + "locationBannerServiceOff": "定位服務已關閉,無法針對你的所在地推送警報。", + "mapLayerStyleTooltip": "顯示樣式", + "lightningLegendCg": "對地 · {minutes} 分內", + "skyTimeAuto": "自動", + "appLogs": "App 日誌", + "feedConnecting": "連線中…", + "notifyBannerDisabled": "通知已關閉,將收不到災害警報。", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "濕度", "typhoonValueMs": "每秒 {n} 公尺", - "typhoonDataTime": "資料時間\n{time}", - "mapLayerMonitor": "強震監視器", - "mapLayerAed": "AED", + "homeForecastHumidity": "濕度 {value}%", + "meshtasticBusyBody": "請先在另一個 Meshtastic App 中斷線。兩個 App 同時連同一台裝置會互相搶走訊息,導致部分訊息遺失。", + "meshtasticChannelNoSlot": "沒有可用的頻道空位 — 請先在裝置上空出一個", + "restroomCategoryTransport": "交通", + "reportFilterLocationHint": "例如:花蓮、東部海域", + "moonSubtitle": "月相與亮度 — 完全本地計算", + "meshtasticBattery": "電量", + "meshtasticDistance": "距離", + "meshtasticSnrTrend": "訊號趨勢 (SNR)", + "meshtasticBatteryTrend": "電量趨勢", + "typhoonOverlayMenuTooltip": "颱風圖層選項", + "mapLayerSatelliteBtdOzone": "ひまわり 對流層頂", + "meshtasticRegionMismatch": "裝置地區為 {region} — DPIP 需要 TW", + "notifySectionEarthquake": "地震", "mapLayerDisasterMap": "防災地圖", - "disasterMapOverlayMenuTooltip": "防災地圖圖層", - "disasterMapOverlaySectionLayers": "圖層", - "disasterMapOverlayAedTooltip": "顯示 AED 位置", + "weatherModeFog": "大霧", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "氣象廳灰階慣例:溫度越低越白", + "moreAnnouncements": "公告", + "mapLayerSatelliteTransparentNoData": "無資料(陸地) = 透明", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "民眾洽公場所", + "typhoonLegendCurrent": "目前中心", "aedAddress": "地址", - "aedRegion": "縣市區域", - "aedCategory": "場所分類", - "aedType": "場所類型", - "aedPlaceDesc": "放置位置說明", - "aedDescription": "備註", - "aedHoursWeekday": "平日開放時間", - "aedHoursSaturday": "週六開放時間", - "aedHoursSunday": "週日開放時間", - "aedOpenRemark": "開放時間備註", - "aedEmergencyPhone": "緊急聯絡電話", - "mapLayerRestroom": "公廁", + "mapLayerAed": "AED", + "changelogTypePrerelease": "公測", + "reportFilterIntensityInfoModernBody": "震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。", + "typhoonOverlayWeatherNone": "無", + "mapLayerStyleGray": "灰階(JMA)", + "weatherModeAuto": "自動", + "typhoonLabelProbCircle": "70%機率圓", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "接收全部", + "displayTheme": "主題", + "mapLayerSatelliteB07": "ひまわり 短波紅外(B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "過去移動方向", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "常用地區", + "typhoonLegendCone": "預測圓錐", + "moreCwaEew": "中央氣象署強震即時警報", + "onboardingPermsTitle": "權限授權", + "mapLayerStyleJma": "雲頂強調(JMA)", + "rainInterval10m": "10 分", + "weatherRankingAnalysisLow": "最低 {value}", + "meshtasticConnectAnyway": "仍要連線", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "ひまわり 近紅外(B06)", + "mapLayerSatelliteTransparentReflectance": "低反射率/夜間 = 透明,顯示底圖", + "chartHourLabel": "{hour}時", "mapLayerShelter": "避難收容場所", - "disasterMapOverlayRestroomTooltip": "顯示公廁", + "typhoonOverlayProbabilityTooltip": "顯示侵襲機率(會隱藏預測圓錐)", + "mapLayerSatelliteNdwi": "ひまわり 水體指數", "disasterMapOverlayShelterTooltip": "顯示避難收容場所", - "dpmOpenInMaps": "開啟地圖", - "@dpmOpenInMaps": { + "mapNavHumidity": "濕度", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "依震度排序", + "homeRainTrendNoData": "無資料", + "mapLayerCategoryRadar": "雷達", + "meshtasticShortName": "簡稱", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "ひまわり 氣團", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "路徑詳情", + "dataSectionWeather": "氣象", + "aedHoursWeekday": "平日開放時間", + "homeActiveEventsTitle": "生效中事件", + "weatherRankingAnalysisHigh": "最高 {value}", + "faq": "常見問題", + "typhoonHistoryLive": "即時", + "eewSerial": "第 {serial} 報", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "排序方式", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "要將這台裝置切換為 TW 地區嗎?裝置會重新啟動並短暫斷線,上面的其他頻道也會一起改變。", + "dataEarthquakeSubtitle": "地震報告", + "typhoonNoActive": "目前無颱風", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "ひまわり 二氧化硫/雲相(B11)", + "navEvents": "事件", + "onboardingTermsTitle": "服務條款", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "鄉鎮名稱", + "notifySetFailed": "設定失敗,請稍後再試。", + "meshtasticDisconnect": "斷線", + "meshtasticUndecoded": "無法解密", + "notifyAnnouncement": "公告", + "onboardingIntroTitle": "歡迎使用 DPIP", + "regionCurrentUnavailable": "無法取得所在地位置資訊", + "languageSystem": "系統預設", + "skyTimeSunset": "日落", + "mapLayerSatelliteDust": "ひまわり 沙塵", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "修改", + "weatherDynamicState": "天氣動態狀態", + "mapPlaceholderDisabled": "地圖(暫時停用)", + "moonNow": "現在", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "外觀", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "月出月沒", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "接下來", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "月曆", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "距離", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "公里", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "視直徑", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "月出", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "月沒", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "下次新月", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "整日在地平線上", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "當日無", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "太陽", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "日出日沒、曙暮光與節氣", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "日照", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "曙暮光", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "光線", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "日晷", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "節氣", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "日出", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "日沒", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "正午", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "白晝長度", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "民用", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "航海", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "天文", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "晨間黃金時刻", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "昏間黃金時刻", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "藍調時刻", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "均時差", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "分", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "下一個節氣", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "行星", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "今晚在哪、有多亮", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "此刻", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "地平線上", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "地平線下", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "太近太陽", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "亮度", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "距日距角", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "時段", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "昏星", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "晨星", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "距離", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "天文單位", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "仰角", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "水星", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "金星", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "火星", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "木星", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "土星", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "天王星", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "海王星", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "春分", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "清明", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "穀雨", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "立夏", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "小滿", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "芒種", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "夏至", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "小暑", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "大暑", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "立秋", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "處暑", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "白露", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "秋分", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "寒露", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "霜降", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "立冬", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "小雪", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "大雪", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "冬至", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "小寒", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "大寒", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "立春", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "雨水", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "驚蟄", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "今夜", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "現在看得到什麼、什麼時候", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "觀測窗口", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "天文夜", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "整夜不全暗", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "暗窗", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "月亮整夜在天上", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "總暗時", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "月光", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "流星雨", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "輻射點不升起", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "顆/時", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "衛星過境", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "此刻可觀測目標", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "象限儀座", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "天琴座", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "寶瓶座η", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "寶瓶座δ", + "@showerDeltaAquariids": { + "description": "Meteor shower name" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "showerPerseids": "英仙座", + "@showerPerseids": { + "description": "Meteor shower name" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "showerOrionids": "獵戶座", + "@showerOrionids": { + "description": "Meteor shower name" }, - "mapAppDefault": "{app}(預設)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "showerSouthernTaurids": "金牛座南", + "@showerSouthernTaurids": { + "description": "Meteor shower name" }, - "mapAppCopyCoordinates": "複製座標", - "@mapAppCopyCoordinates": { + "showerLeonids": "獅子座", + "@showerLeonids": { + "description": "Meteor shower name" }, - "mapAppCoordinatesCopied": "已複製座標", - "@mapAppCoordinatesCopied": { + "showerGeminids": "雙子座", + "@showerGeminids": { + "description": "Meteor shower name" }, - "mapAppOpenFailed": "無法開啟 {app}", - "@mapAppOpenFailed": { + "showerUrsids": "小熊座", + "@showerUrsids": { + "description": "Meteor shower name" }, - - "mapAppCallFailed": "此裝置無法撥打電話", - - "mapOverlaySectionReference": "參考圖層", - "mapLayerCategoryEarthquake": "地震", - "mapLayerCategoryTyphoon": "颱風", - "mapLayerCategoryWeather": "氣象觀測", - "mapLayerCategorySatellite": "衛星", - "mapLayerCategoryRadar": "雷達", - "mapLayerCategoryLife": "生活", - "mapLayerCategoryForecast": "數值預報", "mapOverlaySectionMap": "地圖", - "rainIntervalSection": "統計時間", - - "mapTownLabels": "鄉鎮名稱", - "mapTownLabelsHint": "放大時顯示鄉鎮名稱", - - "mapTerrainRelief": "地形立體感", - "mapTerrainReliefHint": "在底圖上顯示立體地形陰影", - - "dpmSheetEmpty": "點選地圖上的標記查看詳情", - "dpmAddress": "地址", - "restroomTypeLabel": "廁所類型", - "restroomCategoryLabel": "類別", - "restroomGradeLabel": "等級", - "restroomTypeFemale": "女廁所", - "restroomTypeMale": "男廁所", - "restroomTypeMixed": "混合廁所", - "restroomTypeAccessible": "無障礙廁所", - "restroomTypeGenderNeutral": "性別友善廁所", - "restroomTypeFamily": "親子廁所", - "restroomTypeUnspecified": "未設定", - "restroomCategoryTransport": "交通", - "restroomCategoryPark": "公園", - "restroomCategoryCommercial": "商業營業場所", - "restroomCategoryReligious": "宗教禮儀場所", - "restroomCategoryCultural": "文化育樂活動場所", - "restroomCategoryGovernment": "民眾洽公場所", - "restroomCategoryWelfare": "社福機構、集會場所", - "restroomCategoryTourist": "觀光地區及風景區", - "restroomCategoryLeisure": "休閒娛樂場所", - "restroomCategoryOther": "其他", - "restroomGradeExcellent": "特優級", - "restroomGradeGood": "優等級", - "restroomGradeAverage": "普通級", - "restroomGradePoor": "不合格", - "shelterAddressLabel": "地址", - "shelterCapacityLabel": "收容人數", - "shelterCapacityValue": "{n} 人", - "shelterCategoryLabel": "適用災害", - "shelterIndoorLabel": "室內收容", - "shelterOutdoorLabel": "室外收容", - "shelterVulnerableOkLabel": "適合避難弱者安置", - "dpmYes": "是", - "dpmNo": "否", - "stationSheetEmpty": "點選任一測站查看觀測值", - "monitorDelay": "延遲 {value} s", - "monitorWaiting": "等待資料…", - "mapLegendUnit": "單位:{unit}", - "typhoonLegendPast": "實際路徑", - "typhoonIntensityTd": "熱帶性低氣壓", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "熱帶性低氣壓 TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonIntensityMild": "輕度颱風", - "typhoonIntensityModerate": "中度颱風", - "typhoonIntensityIntense": "強烈颱風", - "typhoonLegendForecast": "預測路徑", - "typhoonLegendForecastPoint": "預測點", - "typhoonLegendCurrent": "目前中心", - "typhoonLegendCone": "預測圓錐", - "mapLegendExpand": "圖例", - "mapLegendCollapse": "收合圖例", - "mapMyLocation": "我的位置", - "mapResetNorth": "回到北方", - "typhoonLegendCircle15": "七級風暴風圈", - "typhoonLegendCircleAvg": "平均圓", - "typhoonLegendCircle25": "十級風暴風圈", - "typhoonStormRadii": "東北 {ne} · 東南 {se} · 西南 {sw} · 西北 {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonLegendProbability": "侵襲機率", - "typhoonLegendWarningAreas": "警報區域", - "typhoonOverlayMenuTooltip": "颱風圖層選項", - "typhoonOverlaySectionStorm": "暴風圈", - "typhoonOverlaySectionExtra": "覆蓋層", - "typhoonOverlayStormBandSubtitle": "含平均圓", - "typhoonOverlayProbabilityHint": "會隱藏預測圓錐", - "typhoonOverlayProbabilityTooltip": "顯示侵襲機率(會隱藏預測圓錐)", - "typhoonOverlayWarningTooltip": "標示警報區域縣市", - "typhoonOverlayStormL7Tooltip": "七級暴風圈+平均圓(紫色)", - "typhoonOverlayStormL10Tooltip": "十級暴風圈+平均圓(黃色)", - "typhoonOverlaySectionWeather": "天氣底圖", - "typhoonOverlayWeatherNone": "無", - "typhoonOverlayWeatherHint": "對齊報文時間", - "typhoonOverlayWeatherNoneTooltip": "不疊雷達或紅外線", - "typhoonOverlayWeatherRadarTooltip": "雷達回波(對齊颱風報文時間)", - "typhoonOverlayWeatherSatelliteTooltip": "紅外線(對齊颱風報文時間)", - "typhoonWarningTitle": "颱風警報", - "typhoonWarningAreas": "警戒區域:{areas}", - "typhoonTrackDetail": "路徑詳情", - "typhoonHistoryTitle": "資料時間", - "typhoonHistoryLive": "即時", - "typhoonSatelliteTitle": "衛星雲圖", - "typhoonOverlayForecastCallouts": "預測點資訊", - "typhoonOverlayForecastCalloutsTooltip": "放大時顯示預測點詳細卡片", - "dpmFilterSectionRestroom": "場所類型", - "dpmFilterSectionRestroomType": "廁所類型", - "dpmFilterSectionShelter": "避難所災害類型", - "dpmDisasterFlood": "水災", - "dpmDisasterEarthquake": "震災", - "dpmDisasterLandslide": "土石流", - "dpmDisasterTsunami": "海嘯", - "dpmDisasterSlope": "坡地災害", - "dpmDisasterNuclear": "核子事故", - "skyTime": "天空時間", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "deepSkyOpenCluster": "疏散星團", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" }, - "skyTimeAuto": "自動", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "deepSkyGlobularCluster": "球狀星團", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" }, - "skyTimeDawn": "黎明", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "deepSkySpiralGalaxy": "螺旋星系", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" }, - "skyTimeSunrise": "日出", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "deepSkyEllipticalGalaxy": "橢圓星系", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" }, - "skyTimeMorning": "上午", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "deepSkyIrregularGalaxy": "不規則星系", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" }, - "skyTimeNoon": "正午", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "deepSkyPlanetaryNebula": "行星狀星雲", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" }, - "skyTimeAfternoon": "下午", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "deepSkySupernovaRemnant": "超新星遺跡", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" }, - "skyTimeGolden": "黃金時刻", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "deepSkyEmissionNebula": "發射星雲", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" }, - "skyTimeSunset": "日落", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "deepSkyReflectionNebula": "反射星雲", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" }, - "skyTimeDusk": "暮色", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "deepSkyAsterism": "星群", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" }, - "skyTimeNight": "夜晚", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "almanacTitle": "曆法", + "@almanacTitle": { + "description": "Almanac page title" }, - "weatherModeCloudy": "多雲", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "almanacSubtitle": "農曆日期與未來的日月食", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" }, - "weatherModeOvercast": "陰天", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "almanacSectionToday": "今日", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" }, - "weatherModeSnow": "下雪", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "almanacGregorian": "西曆", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "weatherModeSand": "沙塵", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "almanacLunar": "農曆", + "@almanacLunar": { + "description": "The lunisolar date" }, - "radarScanRange": "顯示掃描範圍", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "almanacYear": "歲次", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "radarScanRangeSubtitle": "標示四座雷達實際觀測到的範圍。", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "almanacMonthLength": "月大小", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "radarScanRangeHint": "框外空白代表未觀測", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "almanacLongMonth": "三十日", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "radarOverlayMenuTooltip": "雷達圖層選項", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "almanacShortMonth": "二十九日", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "radarCountyOutline": "縣市界線", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "almanacLeapPrefix": "閏", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - "radarGlobalOutline": "國界", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "almanacSectionLunarEclipses": "月食", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "radarGlobalOutlineHint": "各國國界外框", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "almanacSectionSolarEclipses": "日食", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "radarCountyOutlineHint": "畫在回波之上", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "almanacNoSolarEclipse": "範圍內無", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "radarCountyOutlineSubtitle": "讓縣市界線在雷達回波下仍然清楚。", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "eclipseTotal": "全食", + "@eclipseTotal": { + "description": "Eclipse type" }, - "radarTownOutline": "鄉鎮界線", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "eclipsePartial": "偏食", + "@eclipsePartial": { + "description": "Eclipse type" }, - "radarTownOutlineHint": "較細的分區", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "eclipseAnnular": "環食", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "radarTownOutlineSubtitle": "讓鄉鎮界線在雷達回波下仍然清楚。", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "eclipsePenumbral": "半影食", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "qpesumsOverlayMenuTooltip": "定量降水預報圖層選項", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "zodiacRat": "鼠", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "windForecastOverlayMenuTooltip": "風場預報圖層選項", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "zodiacOx": "牛", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "windForecastCountyOutlineHint": "繪製於風場之上", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "zodiacTiger": "虎", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "windForecastGlobalOutlineHint": "各國國界外框", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "zodiacRabbit": "兔", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "windForecastTownOutlineHint": "更細的網格", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "zodiacDragon": "龍", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "eewSerial": "第 {serial} 報", - "eewMaxIntensity": "最大震度", - "eewLocalIntensity": "所在地預估", - "eewSWave": "震波", - "eewArrived": "已抵達", - "eewCountdown": "{seconds} 秒" + "zodiacSnake": "蛇", + "@zodiacSnake": { + "description": "Chinese zodiac animal" + }, + "zodiacHorse": "馬", + "@zodiacHorse": { + "description": "Chinese zodiac animal" + }, + "zodiacGoat": "羊", + "@zodiacGoat": { + "description": "Chinese zodiac animal" + }, + "zodiacMonkey": "猴", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" + }, + "zodiacRooster": "雞", + "@zodiacRooster": { + "description": "Chinese zodiac animal" + }, + "zodiacDog": "狗", + "@zodiacDog": { + "description": "Chinese zodiac animal" + }, + "zodiacPig": "豬", + "@zodiacPig": { + "description": "Chinese zodiac animal" + }, + "tideTitle": "潮汐", + "@tideTitle": { + "description": "Tide page title" + }, + "tideSubtitle": "大潮、小潮與月球引力", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" + }, + "tideDisclaimer": "僅為天文引潮力,非港口潮汐表。水位請參考氣象署公布之潮汐預報。", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" + }, + "tideSectionNow": "此刻", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" + }, + "tidePhase": "週期", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" + }, + "tideSpring": "大潮", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" + }, + "tideNeap": "小潮", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" + }, + "tideMiddling": "中潮", + "@tideMiddling": { + "description": "Between spring and neap" + }, + "tideLunarDistanceFactor": "月球引力", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" + }, + "tideEquilibrium": "平衡潮高", + "@tideEquilibrium": { + "description": "The equilibrium tide height" + }, + "tideMetres": "公尺", + "@tideMetres": { + "description": "Unit: metres" + }, + "tidePerigeanSpring": "下次近地點大潮", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" + }, + "tideSectionTurningPoints": "轉折點", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" + }, + "tideHigh": "高", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "低", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "星圖", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "頭頂上肉眼可見的天空", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "北", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "東", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "南", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "西", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "軌道資料 {days} 天前", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}{month} 月 {day} 日", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "目前無流星雨", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "48 小時內無可見過境", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "無法讀取軌道資料", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "無足夠高度的目標", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "無法讀取星表", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb index 747c754d2..e87c088d4 100644 --- a/lib/l10n/app_zh_Hans.arb +++ b/lib/l10n/app_zh_Hans.arb @@ -1,679 +1,1739 @@ { - "@@locale": "zh_Hans", - "languageName": "简体中文", - "navHome": "主页", - "navEvents": "事件", - "navMap": "地图", - "navData": "资料", - "navEarthquake": "地震", - "dataSectionSeismic": "地震", - "dataEarthquakeSubtitle": "地震报告", - "dataSectionWeather": "气象", - "dataWeatherRankingSubtitle": "即时观测排行", - "weatherRankingTitle": "观测排行", - "weatherRankingMeta": "资料时间:{time}\n共 {count} 观测点", - "weatherRankingEmpty": "目前没有可排序的观测", - "weatherRankingBy": "依", - "weatherRankingHighest": "最高", - "weatherRankingLowest": "最低", - "weatherRankingMergeTo": "合并至", - "weatherRankingMergeTown": "乡镇", - "weatherRankingMergeCounty": "县市", - "weatherRankingWind": "风速", - "weatherRankingGust": "阵风", + "typhoonValueLat": "北纬 {lat} 度", + "onboardingSkipBody": "未授权定位与通知,DPIP 将无法实时通知你所在地的地震与灾害。你仍可稍后在设置中开启。", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 时", + "homeRainTrendHeavyStopping": "预计 {minutes} 分钟后停止下大雨", + "mapTimelineObserved": "观测", + "regionSelectTitle": "选择地区", + "skyTimeNoon": "正午", + "radarCountyOutlineSubtitle": "让县市界线在雷达回波下仍然清楚。", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "厕所类型", + "mapLayerSatelliteB03": "ひまわり 可见光-红(B03)", + "reportFilterIntensity": "震度", + "mapLayerLightning": "闪电", + "restroomTypeMale": "男厕所", + "meshtasticLastReceived": "最近接收", + "reportDetailSortByCounty": "依县市排序", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "可能会有零星降雨", + "meshtasticUptime": "运行时间", "weatherRankingTempExtremes": "温度极值", - "weatherRankingExtremeHigh": "今日最高", - "weatherRankingExtremeLow": "今日最低", + "themeLight": "浅色", + "mapTerrainReliefHint": "在底图上显示立体地形阴影", + "meshtasticEmptyMessage": "(空白讯息)", + "moreSectionRegion": "地区", + "dpmDisasterEarthquake": "震灾", + "mapLayerSatellite": "ひまわり 红外线(B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "周六开放时间", + "dpmDisasterSlope": "坡地灾害", + "moonPhaseNew": "新月", + "notifySectionEew": "地震预警", + "mapResetNorth": "回到正北", + "rainInterval2d": "2 日", + "mapTownLabelsHint": "放大时显示乡镇名称", + "commonCancel": "取消", + "notifyOptTsunamiWarning": "仅接收海啸警报", + "mapLayerSatelliteBtdFog": "ひまわり 夜间雾", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "高级", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "日温差", + "notifySettingsMenu": "通知设置", + "typhoonHistoryTitle": "资料时间", + "mapAppDefault": "{app}(默认)", + "trendRange24h": "24 小时", + "mapLayerStyleJmaTooltip": "灰阶为底,−40 °C 以下上色,凸显云顶高度", "weatherRankingRecordedAt": "记录于 {time}", - "weatherRankingAnalysisCurrent": "当下 {value}°C", - "weatherRankingAnalysisHigh": "最高 {value}", - "weatherRankingAnalysisLow": "最低 {value}", - "weatherRankingAnalysisRange": "温差 {value}°C", - "reportListEmpty": "当前没有地震报告", - "reportListEmptyFiltered": "没有符合条件的地震报告", - "reportListMeta": "M{magnitude} · {depth} 公里", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "公里", - "reportListLocalFelt": "小区域有感", - "reportListToday": "今天", - "reportListYesterday": "昨天", - "reportListDayCount": "{count}", - "reportListEnd": "已到最后一页", - "reportFilterTitle": "筛选", - "reportFilterSort": "排序方式", - "reportFilterSortTime": "时间", - "reportFilterSortIntensity": "震度", - "reportFilterSortMagnitude": "规模", - "reportFilterSortDepth": "深度", + "mapLayerRain": "雨量", + "mapLayerQpesums": "未来 1 小时降水预报", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "地图", + "mapTerrainRelief": "地形立体感", + "eewMaxIntensity": "最大震度", + "mapLegendCollapse": "收起图例", + "changelogTitle": "更新日志", "reportFilterOrderDesc": "降序", - "reportFilterOrderAsc": "升序", - "reportFilterIntensity": "震度", + "meshtasticExcludeMqttSubtitle": "经互联网桥接、并非无线电听到的节点", "reportFilterIntensityInfoTitle": "震度新制与旧制", - "reportFilterIntensityInfoIntro": "中央气象署自 2020 年 1 月 1 日(台北时间)起改用新制震度。", - "reportFilterIntensityInfoLegacyTitle": "旧制(2020 以前)", - "reportFilterIntensityInfoLegacyBody": "震度仅 0–7,没有 5弱/5强/6弱/6强。", - "reportFilterIntensityInfoModernTitle": "新制(2020 起)", - "reportFilterIntensityInfoModernBody": "震度为 0–4、5弱、5强、6弱、6强、7。筛选滑杆依新制;列表中较早的地震会以旧制标示显示。", - "reportFilterMagnitude": "规模", - "reportFilterDepth": "深度", - "reportFilterDepthKm": "{depth} 公里", - "reportFilterDate": "日期", - "reportFilterDatePick": "选择日期", - "reportFilterDateStartNote": "开始日:当日 00:00(台北时间)", + "mapLayerTyphoon": "台风", + "radarOverlayMenuTooltip": "雷达图层选项", + "mapMyLocation": "我的位置", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "節點", + "meshtasticSend": "傳送", + "typhoonOverlayStormL7Tooltip": "七级风风场 + 平均圆(紫)", + "aedType": "场所类型", + "termsOfService": "服务条款", + "typhoonLegendCircle25": "十级风暴风圈", + "sponsorTitle": "支持 DPIP", + "mapNavSatellite": "卫星", + "homeRainTrendUpdated": "更新 {time}", + "onboardingNext": "下一步", + "weatherRankingMergeTown": "乡镇", + "mapLayerMonitor": "强震监视器", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "订阅制", + "typhoonValueLon": "东经 {lon} 度", + "skyTime": "天空时间", + "weatherModeCloudy": "多云", + "skyTimeDusk": "暮色", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "固件", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "结束日:当日 24:00(台北时间)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "地点", - "reportFilterLocationHint": "例如:花莲、东部海域", - "reportFilterAny": "不限", - "reportFilterApply": "应用", - "reportFilterReset": "重置", - "reportListSearch": "查询", - "reportDetailTitle": "地震报告", - "reportDetailNumbered": "编号 {number} 显著有感地震", - "reportDetailLocalFelt": "小区域有感地震", - "reportDetailInfo": "详细信息", - "reportDetailOriginTime": "发震时间", - "reportDetailEpicenter": "震中坐标", - "reportDetailMagnitude": "地震规模", - "reportDetailDepth": "震源深度", - "reportDetailAreaIntensity": "各地震度", - "reportDetailLocalIntensity": "所在地的震度", - "reportDetailLocalIntensityUnavailable": "没有震度信息", - "reportDetailSortByIntensity": "依震度排序", - "reportDetailSortByCounty": "依县市排序", - "reportDetailImage": "地震报告图", - "reportDetailImageUnavailable": "报告图尚未提供", - "reportDetailOpenReport": "报告页面", - "reportDetailReplay": "重播", - "navMore": "更多", - "appLogs": "应用日志", - "changelogTitle": "更新日志", - "changelogEmpty": "目前没有更新日志", - "changelogTypePrerelease": "公测", - "changelogTypeStable": "正式", - "changelogCurrentVersion": "当前版本", - "changelogVersionDetails": "版本信息", - "changelogBodyEmpty": "此版本没有说明。", - "mapPlaceholderDisabled": "地图(暂时禁用)", - "moreSectionRegion": "地区", - "moreSectionNotify": "通知", - "moreSectionDisplay": "显示", - "regionManageTitle": "常用地区", - "regionAddButton": "添加地区", - "regionEmpty": "尚未添加常用地区", - "regionSelectTitle": "选择地区", - "regionSelectCount": "已选 {count}/{max}", - "regionSelectFull": "最多只能选择 {max} 个地区", - "regionEdit": "修改", - "moreSectionAdvanced": "高级", - "moreDeveloper": "调试信息", - "experimentalFeatures": "实验性功能", - "moreSectionLinks": "相关链接", - "moreCwaEew": "中央气象署地震预警", - "moreTremReport": "TREM 检测报告", - "moreServerStatus": "服务器状态", - "moreAnnouncements": "公告", - "moreDiscord": "Discord 社区", - "moreNotifyLog": "DPIP 通知发送记录", - "moreLinkOpenFailed": "无法打开链接", - "weatherDynamicState": "天气动画", - "weatherDynamicStateSubtitle": "覆盖首页背景天气", - "weatherModeAuto": "自动", - "weatherModeClear": "晴天", - "weatherModeRain": "雨天", - "weatherModeFog": "大雾", - "weatherModeThunderstorm": "雷雨", - "commonLoading": "加载中…", - "commonRetry": "重试", - "commonError": "出错了", - "commonFetchFailed": "无法获取数据,请稍后重试", - "commonEmpty": "暂无内容", - "feedConnecting": "连接中…", - "feedStale": "数据可能已过期", - "feedOffline": "连接中断", - "eewTitle": "地震预警", - "eewNone": "当前没有地震预警", - "eewSummary": "震级 {magnitude}·深度 {depth} 公里", - "regionNationwide": "全国", - "regionCurrent": "当前位置", - "regionCurrentUnavailable": "无法获取所在地位置信息", - "weatherPrecipitation": "降水量", - "weatherHumidity": "湿度", - "weatherDataTime": "{station} ∙ 资料时间 {time}", - "homeViewOnMap": "前往地图察看", - "homeForecastTitle": "24小时预报", + "meshtasticSilent": "已静默", + "reportFilterSortMagnitude": "规模", + "mapLayerCategoryEarthquake": "地震", + "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", + "typhoonLegendPast": "实际路径", + "restroomCategoryOther": "其他", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "高 {high}° · 低 {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "体感 {temp}°", - "homeForecastHumidity": "湿度 {value}%", - "homeForecastWind": "{direction} · {level} 级", - "homeForecastUnavailable": "选择乡镇后可查看预报", - "homeForecastEmpty": "目前没有预报数据", - "homeActiveEventsTitle": "生效中事件", - "homeActiveEventsEmpty": "目前没有生效中的事件", - "homeRainTrendTitle": "近 1 小时降水趋势", - "homeRainTrendMinute": "{minute}分", - "homeRainTrendUpdated": "更新 {time}", - "homeRainTrendNoData": "无资料", - - "homeRainTrendScattered": "可能会有零星降雨", - "homeRainTrendLightSustained": "未来 1 小时会有持续小雨", - "homeRainTrendLightStopping": "预计 {minutes} 分钟后停止下小雨", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "打开设置", + "mapLegendExpand": "图例", + "eewNone": "当前没有地震预警", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "海啸消息、海啸警报", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "节点选项", + "onboardingAgreeContinue": "同意并继续", + "meshtasticNodeId": "节点 ID", + "commonRetry": "重试", + "reportDetailNumbered": "编号 {number} 显著有感地震", + "typhoonOverlayStormBandSubtitle": "含平均圆", + "disasterMapOverlayRestroomTooltip": "显示公厕", + "weatherRankingTitle": "观测排行", "homeRainTrendHeavySustained": "未来 1 小时会有持续大雨", - "homeRainTrendHeavyStopping": "预计 {minutes} 分钟后停止下大雨", - "mapLayers": "图层", - "mapLayerOrderTitle": "调整图层顺序", - "mapLayerOrderReset": "恢复默认顺序", - "mapLayerRadar": "雷达合成回波图", - "mapLayerSatellite": "ひまわり 红外线(B13)", - "mapLayerSatelliteB01": "ひまわり 可见光-蓝(B01)", - "mapLayerSatelliteB02": "ひまわり 可见光-绿(B02)", - "mapLayerSatelliteB03": "ひまわり 可见光-红(B03)", - "mapLayerSatelliteB04": "ひまわり 近红外(B04)", + "notifySectionTsunami": "海啸", + "restroomCategoryPark": "公园", + "moreLinkOpenFailed": "无法打开链接", + "themeDark": "深色", + "sponsorRestore": "恢复购买", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "正在设定 DPIP 频道…", + "meshtasticRegionSwitch": "切换为 TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "流量", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD 曲线——热带气旋强度分析的阶梯灰度", + "disasterMapOverlayAedTooltip": "显示 AED 位置", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "湿度", + "mapLayerSatelliteTransparentNight": "夜间 = 透明,显示底图", + "meshtasticScanning": "掃描中…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "最多只能选择 {max} 个地区", + "meshtasticTitle": "Meshtastic", + "navMore": "更多", + "meshtasticDpipChannel": "DPIP 频道", + "disasterMapOverlaySectionLayers": "图层", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "ひまわり 近红外(B05)", - "mapLayerSatelliteB06": "ひまわり 近红外(B06)", - "mapLayerSatelliteB07": "ひまわり 短波红外(B07)", - "mapLayerSatelliteB08": "ひまわり 上层水气(B08)", - "mapLayerSatelliteB09": "ひまわり 中层水气(B09)", - "mapLayerSatelliteB10": "ひまわり 低层水气(B10)", - "mapLayerSatelliteB11": "ひまわり 二氧化硫/云相(B11)", - "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", - "mapLayerSatelliteB13": "ひまわり 红外线(B13)", - "mapLayerSatelliteB14": "ひまわり 长波红外线(B14)", - "mapLayerSatelliteB15": "ひまわり 长波红外线(B15)", - "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "东北侧", + "meshtasticCopied": "已复制讯息", + "reportListEmpty": "当前没有地震报告", + "reportListEnd": "已到最后一页", "mapLayerSatelliteTruecolor": "ひまわり 真彩色", - "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", - "mapLayerSatelliteAsh": "ひまわり 火山灰", - "mapLayerSatelliteDust": "ひまわり 沙尘", - "mapLayerSatelliteAirmass": "ひまわり 气团", - "mapLayerSatelliteNightmicrophysics": "ひまわり 夜间微物理", - "mapLayerSatelliteWatervapor": "ひまわり 水气", - "mapLayerSatelliteBtdSplit": "ひまわり 分割视窗", - "mapLayerSatelliteBtdFog": "ひまわり 夜间雾", - "mapLayerSatelliteBtdWvirw": "ひまわり 过冲云顶", - "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/云相", - "mapLayerSatelliteBtdCo2": "ひまわり 卷云/云高", - "mapLayerSatelliteBtdOzone": "ひまわり 对流层顶", - "mapLayerSatelliteCloudtop": "ひまわり 云顶温度", - "mapLayerSatelliteCloudmask": "ひまわり 云遮罩", - "mapLayerSatelliteSst": "ひまわり 海表温度", - "mapLayerSatelliteNdvi": "ひまわり 植被指数", - "mapLayerSatelliteNdwi": "ひまわり 水体指数", - "mapLayerSatelliteMndwi": "ひまわり 改良水体指数", + "typhoonOverlaySectionExtra": "叠加层", + "eewSWave": "震波", + "meshtasticBusyTitle": "另一个 App 正在使用这台设备", + "restroomCategoryCultural": "文化育乐活动场所", + "typhoonLabelWind": "近中心最大风速", + "radarGlobalOutlineHint": "各国国界外框", + "notifyEvacuation": "防灾信息", + "typhoonLegendCircle15": "七级风暴风圈", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "天文", + "homeRainTrendLightSustained": "未来 1 小时会有持续小雨", + "commonError": "出错了", + "moonPhaseWaningCrescent": "殘月", + "meshtasticPower": "电力", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "现在", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "报告页面", + "trendRange7d": "7 天", + "typhoonWarningAreas": "警戒区域:{areas}", + "rainIntervalSection": "统计时间", + "notifyTitle": "通知", + "meshtasticTxPower": "发射功率", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "类别", + "sponsorRestoring": "正在恢复购买…", + "sponsorIntro": "DPIP 致力于提供实时防灾信息,没有广告或其他盈利模式。您的支持能帮助我们维持服务器运行并持续开发。", + "shelterAddressLabel": "地址", + "typhoonLabelStormAvg": "十级风平均暴风半径", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "商业营业场所", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "县市区域", + "homeRainTrendLightStopping": "预计 {minutes} 分钟后停止下小雨", + "reportDetailInfo": "详细信息", + "mapNavWind": "风向", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "风场预报图层选项", + "dataWeatherRankingSubtitle": "即时观测排行", + "rainInterval6h": "6 时", + "homeRainTrendMinute": "{minute}分", + "restroomTypeUnspecified": "未设定", + "typhoonOverlayProbabilityHint": "会隐藏预测圆锥", "mapLayerSatelliteGlobalOutline": "国界", - "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", - "mapLayerSatelliteCloudClear": "晴空", - "mapLayerSatelliteCloudProbablyClear": "可能晴空", - "mapLayerSatelliteCloudProbablyCloudy": "可能有云", + "mapNavTemperature": "温度", + "typhoonLegendForecastPoint": "预测点", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "昨天", + "moreSectionLinks": "相关链接", + "feedOffline": "连接中断", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "显示", + "rainInterval3d": "3 日", + "defaultMapLayerSubtitle": "打开地图标签页时显示此图层,底部导航栏图标与文字会一并更新。", + "aedDescription": "备注", + "typhoonOverlayWeatherRadarTooltip": "最接近台风报文时间的雷达回波", + "onboardingPermLocationDesc": "根据你所在的位置推送本地预警。", + "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "目前没有生效中的事件", + "typhoonLabelPosition": "中心位置", + "weatherRankingBy": "依", + "typhoonIntensityMild": "轻度台风", + "windForecastGlobalOutlineHint": "各国国界外框", + "rainInterval1h": "1 时", + "eewLocalIntensity": "所在地预估", + "mapLayerRadar": "雷达合成回波图", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "宗教礼仪场所", + "meshtasticRole": "角色", "mapLayerSatelliteCloudCloudy": "有云", - "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,显示底图", - "mapLayerSatelliteTransparentReflectance": "低反射率/夜间 = 透明,显示底图", - "mapLayerSatelliteTransparentZero": "零差值 = 透明(无信号)", - "mapLayerSatelliteTransparentNight": "夜间 = 透明,显示底图", - "mapLayerSatelliteTransparentNoData": "无资料(陆地) = 透明", - "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(无植被)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(无水体)", - "mapLayerSatelliteTransparentClear": "晴空 = 透明,显示底图", + "skyTimeSunrise": "日出", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "尚无讯息", + "onboardingPermNotifyDesc": "在地震、天气与灾害发生时,即时推送预警通知。", + "radarTownOutline": "乡镇界线", "mapLayerStyleSection": "显示样式", - "mapLayerStyleTooltip": "显示样式", - "mapLayerStyleGray": "灰度(JMA)", - "mapLayerStyleGrayTooltip": "气象厅灰度惯例:温度越低越白", - "mapLayerStyleJma": "云顶强调(JMA)", - "mapLayerStyleJmaTooltip": "灰阶为底,−40 °C 以下上色,凸显云顶高度", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD 曲线——热带气旋强度分析的阶梯灰度", - "mapLayerQpesums": "未来 1 小时降水预报", - "mapLayerLightning": "闪电", - "lightningLegendCg": "对地 · {minutes} 分钟内", - "lightningLegendCc": "云间 · {minutes} 分钟内", - "mapTimelineNow": "现在", - "mapTimelinePast": "历史", - "mapTimelineFuture": "未来", - "mapTimelineObserved": "观测", - "mapTimelineForecast": "预报", - "mapTimelineDataTime": "资料时间 {time}", - "notifySettingsMenu": "通知设置", - "notifyTitle": "通知", - "notifyUnavailable": "推送通知尚未就绪,请稍后再试。", - "notifySetFailed": "设置失败,请稍后再试。", - "notifySectionEew": "地震预警", - "notifySectionEarthquake": "地震", - "notifySectionWeather": "天气", - "notifySectionTsunami": "海啸", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "防灾地图图层", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "近期听到", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "西南侧", + "typhoonForecastLead": "预测 +{hours} 小时", + "dpmDisasterTsunami": "海啸", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "正式", + "mapLayerSatelliteTransparentClear": "晴空 = 透明,显示底图", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "参考图层", + "mapLayerSatelliteB02": "ひまわり 可见光-绿(B02)", + "reportListLocalFelt": "小区域有感", + "weatherRankingEmpty": "目前没有可排序的观测", "notifySectionOther": "其他", - "notifyEew": "紧急地震预警", - "notifyMonitor": "强震监视器", - "notifyReport": "地震报告", - "notifyIntensity": "震度速报", - "notifyThunderstorm": "雷雨预警", - "notifyAdvisory": "气象预警", - "notifyEvacuation": "防灾信息", - "notifyTsunami": "海啸信息", - "notifyAnnouncement": "公告", - "notifyOptOff": "关闭", - "notifyOptAll": "接收全部", + "weatherRankingMeta": "资料时间:{time}\n共 {count} 观测点", + "onboardingTermsAgree": "我已阅读并同意服务条款", + "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(无植被)", "notifyOptLocalIntensity4": "本地震度4以上", - "notifyOptLocalIntensity1": "本地震度1以上", - "notifyOptWeatherLocal": "仅接收当前位置", - "notifyOptTsunamiWarning": "仅接收海啸警报", - "notifyOptTsunamiAll": "海啸消息、海啸警报", - "onboardingNext": "下一步", - "onboardingBack": "上一步", + "eewArrived": "已抵达", + "meshtasticNoDevices": "找不到 Meshtastic 裝置", + "mapLayerCategoryLife": "生活", + "reportFilterSortIntensity": "震度", + "typhoonMotion": "移动", + "meshtasticStateDisconnected": "未連線", + "typhoonIntensityIntense": "强烈台风", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "调整图层顺序", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "是", + "meshtasticNoHistory": "历史纪录还不够", + "reportDetailLocalIntensityUnavailable": "没有震度信息", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "公里", + "reportFilterDepth": "深度", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "向下滚动以继续", - "onboardingIntroTitle": "欢迎使用 DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "预报", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "地图", + "notifyAdvisory": "气象预警", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "重置", + "mapLayerSatelliteMndwi": "ひまわり 改良水体指数", + "typhoonOverlaySectionStorm": "暴风圈", + "moonPhaseFull": "滿月", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "虧凸月", + "weatherDynamicStateSubtitle": "覆盖首页背景天气", + "reportFilterIntensityInfoModernTitle": "新制(2020 起)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "资料时间\n{time}", + "restroomTypeAccessible": "无障碍厕所", + "moreSectionAbout": "关于", + "meshtasticSelectDevice": "选择装置", "onboardingIntroBody": "DPIP 是与你并肩的防灾伙伴,整合地震预警、地震报告、天气与各类灾害信息,在关键时刻即时通知你。\n\n• 地震:地震预警、震度速报与详细报告\n• 天气:实时雷雨消息与气象预警\n• 海啸与防灾信息\n\n接下来,我们会请你阅读服务条款,并授权几项权限,让 DPIP 能实时守护你。", - "onboardingTermsTitle": "服务条款", - "onboardingTermsBody": "使用 DPIP 前,请详细阅读以下注意事项:\n\n• 任何信息均应以中央气象署(CWA)发布的内容为准。\n\n• 受网络状态、服务器状态、应用程序状态、上游数据来源状态等因素影响,存在收不到信息的可能,我们会尽力避免此类情况,但不保证一定不会发生。\n\n• 强烈震动有可能比通知更早抵达您所在的位置。\n\n• 地震预警为快速计算的结果,可能存在较大误差,请理解并谨慎使用。\n\n• 任何未获官方认可的行为均可能承担法律风险,请务必遵守相关规定。\n\n此外,为提供本地化预警,本服务会在前台及后台收集并上传您的大致位置与设备推送标识符,仅用于决定应向您推送哪些预警。\n\n点击下方“同意并继续”即表示您已阅读、理解并同意上述事项。", - "onboardingTermsAgree": "我已阅读并同意服务条款", - "onboardingAgreeContinue": "同意并继续", - "onboardingPermsTitle": "权限授权", - "onboardingPermsBody": "为了在灾害发生的第一时间通知你,请授权以下权限。你可以随时在系统设置中更改。", + "shelterCapacityLabel": "收容人数", + "reportDetailImage": "地震报告图", + "meshtasticStateConfiguring": "設定中…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "七级风平均暴风半径", "onboardingPermNotify": "通知", - "onboardingPermNotifyDesc": "在地震、天气与灾害发生时,即时推送预警通知。", - "onboardingPermCritical": "重要警告", - "onboardingPermCriticalDesc": "让危及生命的地震预警,即使在静音或勿扰模式下也能发出声响。", - "onboardingPermLocation": "定位", - "onboardingPermLocationDesc": "根据你所在的位置推送本地预警。", - "onboardingPermBackground": "后台定位", - "onboardingPermBackgroundDesc": "选择“始终允许”,关闭应用后也能向你推送本地预警。", - "onboardingPermBattery": "电池优化白名单", - "onboardingPermBatteryDesc": "允许 DPIP 在后台持续运行,避免预警延迟或漏收。", - "onboardingGrant": "授权", - "onboardingGranted": "已授权", - "onboardingStart": "开始使用", - "language": "语言", - "languageSettings": "语言设置", - "languageSystem": "系统默认", - "locationBannerServiceOff": "定位服务已关闭,无法向你所在的区域推送本地预警。", - "locationBannerPermission": "尚未授予定位权限,无法向你所在的区域推送本地预警。", - "locationBannerFix": "打开设置", - "notifyBannerDisabled": "通知已关闭,将收不到灾害警报。", - "onboardingSkipTitle": "尚未完成授权", - "onboardingSkipBody": "未授权定位与通知,DPIP 将无法实时通知你所在地的地震与灾害。你仍可稍后在设置中开启。", - "onboardingSkipStay": "返回授权", - "onboardingSkipLeave": "仍要跳过", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "清除讯息", + "meshtasticNotifyMessages": "新讯息通知", + "defaultMapLayerSettings": "地图默认图层", + "moreSectionNotify": "通知", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "推送通知尚未就绪,请稍后再试。", + "mapLayerOrderReset": "恢复默认顺序", + "dpmAddress": "地址", + "weatherRankingMergeCounty": "县市", + "moreSectionApp": "获取 App", + "reportFilterIntensityInfoLegacyBody": "震度仅 0–7,没有 5弱/5强/6弱/6强。", + "mapLayerSatelliteSst": "ひまわり 海表温度", + "qpesumsOverlayMenuTooltip": "定量降水预报图层选项", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "未来", + "typhoonLegendCircleAvg": "平均圆", + "reportFilterDepthKm": "{depth} 公里", + "typhoonLabelSe": "东南侧", + "radarTownOutlineHint": "较细的分区", + "eewCountdown": "{seconds} 秒", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "瞬间最大阵风", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "使用条款", + "restroomTypeGenderNeutral": "性别友善厕所", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "雷雨预警", + "skyTimeGolden": "黄金时刻", + "moonAge": "月齡", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "当下 {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "选择乡镇后可查看预报", + "mapLayers": "图层", + "meshtasticHardware": "硬件", + "languageSettings": "语言设置", + "dpmDisasterNuclear": "核子事故", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "语言", + "homeForecastFeelsLike": "体感 {temp}°", + "typhoonOverlayWeatherHint": "对齐报文时间", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "黎明", + "skyTimeAfternoon": "下午", + "meshtasticLastHeard": "最后听到", + "typhoonWarningTitle": "台风警报", "moreSourceCode": "源代码", - "moreSectionApp": "获取 App", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "显示设置", - "defaultMapLayerSettings": "地图默认图层", - "defaultMapLayerSubtitle": "打开地图标签页时显示此图层,底部导航栏图标与文字会一并更新。", - "mapNavRadar": "雷达", - "mapNavQpesums": "预报", - "mapNavSatellite": "卫星", - "mapNavLightning": "闪电", - "mapNavTyphoon": "台风", + "mapLayerCategoryWeather": "气象观测", + "mapLayerSatelliteB09": "ひまわり 中层水气(B09)", + "windForecastTownOutlineHint": "更细的网格", + "mapLayerSatelliteCloudmask": "ひまわり 云遮罩", + "mapAppCopyCoordinates": "复制坐标", + "reportFilterIntensityInfoIntro": "中央气象署自 2020 年 1 月 1 日(台北时间)起改用新制震度。", "mapNavEarthquake": "地震", - "mapNavTemperature": "温度", - "mapNavHumidity": "湿度", - "mapNavPressure": "气压", - "mapNavWind": "风向", + "typhoonGust": "阵风", + "restroomGradeAverage": "普通级", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "ひまわり 卷云/云高", + "onboardingPermBackgroundDesc": "选择“始终允许”,关闭应用后也能向你推送本地预警。", + "mapTimelineForecast": "预报", + "restroomTypeLabel": "厕所类型", + "navEarthquake": "地震", + "typhoonOverlayStormL10Tooltip": "十级风风场 + 平均圆(黄)", + "moonPhaseWaxingGibbous": "盈凸月", + "reportDetailTitle": "地震报告", + "moreTremReport": "TREM 检测报告", + "weatherDataTime": "{station} ∙ 资料时间 {time}", + "meshtasticNoNodes": "尚未听到任何节点", + "meshtasticViaMqtt": "经 MQTT(互联网)", + "radarCountyOutline": "县市界线", + "onboardingGranted": "已授权", + "@mapAppCopyCoordinates": {}, + "commonClose": "关闭", + "restroomGradeLabel": "等级", + "rainIntervalNow": "今日", + "changelogCurrentVersion": "当前版本", + "typhoonOverlayForecastCalloutsTooltip": "放大时显示预测点详细卡片", + "typhoonLabelPressure": "中心气压", + "aedOpenRemark": "开放时间备注", + "onboardingPermsBody": "为了在灾害发生的第一时间通知你,请授权以下权限。你可以随时在系统设置中更改。", + "typhoonOverlaySectionWeather": "天气底图", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "仅接收当前位置", "mapNavRain": "雨量", - "mapNavDisaster": "防灾", - "displayTheme": "主题", + "moonDays": "天", + "mapLegendUnit": "单位:{unit}", + "weatherModeClear": "晴天", + "meshtasticRadio": "电台", + "commonEmpty": "暂无内容", + "mapLayerSatelliteB01": "ひまわり 可见光-蓝(B01)", + "meshtasticExternalPower": "外部供电", + "moonPhaseLastQuarter": "下弦月", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "升序", + "reportFilterApply": "应用", + "reportDetailImageUnavailable": "报告图尚未提供", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "最高", + "reportDetailReplay": "重播", + "mapLayerRestroom": "公厕", + "restroomCategoryWelfare": "社福机构、集会场所", + "restroomGradeExcellent": "特优级", + "meshtasticLastSent": "最近送出", + "meshtasticName": "名称", + "meshtasticScan": "掃描", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "数值预报", + "meshtasticChannelFailed": "无法设定 DPIP 频道", "themeSystem": "跟随系统", - "themeLight": "浅色", - "themeDark": "深色", - "moreSectionAbout": "关于", - "termsOfService": "服务条款", - "faq": "常见问题", - "openSourceLicenses": "开源许可", - "sponsorTitle": "支持 DPIP", - "sponsorIntro": "DPIP 致力于提供实时防灾信息,没有广告或其他盈利模式。您的支持能帮助我们维持服务器运行并持续开发。", - "sponsorSubscriptions": "订阅制", - "sponsorRecommended": "推荐", - "sponsorOneTime": "单次支持", - "sponsorPerMonth": "{price} / 月", - "sponsorRestore": "恢复购买", - "sponsorTerms": "使用条款", - "sponsorPrivacy": "隐私政策", - "sponsorRestoring": "正在恢复购买…", - "sponsorRestoreUnavailable": "无法连接到商店,请稍后再试", - "commonClose": "关闭", + "mapLayerSatelliteNdvi": "ひまわり 植被指数", + "typhoonLegendForecast": "预测路径", + "typhoonValueHpa": "{n} 百帕", + "weatherPrecipitation": "降水量", + "moonNextFullMoon": "下次滿月", + "dpmSheetEmpty": "点击地图上的标记查看详情", + "onboardingSkipLeave": "仍要跳过", + "onboardingBack": "上一步", + "aedPlaceDesc": "放置位置说明", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "尚未完成授权", + "restroomTypeFamily": "亲子厕所", + "typhoonValueKm": "{n} 公里", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "气压", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "电池优化白名单", + "typhoonLabelNw": "西北侧", + "dpmDisasterFlood": "水灾", + "moonPhaseWaxingCrescent": "眉月", + "restroomCategoryLeisure": "休闲娱乐场所", "mapLayerTemperature": "温度", - "trendRange24h": "24 小时", - "trendRange7d": "7 天", - "trendNoData": "没有趋势数据", - "trendCumulativeTotal": "累计 {total} mm", - "chartHourLabel": "{hour}时", - "mapLayerHumidity": "湿度", - "mapLayerPressure": "气压", + "aedCategory": "场所分类", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "频道", + "monitorWaiting": "等待数据…", + "typhoonOverlayForecastCallouts": "预测点信息", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "震中坐标", + "meshtasticVoltage": "电压", + "mapLayerMeshtasticSubtitle": "电台听到过的 LoRa 网状网路节点", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "风向", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "雨量", - "rainIntervalMenu": "累积时段", - "rainIntervalNow": "今日", - "rainInterval10m": "10 分", - "rainInterval1h": "1 时", - "rainInterval3h": "3 时", - "rainInterval6h": "6 时", + "reportDetailMagnitude": "地震规模", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "各地震度", "rainInterval12h": "12 时", - "rainInterval24h": "24 时", - "rainInterval2d": "2 日", - "rainInterval3d": "3 日", - "mapLayerTyphoon": "台风", - "typhoonNoActive": "目前无台风", - "typhoonWind": "风速", - "typhoonGust": "阵风", - "typhoonPressure": "气压", - "typhoonMotion": "移动", - "mapLayerMonitor": "强震监视器", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "防灾地图", - "disasterMapOverlayMenuTooltip": "防灾地图图层", - "disasterMapOverlaySectionLayers": "图层", - "disasterMapOverlayAedTooltip": "显示 AED 位置", - "aedAddress": "地址", - "aedRegion": "县市区域", - "aedCategory": "场所分类", - "aedType": "场所类型", - "aedPlaceDesc": "放置位置说明", - "aedDescription": "备注", - "aedHoursWeekday": "平日开放时间", - "aedHoursSaturday": "周六开放时间", - "aedHoursSunday": "周日开放时间", - "aedOpenRemark": "开放时间备注", - "aedEmergencyPhone": "紧急联络电话", - "mapLayerRestroom": "公厕", - "mapLayerShelter": "避难收容场所", - "disasterMapOverlayRestroomTooltip": "显示公厕", - "disasterMapOverlayShelterTooltip": "显示避难收容场所", - "dpmOpenInMaps": "打开地图", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "土石流", + "notifyMonitor": "强震监视器", + "onboardingStart": "开始使用", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / 月", + "mapLayerPressure": "气压", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "ひまわり 近红外(B04)", + "mapLayerSatelliteTransparentZero": "零差值 = 透明(无信号)", + "shelterIndoorLabel": "室内收容", + "notifyOptOff": "关闭", + "reportFilterSortTime": "时间", + "mapLayerSatelliteCloudProbablyClear": "可能晴空", + "weatherModeThunderstorm": "雷雨", + "homeViewOnMap": "前往地图察看", + "reportFilterIntensityInfoLegacyTitle": "旧制(2020 以前)", + "typhoonLabelSpeed": "过去移动时速", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "无法打开 {app}", + "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "已接收", + "weatherRankingExtremeLow": "今日最低", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "ひまわり 低层水气(B10)", + "mapLayerSatelliteCloudProbablyCloudy": "可能有云", + "shelterCategoryLabel": "适用灾害", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(无水体)", + "meshtasticStateConnecting": "連線中…", + "moonTitle": "月亮", + "weatherRankingGust": "阵风", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "避难所灾害类型", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "服务器状态", + "notifySectionWeather": "天气", + "meshtasticPreset": "调变预设", + "dataSectionSeismic": "地震", + "changelogBodyEmpty": "此版本没有说明。", + "radarGlobalOutline": "国界", + "notifyEew": "紧急地震预警", + "regionNationwide": "全国", + "moreNotifyLog": "DPIP 通知发送记录", + "regionCurrent": "当前位置", + "dpmFilterSectionRestroom": "场所类型", + "meshtasticNotConnected": "尚未连线至装置", + "weatherModeSnow": "下雪", + "mapLayerMeshtastic": "Meshtastic 节点", + "moreDeveloper": "调试信息", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "ひまわり 长波红外线(B14)", + "meshtasticChannelUse": "频道使用率", + "mapNavLightning": "闪电", + "homeForecastEmpty": "目前没有预报数据", + "sponsorOneTime": "单次支持", + "mapLayerSatelliteBtdSplit": "ひまわり 分割视窗", + "onboardingPermBackground": "后台定位", + "aedEmergencyPhone": "紧急联络电话", + "dpmOpenInMaps": "打开地图", + "meshtasticNotifyNodes": "新节点通知", + "onboardingPermCriticalDesc": "让危及生命的地震预警,即使在静音或勿扰模式下也能发出声响。", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,显示底图", + "meshtasticSent": "已送出", + "homeForecastTitle": "24小时预报", + "typhoonLegendWarningAreas": "警报区域", + "meshtasticExcludeMqttHidden": "已隐藏 {count} 个", + "notifyOptLocalIntensity1": "本地震度1以上", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "历史", + "restroomTypeFemale": "女厕所", + "reportListToday": "今天", + "meshtasticTapNode": "点选节点查看详细信息", + "commonLoading": "加载中…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "中度台风", + "typhoonWind": "风速", + "mapLayerSatelliteAsh": "ひまわり 火山灰", + "rainInterval3h": "3 时", + "reportListSearch": "查询", + "meshtasticChannelReady": "DPIP 频道已就绪", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "卫星", + "reportFilterLocation": "地点", + "mapLayerSatelliteNightmicrophysics": "ひまわり 夜间微物理", + "typhoonIntensityTd": "热带性低气压", + "reportFilterDate": "日期", + "sponsorRestoreUnavailable": "无法连接到商店,请稍后再试", + "homeForecastPop": "{pop}%", + "regionEmpty": "尚未添加常用地区", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "允许 DPIP 在后台持续运行,避免预警延迟或漏收。", + "mapNavDisaster": "防灾", + "radarScanRangeSubtitle": "标示四座雷达实际观测到的范围。", + "aedHoursSunday": "周日开放时间", + "reportDetailOriginTime": "发震时间", + "trendNoData": "没有趋势数据", + "onboardingPermLocation": "定位", + "moreDiscord": "Discord 社区", + "mapNavPressure": "气压", + "mapLayerSatelliteB13": "ひまわり 红外线(B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "目前没有更新日志", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "开始日:当日 00:00(台北时间)", + "eewTitle": "地震预警", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "zh_Hans", + "regionSelectCount": "已选 {count}/{max}", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/云相", + "meshtasticStateError": "錯誤", + "weatherModeOvercast": "阴天", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "震源深度", + "typhoonOverlayWarningTooltip": "标示发布台风警报的县市", + "reportFilterDatePick": "选择日期", + "onboardingSkipStay": "返回授权", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "无法获取数据,请稍后重试", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "室外收容", + "meshtasticStateConnected": "已連線", + "mapNavRadar": "雷达", + "mapLayerSatelliteCloudClear": "晴空", + "eewSummary": "震级 {magnitude}·深度 {depth} 公里", + "locationBannerPermission": "尚未授予定位权限,无法向你所在的区域推送本地预警。", + "typhoonOverlayWeatherNoneTooltip": "不显示雷达或红外线底图", + "radarCountyOutlineHint": "画在回波之上", + "windForecastCountyOutlineHint": "绘制于风场之上", + "homeRainTrendTitle": "近 1 小时降水趋势", + "moonPhaseFirstQuarter": "上弦月", + "mapLayerCategoryTyphoon": "台风", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "空中工时(24 小时)", + "restroomTypeMixed": "混合厕所", + "restroomGradeGood": "优等级", + "notifyTsunami": "海啸信息", + "navData": "资料", + "mapLayerSatelliteBtdWvirw": "ひまわり 过冲云顶", + "meshtasticReadingAge": "数值时间", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "此设备无法拨打电话", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "不限", + "weatherRankingMergeTo": "合并至", + "notifyIntensity": "震度速报", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "累积时段", + "reportDetailLocalFelt": "小区域有感地震", + "meshtasticDevice": "设备", + "onboardingGrant": "授权", + "weatherModeRain": "雨天", + "shelterVulnerableOkLabel": "适合避难弱者安置", + "stationSheetEmpty": "点选任一测站查看观测值", + "typhoonLegendProbability": "侵袭概率", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "规模", + "skyTimeMorning": "上午", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "实验性功能", + "onboardingTermsBody": "使用 DPIP 前,请详细阅读以下注意事项:\n\n• 任何信息均应以中央气象署(CWA)发布的内容为准。\n\n• 受网络状态、服务器状态、应用程序状态、上游数据来源状态等因素影响,存在收不到信息的可能,我们会尽力避免此类情况,但不保证一定不会发生。\n\n• 强烈震动有可能比通知更早抵达您所在的位置。\n\n• 地震预警为快速计算的结果,可能存在较大误差,请理解并谨慎使用。\n\n• 任何未获官方认可的行为均可能承担法律风险,请务必遵守相关规定。\n\n此外,为提供本地化预警,本服务会在前台及后台收集并上传您的大致位置与设备推送标识符,仅用于决定应向您推送哪些预警。\n\n点击下方“同意并继续”即表示您已阅读、理解并同意上述事项。", + "reportFilterTitle": "筛选", + "onboardingPermCritical": "重要警告", + "trendCumulativeTotal": "累计 {total} mm", + "languageName": "简体中文", + "reportListEmptyFiltered": "没有符合条件的地震报告", + "meshtasticExcludeMqtt": "隐藏 MQTT 节点", + "mapNavTyphoon": "台风", + "weatherModeSand": "沙尘", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "卫星云图", + "@dpmOpenInMaps": {}, + "notifyReport": "地震报告", + "mapAppCoordinatesCopied": "已复制坐标", + "skyTimeNight": "夜晚", + "sponsorRecommended": "推荐", + "mapLayerSatelliteB15": "ひまわり 长波红外线(B15)", + "weatherRankingWind": "风速", + "feedStale": "数据可能已过期", + "homeForecastWind": "{direction} · {level} 级", + "navHome": "主页", + "meshtasticRegionLabel": "地区", + "mapLayerSatelliteCloudtop": "ひまわり 云顶温度", + "moonTimelineCaption": "月相", + "reportListMeta": "M{magnitude} · {depth} 公里", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "开源许可", + "weatherRankingLowest": "最低", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "深度", + "mapTimelineDataTime": "资料时间 {time}", + "radarScanRange": "显示扫描范围", + "meshtasticHopLimit": "跳数上限", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "温差 {value}°C", + "weatherRankingExtremeHigh": "今日最高", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "版本信息", + "sponsorPrivacy": "隐私政策", + "reportDetailLocalIntensity": "所在地的震度", + "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", + "meshtasticAirtime": "发射占空比", + "shelterCapacityValue": "{n} 人", + "lightningLegendCc": "云间 · {minutes} 分钟内", + "meshtasticSendHint": "要廣播的訊息", + "monitorDelay": "延迟 {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "否", + "mapLayerSatelliteB08": "ひまわり 上层水气(B08)", + "meshtasticReconnecting": "重新连线中…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "让乡镇界线在雷达回波下仍然清楚。", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "最接近台风报文时间的红外线", + "radarScanRangeHint": "框外空白代表未观测", + "typhoonPickerTd": "热带性低气压 TD {no}", + "mapLayerSatelliteWatervapor": "ひまわり 水气", + "regionAddButton": "添加地区", + "displaySettings": "显示设置", + "restroomGradePoor": "不合格", + "restroomCategoryTourist": "观光地区及风景区", + "locationBannerServiceOff": "定位服务已关闭,无法向你所在的区域推送本地预警。", + "mapLayerStyleTooltip": "显示样式", + "lightningLegendCg": "对地 · {minutes} 分钟内", + "skyTimeAuto": "自动", + "appLogs": "应用日志", + "feedConnecting": "连接中…", + "notifyBannerDisabled": "通知已关闭,将收不到灾害警报。", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "湿度", + "typhoonValueMs": "每秒 {n} 公尺", + "homeForecastHumidity": "湿度 {value}%", + "meshtasticBusyBody": "请先在另一个 Meshtastic App 中断线。两个 App 同时连同一台设备会互相抢走讯息,导致部分讯息遗失。", + "meshtasticChannelNoSlot": "没有可用的频道空位 — 请先在设备上空出一个", + "restroomCategoryTransport": "交通", + "reportFilterLocationHint": "例如:花莲、东部海域", + "moonSubtitle": "月相與亮度 — 完全本地計算", + "meshtasticBattery": "电量", + "meshtasticDistance": "距离", + "meshtasticSnrTrend": "信号趋势 (SNR)", + "meshtasticBatteryTrend": "电量趋势", + "typhoonOverlayMenuTooltip": "台风图层选项", + "mapLayerSatelliteBtdOzone": "ひまわり 对流层顶", + "meshtasticRegionMismatch": "设备地区为 {region} — DPIP 需要 TW", + "notifySectionEarthquake": "地震", + "mapLayerDisasterMap": "防灾地图", + "weatherModeFog": "大雾", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "气象厅灰度惯例:温度越低越白", + "moreAnnouncements": "公告", + "mapLayerSatelliteTransparentNoData": "无资料(陆地) = 透明", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "民众洽公场所", + "typhoonLegendCurrent": "目前中心", + "aedAddress": "地址", + "mapLayerAed": "AED", + "changelogTypePrerelease": "公测", + "reportFilterIntensityInfoModernBody": "震度为 0–4、5弱、5强、6弱、6强、7。筛选滑杆依新制;列表中较早的地震会以旧制标示显示。", + "typhoonOverlayWeatherNone": "无", + "mapLayerStyleGray": "灰度(JMA)", + "weatherModeAuto": "自动", + "typhoonLabelProbCircle": "70%概率圆", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "接收全部", + "displayTheme": "主题", + "mapLayerSatelliteB07": "ひまわり 短波红外(B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "过去移动方向", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "常用地区", + "typhoonLegendCone": "预测圆锥", + "moreCwaEew": "中央气象署地震预警", + "onboardingPermsTitle": "权限授权", + "mapLayerStyleJma": "云顶强调(JMA)", + "rainInterval10m": "10 分", + "weatherRankingAnalysisLow": "最低 {value}", + "meshtasticConnectAnyway": "仍要连线", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "ひまわり 近红外(B06)", + "mapLayerSatelliteTransparentReflectance": "低反射率/夜间 = 透明,显示底图", + "chartHourLabel": "{hour}时", + "mapLayerShelter": "避难收容场所", + "typhoonOverlayProbabilityTooltip": "显示侵袭概率(隐藏预测圆锥)", + "mapLayerSatelliteNdwi": "ひまわり 水体指数", + "disasterMapOverlayShelterTooltip": "显示避难收容场所", + "mapNavHumidity": "湿度", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "依震度排序", + "homeRainTrendNoData": "无资料", + "mapLayerCategoryRadar": "雷达", + "meshtasticShortName": "简称", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "ひまわり 气团", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "路径详情", + "dataSectionWeather": "气象", + "aedHoursWeekday": "平日开放时间", + "homeActiveEventsTitle": "生效中事件", + "weatherRankingAnalysisHigh": "最高 {value}", + "faq": "常见问题", + "typhoonHistoryLive": "实时", + "eewSerial": "第 {serial} 报", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "排序方式", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "要将这台设备切换为 TW 地区吗?设备会重新启动并短暂断线,上面的其他频道也会一起改变。", + "dataEarthquakeSubtitle": "地震报告", + "typhoonNoActive": "目前无台风", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "ひまわり 二氧化硫/云相(B11)", + "navEvents": "事件", + "onboardingTermsTitle": "服务条款", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "乡镇名称", + "notifySetFailed": "设置失败,请稍后再试。", + "meshtasticDisconnect": "斷線", + "meshtasticUndecoded": "无法解密", + "notifyAnnouncement": "公告", + "onboardingIntroTitle": "欢迎使用 DPIP", + "regionCurrentUnavailable": "无法获取所在地位置信息", + "languageSystem": "系统默认", + "skyTimeSunset": "日落", + "mapLayerSatelliteDust": "ひまわり 沙尘", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "修改", + "weatherDynamicState": "天气动画", + "mapPlaceholderDisabled": "地图(暂时禁用)", + "moonNow": "现在", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "外观", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "月出月落", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "接下来", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "月历", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "距离", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "公里", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "视直径", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "月出", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "月落", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "下次新月", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "整日在地平线上", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "当日无", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "太阳", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "日出日落、曙暮光与节气", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "日照", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "曙暮光", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "光线", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "日晷", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "节气", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "日出", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "日落", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "正午", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "白昼长度", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "民用", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "航海", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "天文", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "晨间黄金时刻", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "昏间黄金时刻", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "蓝调时刻", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "均时差", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "分", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "下一个节气", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "行星", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "今晚在哪、有多亮", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "此刻", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "地平线上", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "地平线下", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "太近太阳", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "亮度", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "距日距角", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "时段", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "昏星", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "晨星", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "距离", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "天文单位", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "仰角", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "水星", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "金星", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "火星", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "木星", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "土星", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "天王星", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "海王星", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "春分", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "清明", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "谷雨", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "立夏", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "小满", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "芒种", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "夏至", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "小暑", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "大暑", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "立秋", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "处暑", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "白露", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "秋分", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "寒露", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "霜降", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "立冬", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "小雪", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "大雪", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "冬至", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "小寒", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "大寒", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "立春", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "雨水", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "惊蛰", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "今夜", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "现在看得到什麼、什麼时候", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "观测窗口", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "天文夜", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "整夜不全暗", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "暗窗", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "月亮整夜在天上", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "總暗时", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "月光", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "流星雨", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "輻射点不升起", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "颗/时", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "卫星过境", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "此刻可观测目標", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "象限儀座", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "天琴座", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "寶瓶座η", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "寶瓶座δ", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "英仙座", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "獵戶座", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "金牛座南", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "獅子座", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "雙子座", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "小熊座", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "疏散星团", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "球狀星团", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "螺旋星系", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "椭圆星系", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "不規则星系", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "行星狀星云", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "超新星遗迹", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "发射星云", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "反射星云", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "星群", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "历法", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "农历日期与未来的日月食", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "今日", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "西历", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "农历", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "岁次", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app}(默认)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "月大小", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "复制坐标", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "三十日", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "已复制坐标", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "二十九日", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "无法打开 {app}", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "闰", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "此设备无法拨打电话", - - "mapOverlaySectionReference": "参考图层", - "mapLayerCategoryEarthquake": "地震", - "mapLayerCategoryTyphoon": "台风", - "mapLayerCategoryWeather": "气象观测", - "mapLayerCategorySatellite": "卫星", - "mapLayerCategoryRadar": "雷达", - "mapLayerCategoryLife": "生活", - "mapLayerCategoryForecast": "数值预报", "mapOverlaySectionMap": "地图", - "rainIntervalSection": "统计时间", - - "mapTownLabels": "乡镇名称", - "mapTownLabelsHint": "放大时显示乡镇名称", - - "mapTerrainRelief": "地形立体感", - "mapTerrainReliefHint": "在底图上显示立体地形阴影", - - "dpmSheetEmpty": "点击地图上的标记查看详情", - "dpmAddress": "地址", - "restroomTypeLabel": "厕所类型", - "restroomCategoryLabel": "类别", - "restroomGradeLabel": "等级", - "restroomTypeFemale": "女厕所", - "restroomTypeMale": "男厕所", - "restroomTypeMixed": "混合厕所", - "restroomTypeAccessible": "无障碍厕所", - "restroomTypeGenderNeutral": "性别友善厕所", - "restroomTypeFamily": "亲子厕所", - "restroomTypeUnspecified": "未设定", - "restroomCategoryTransport": "交通", - "restroomCategoryPark": "公园", - "restroomCategoryCommercial": "商业营业场所", - "restroomCategoryReligious": "宗教礼仪场所", - "restroomCategoryCultural": "文化育乐活动场所", - "restroomCategoryGovernment": "民众洽公场所", - "restroomCategoryWelfare": "社福机构、集会场所", - "restroomCategoryTourist": "观光地区及风景区", - "restroomCategoryLeisure": "休闲娱乐场所", - "restroomCategoryOther": "其他", - "restroomGradeExcellent": "特优级", - "restroomGradeGood": "优等级", - "restroomGradeAverage": "普通级", - "restroomGradePoor": "不合格", - "shelterAddressLabel": "地址", - "shelterCapacityLabel": "收容人数", - "shelterCapacityValue": "{n} 人", - "shelterCategoryLabel": "适用灾害", - "shelterIndoorLabel": "室内收容", - "shelterOutdoorLabel": "室外收容", - "shelterVulnerableOkLabel": "适合避难弱者安置", - "dpmYes": "是", - "dpmNo": "否", - "stationSheetEmpty": "点选任一测站查看观测值", - "monitorDelay": "延迟 {value} s", - "monitorWaiting": "等待数据…", - "mapLegendUnit": "单位:{unit}", - "typhoonLegendPast": "实际路径", - "typhoonLegendForecast": "预测路径", - "typhoonLegendForecastPoint": "预测点", - "typhoonLegendCurrent": "目前中心", - "typhoonLegendCone": "预测圆锥", - "mapLegendExpand": "图例", - "mapLegendCollapse": "收起图例", - "mapMyLocation": "我的位置", - "mapResetNorth": "回到正北", - "typhoonLegendCircle15": "七级风暴风圈", - "typhoonLegendCircle25": "十级风暴风圈", - "typhoonLegendProbability": "侵袭概率", - "typhoonLegendWarningAreas": "警报区域", - "typhoonWarningTitle": "台风警报", - "typhoonWarningAreas": "警戒区域:{areas}", - "typhoonTrackDetail": "路径详情", - "typhoonHistoryTitle": "资料时间", - "typhoonHistoryLive": "实时", - "typhoonSatelliteTitle": "卫星云图", - "typhoonDataTime": "资料时间\n{time}", - "typhoonForecastLead": "预测 +{hours} 小时", - "typhoonIntensityIntense": "强烈台风", - "typhoonIntensityMild": "轻度台风", - "typhoonIntensityModerate": "中度台风", - "typhoonIntensityTd": "热带性低气压", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "热带性低气压 TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "过去移动方向", - "typhoonLabelGaleAvg": "七级风平均暴风半径", - "typhoonLabelGust": "瞬间最大阵风", - "typhoonLabelNe": "东北侧", - "typhoonLabelNw": "西北侧", - "typhoonLabelPosition": "中心位置", - "typhoonLabelPressure": "中心气压", - "typhoonLabelProbCircle": "70%概率圆", - "typhoonLabelSe": "东南侧", - "typhoonLabelSpeed": "过去移动时速", - "typhoonLabelStormAvg": "十级风平均暴风半径", - "typhoonLabelSw": "西南侧", - "typhoonLabelWind": "近中心最大风速", - "typhoonLegendCircleAvg": "平均圆", - "typhoonOverlayMenuTooltip": "台风图层选项", - "typhoonOverlayProbabilityHint": "会隐藏预测圆锥", - "typhoonOverlayProbabilityTooltip": "显示侵袭概率(隐藏预测圆锥)", - "typhoonOverlaySectionExtra": "叠加层", - "typhoonOverlaySectionStorm": "暴风圈", - "typhoonOverlaySectionWeather": "天气底图", - "typhoonOverlayStormBandSubtitle": "含平均圆", - "typhoonOverlayStormL10Tooltip": "十级风风场 + 平均圆(黄)", - "typhoonOverlayStormL7Tooltip": "七级风风场 + 平均圆(紫)", - "typhoonOverlayWarningTooltip": "标示发布台风警报的县市", - "typhoonOverlayWeatherHint": "对齐报文时间", - "typhoonOverlayWeatherNone": "无", - "typhoonOverlayWeatherNoneTooltip": "不显示雷达或红外线底图", - "typhoonOverlayWeatherRadarTooltip": "最接近台风报文时间的雷达回波", - "typhoonOverlayWeatherSatelliteTooltip": "最接近台风报文时间的红外线", - "typhoonStormRadii": "NE {ne} · SE {se} · SW {sw} · NW {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} 百帕", - "typhoonValueKm": "{n} 公里", - "typhoonValueLat": "北纬 {lat} 度", - "typhoonValueLon": "东经 {lon} 度", - "typhoonValueMs": "每秒 {n} 公尺", - "typhoonOverlayForecastCallouts": "预测点信息", - "typhoonOverlayForecastCalloutsTooltip": "放大时显示预测点详细卡片", - "dpmFilterSectionRestroom": "场所类型", - "dpmFilterSectionRestroomType": "厕所类型", - "dpmFilterSectionShelter": "避难所灾害类型", - "dpmDisasterFlood": "水灾", - "dpmDisasterEarthquake": "震灾", - "dpmDisasterLandslide": "土石流", - "dpmDisasterTsunami": "海啸", - "dpmDisasterSlope": "坡地灾害", - "dpmDisasterNuclear": "核子事故", - "skyTime": "天空时间", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "月食", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "自动", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "日食", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "黎明", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "范围內無", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "日出", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "全食", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "上午", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "偏食", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "正午", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "环食", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "下午", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "半影食", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "黄金时刻", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "鼠", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "日落", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "牛", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "暮色", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "虎", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "夜晚", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "兔", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "多云", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "龙", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "阴天", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "蛇", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "下雪", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "马", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "沙尘", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "羊", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "显示扫描范围", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "猴", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "标示四座雷达实际观测到的范围。", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "鸡", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "框外空白代表未观测", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "狗", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "雷达图层选项", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "猪", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "县市界线", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "潮汐", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "国界", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "大潮、小潮与月球引力", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "各国国界外框", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "仅为天文引潮力,非港口潮汐表。水位請参考气象署公布之潮汐预报。", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "画在回波之上", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "此刻", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "让县市界线在雷达回波下仍然清楚。", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "周期", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "乡镇界线", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "大潮", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "较细的分区", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "小潮", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "让乡镇界线在雷达回波下仍然清楚。", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "中潮", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "定量降水预报图层选项", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "月球引力", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "风场预报图层选项", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "平衡潮高", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "绘制于风场之上", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "公尺", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "各国国界外框", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "下次近地点大潮", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "更细的网格", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "转折点", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "第 {serial} 报", - "eewMaxIntensity": "最大震度", - "eewLocalIntensity": "所在地预估", - "eewSWave": "震波", - "eewArrived": "已抵达", - "eewCountdown": "{seconds} 秒" + "tideHigh": "高", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "低", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "星图", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "头頂上肉眼可见的天空", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "北", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "东", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "南", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "西", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "轨道数据 {days} 天前", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}{month} 月 {day} 日", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "目前无流星雨", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "48 小时内无可见过境", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "无法读取轨道数据", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "无足够高度的目标", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "无法读取星表", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_zh_Hant_HK.arb b/lib/l10n/app_zh_Hant_HK.arb index 6aca268c8..78af4ffaf 100644 --- a/lib/l10n/app_zh_Hant_HK.arb +++ b/lib/l10n/app_zh_Hant_HK.arb @@ -1,679 +1,1739 @@ { - "@@locale": "zh_Hant_HK", - "languageName": "繁體中文(香港)", - "navHome": "主頁", - "navEvents": "事件", - "navMap": "地圖", - "navData": "資料", - "navEarthquake": "地震", - "dataSectionSeismic": "地震", - "dataEarthquakeSubtitle": "地震報告", - "dataSectionWeather": "氣象", - "dataWeatherRankingSubtitle": "即時觀測排行", - "weatherRankingTitle": "觀測排行", - "weatherRankingMeta": "資料時間:{time}\n共 {count} 觀測點", - "weatherRankingEmpty": "目前沒有可排序的觀測", - "weatherRankingBy": "依", - "weatherRankingHighest": "最高", - "weatherRankingLowest": "最低", - "weatherRankingMergeTo": "合併至", - "weatherRankingMergeTown": "鄉鎮", - "weatherRankingMergeCounty": "縣市", - "weatherRankingWind": "風速", - "weatherRankingGust": "陣風", + "typhoonValueLat": "北緯 {lat} 度", + "onboardingSkipBody": "未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 時", + "homeRainTrendHeavyStopping": "預計 {minutes} 分鐘後停止下大雨", + "mapTimelineObserved": "觀測", + "regionSelectTitle": "選擇地區", + "skyTimeNoon": "正午", + "radarCountyOutlineSubtitle": "讓縣市界線在雷達回波下仍然清楚。", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "廁所類型", + "mapLayerSatelliteB03": "ひまわり 可見光-紅(B03)", + "reportFilterIntensity": "震度", + "mapLayerLightning": "閃電", + "restroomTypeMale": "男廁所", + "meshtasticLastReceived": "最近接收", + "reportDetailSortByCounty": "依縣市排序", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "可能會有零星降雨", + "meshtasticUptime": "運行時間", "weatherRankingTempExtremes": "溫度極值", - "weatherRankingExtremeHigh": "今日最高", - "weatherRankingExtremeLow": "今日最低", + "themeLight": "淺色", + "mapTerrainReliefHint": "在底圖上顯示立體地形陰影", + "meshtasticEmptyMessage": "(空白訊息)", + "moreSectionRegion": "地區", + "dpmDisasterEarthquake": "震災", + "mapLayerSatellite": "ひまわり 紅外線(B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "週六開放時間", + "dpmDisasterSlope": "坡地災害", + "moonPhaseNew": "新月", + "notifySectionEew": "地震速報", + "mapResetNorth": "回到北方", + "rainInterval2d": "2 日", + "mapTownLabelsHint": "放大時顯示鄉鎮名稱", + "commonCancel": "取消", + "notifyOptTsunamiWarning": "只接收海嘯警報", + "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "進階", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "日溫差", + "notifySettingsMenu": "通知設定", + "typhoonHistoryTitle": "資料時間", + "mapAppDefault": "{app}(預設)", + "trendRange24h": "24 小時", + "mapLayerStyleJmaTooltip": "灰階為底,−40 °C 以下上色,凸顯雲頂高度", "weatherRankingRecordedAt": "記錄於 {time}", - "weatherRankingAnalysisCurrent": "當下 {value}°C", - "weatherRankingAnalysisHigh": "最高 {value}", - "weatherRankingAnalysisLow": "最低 {value}", - "weatherRankingAnalysisRange": "溫差 {value}°C", - "reportListEmpty": "目前沒有地震報告", - "reportListEmptyFiltered": "沒有符合條件的地震報告", - "reportListMeta": "M{magnitude} · {depth} 公里", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "公里", - "reportListLocalFelt": "小區域有感", - "reportListToday": "今天", - "reportListYesterday": "昨天", - "reportListDayCount": "{count}", - "reportListEnd": "已到最後一頁", - "reportFilterTitle": "篩選", - "reportFilterSort": "排序方式", - "reportFilterSortTime": "時間", - "reportFilterSortIntensity": "震度", - "reportFilterSortMagnitude": "規模", - "reportFilterSortDepth": "深度", + "mapLayerRain": "雨量", + "mapLayerQpesums": "未來 1 小時降水預報", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "地圖", + "mapTerrainRelief": "地形立體感", + "eewMaxIntensity": "最大震度", + "mapLegendCollapse": "收合圖例", + "changelogTitle": "更新日誌", "reportFilterOrderDesc": "降序", - "reportFilterOrderAsc": "升序", - "reportFilterIntensity": "震度", + "meshtasticExcludeMqttSubtitle": "經網際網路橋接、並非無線電聽到的節點", "reportFilterIntensityInfoTitle": "震度新制與舊制", - "reportFilterIntensityInfoIntro": "中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。", - "reportFilterIntensityInfoLegacyTitle": "舊制(2020 以前)", - "reportFilterIntensityInfoLegacyBody": "震度僅 0–7,沒有 5弱/5強/6弱/6強。", - "reportFilterIntensityInfoModernTitle": "新制(2020 起)", - "reportFilterIntensityInfoModernBody": "震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。", - "reportFilterMagnitude": "規模", - "reportFilterDepth": "深度", - "reportFilterDepthKm": "{depth} 公里", - "reportFilterDate": "日期", - "reportFilterDatePick": "選擇日期", - "reportFilterDateStartNote": "開始日:當日 00:00(台北時間)", + "mapLayerTyphoon": "颱風", + "radarOverlayMenuTooltip": "雷達圖層選項", + "mapMyLocation": "我的位置", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "節點", + "meshtasticSend": "傳送", + "typhoonOverlayStormL7Tooltip": "七級暴風圈+平均圓(紫色)", + "aedType": "場所類型", + "termsOfService": "服務條款", + "typhoonLegendCircle25": "十級風暴風圈", + "sponsorTitle": "支持 DPIP", + "mapNavSatellite": "衛星", + "homeRainTrendUpdated": "更新 {time}", + "onboardingNext": "下一步", + "weatherRankingMergeTown": "鄉鎮", + "mapLayerMonitor": "強震監視器", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "訂閱制", + "typhoonValueLon": "東經 {lon} 度", + "skyTime": "天空時間", + "weatherModeCloudy": "多雲", + "skyTimeDusk": "暮色", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "韌體", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "結束日:當日 24:00(台北時間)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "地點", - "reportFilterLocationHint": "例如:花蓮、東部海域", - "reportFilterAny": "不限", - "reportFilterApply": "套用", - "reportFilterReset": "重設", - "reportListSearch": "查詢", - "reportDetailTitle": "地震報告", - "reportDetailNumbered": "編號 {number} 顯著有感地震", - "reportDetailLocalFelt": "小區域有感地震", - "reportDetailInfo": "詳細資訊", - "reportDetailOriginTime": "發震時間", - "reportDetailEpicenter": "震央座標", - "reportDetailMagnitude": "地震規模", - "reportDetailDepth": "震源深度", - "reportDetailAreaIntensity": "各地震度", - "reportDetailLocalIntensity": "所在地的震度", - "reportDetailLocalIntensityUnavailable": "沒有震度訊息", - "reportDetailSortByIntensity": "依震度排序", - "reportDetailSortByCounty": "依縣市排序", - "reportDetailImage": "地震報告圖", - "reportDetailImageUnavailable": "報告圖尚未提供", - "reportDetailOpenReport": "報告頁面", - "reportDetailReplay": "重播", - "navMore": "更多", - "appLogs": "App 日誌", - "changelogTitle": "更新日誌", - "changelogEmpty": "目前沒有更新日誌", - "changelogTypePrerelease": "公測", - "changelogTypeStable": "正式", - "changelogCurrentVersion": "目前版本", - "changelogVersionDetails": "版本資訊", - "changelogBodyEmpty": "此版本沒有說明。", - "mapPlaceholderDisabled": "地圖(暫時停用)", - "moreSectionRegion": "地區", - "moreSectionNotify": "通知", - "moreSectionDisplay": "顯示", - "regionManageTitle": "常用地區", - "regionAddButton": "新增地區", - "regionEmpty": "尚未新增常用地區", - "regionSelectTitle": "選擇地區", - "regionSelectCount": "已選 {count}/{max}", - "regionSelectFull": "最多只能選擇 {max} 個地區", - "regionEdit": "修改", - "moreSectionAdvanced": "進階", - "moreDeveloper": "偵錯資訊", - "experimentalFeatures": "實驗性功能", - "moreSectionLinks": "相關連結", - "moreCwaEew": "中央氣象署強震即時警報", - "moreTremReport": "TREM 偵測報告", - "moreServerStatus": "伺服器狀態", - "moreAnnouncements": "公告", - "moreDiscord": "Discord 社群", - "moreNotifyLog": "DPIP 通知發送記錄", - "moreLinkOpenFailed": "無法開啟連結", - "weatherDynamicState": "天氣動態狀態", - "weatherDynamicStateSubtitle": "覆寫主頁背景天氣", - "weatherModeAuto": "自動", - "weatherModeClear": "晴天", - "weatherModeRain": "雨天", - "weatherModeFog": "大霧", - "weatherModeThunderstorm": "雷暴", - "commonLoading": "載入中…", - "commonRetry": "重試", - "commonError": "發生錯誤", - "commonFetchFailed": "無法獲取資料,請稍後重試", - "commonEmpty": "沒有資料", - "feedConnecting": "連接中…", - "feedStale": "資料可能已過期", - "feedOffline": "連接中斷", - "eewTitle": "地震速報", - "eewNone": "目前沒有地震速報", - "eewSummary": "規模 {magnitude}・深度 {depth} 公里", - "regionNationwide": "全國", - "regionCurrent": "所在地", - "regionCurrentUnavailable": "無法取得所在地位置資訊", - "weatherPrecipitation": "降水量", - "weatherHumidity": "濕度", - "weatherDataTime": "{station} ∙ 資料時間 {time}", - "homeViewOnMap": "前往地圖察看", - "homeForecastTitle": "24小時預報", + "meshtasticSilent": "已靜默", + "reportFilterSortMagnitude": "規模", + "mapLayerCategoryEarthquake": "地震", + "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", + "typhoonLegendPast": "實際路徑", + "restroomCategoryOther": "其他", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "高 {high}° · 低 {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "體感 {temp}°", - "homeForecastHumidity": "濕度 {value}%", - "homeForecastWind": "{direction} · {level} 級", - "homeForecastUnavailable": "選擇地區後可查看預報", - "homeForecastEmpty": "目前沒有預報資料", - "homeActiveEventsTitle": "生效中事件", - "homeActiveEventsEmpty": "目前沒有生效中的事件", - "homeRainTrendTitle": "近 1 小時降水趨勢", - "homeRainTrendMinute": "{minute}分", - "homeRainTrendUpdated": "更新 {time}", - "homeRainTrendNoData": "無資料", - - "homeRainTrendScattered": "可能會有零星降雨", - "homeRainTrendLightSustained": "未來 1 小時會有持續小雨", - "homeRainTrendLightStopping": "預計 {minutes} 分鐘後停止下小雨", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "開啟設定", + "mapLegendExpand": "圖例", + "eewNone": "目前沒有地震速報", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "海嘯消息、海嘯警報", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "節點選項", + "onboardingAgreeContinue": "同意並繼續", + "meshtasticNodeId": "節點 ID", + "commonRetry": "重試", + "reportDetailNumbered": "編號 {number} 顯著有感地震", + "typhoonOverlayStormBandSubtitle": "含平均圓", + "disasterMapOverlayRestroomTooltip": "顯示公廁", + "weatherRankingTitle": "觀測排行", "homeRainTrendHeavySustained": "未來 1 小時會有持續大雨", - "homeRainTrendHeavyStopping": "預計 {minutes} 分鐘後停止下大雨", - "mapLayers": "圖層", - "mapLayerOrderTitle": "調整圖層順序", - "mapLayerOrderReset": "回復預設順序", - "mapLayerRadar": "雷達合成回波圖", - "mapLayerSatellite": "ひまわり 紅外線(B13)", - "mapLayerSatelliteB01": "ひまわり 可見光-藍(B01)", - "mapLayerSatelliteB02": "ひまわり 可見光-綠(B02)", - "mapLayerSatelliteB03": "ひまわり 可見光-紅(B03)", - "mapLayerSatelliteB04": "ひまわり 近紅外(B04)", + "notifySectionTsunami": "海嘯", + "restroomCategoryPark": "公園", + "moreLinkOpenFailed": "無法開啟連結", + "themeDark": "深色", + "sponsorRestore": "恢復購買", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "正在設定 DPIP 頻道…", + "meshtasticRegionSwitch": "切換為 TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "流量", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階", + "disasterMapOverlayAedTooltip": "顯示 AED 位置", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "濕度", + "mapLayerSatelliteTransparentNight": "夜間 = 透明,顯示底圖", + "meshtasticScanning": "掃描中…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "最多只能選擇 {max} 個地區", + "meshtasticTitle": "Meshtastic", + "navMore": "更多", + "meshtasticDpipChannel": "DPIP 頻道", + "disasterMapOverlaySectionLayers": "圖層", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "ひまわり 近紅外(B05)", - "mapLayerSatelliteB06": "ひまわり 近紅外(B06)", - "mapLayerSatelliteB07": "ひまわり 短波紅外(B07)", - "mapLayerSatelliteB08": "ひまわり 上層水氣(B08)", - "mapLayerSatelliteB09": "ひまわり 中層水氣(B09)", - "mapLayerSatelliteB10": "ひまわり 低層水氣(B10)", - "mapLayerSatelliteB11": "ひまわり 二氧化硫/雲相(B11)", - "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", - "mapLayerSatelliteB13": "ひまわり 紅外線(B13)", - "mapLayerSatelliteB14": "ひまわり 長波紅外線(B14)", - "mapLayerSatelliteB15": "ひまわり 長波紅外線(B15)", - "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "typhoonStormRadii": "東北 {ne} · 東南 {se} · 西南 {sw} · 西北 {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "東北側", + "meshtasticCopied": "已複製訊息", + "reportListEmpty": "目前沒有地震報告", + "reportListEnd": "已到最後一頁", "mapLayerSatelliteTruecolor": "ひまわり 真彩色", - "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", - "mapLayerSatelliteAsh": "ひまわり 火山灰", - "mapLayerSatelliteDust": "ひまわり 沙塵", - "mapLayerSatelliteAirmass": "ひまわり 氣團", - "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", - "mapLayerSatelliteWatervapor": "ひまわり 水氣", - "mapLayerSatelliteBtdSplit": "ひまわり 分割視窗", - "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", - "mapLayerSatelliteBtdWvirw": "ひまわり 過衝雲頂", - "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/雲相", - "mapLayerSatelliteBtdCo2": "ひまわり 卷雲/雲高", - "mapLayerSatelliteBtdOzone": "ひまわり 對流層頂", - "mapLayerSatelliteCloudtop": "ひまわり 雲頂溫度", - "mapLayerSatelliteCloudmask": "ひまわり 雲遮罩", - "mapLayerSatelliteSst": "ひまわり 海表溫度", - "mapLayerSatelliteNdvi": "ひまわり 植生指數", - "mapLayerSatelliteNdwi": "ひまわり 水體指數", - "mapLayerSatelliteMndwi": "ひまわり 改良水體指數", + "typhoonOverlaySectionExtra": "覆蓋層", + "eewSWave": "震波", + "meshtasticBusyTitle": "另一個 App 正在使用這台裝置", + "restroomCategoryCultural": "文化育樂活動場所", + "typhoonLabelWind": "近中心最大風速", + "radarGlobalOutlineHint": "各國國界外框", + "notifyEvacuation": "防災資訊", + "typhoonLegendCircle15": "七級風暴風圈", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "天文", + "homeRainTrendLightSustained": "未來 1 小時會有持續小雨", + "commonError": "發生錯誤", + "moonPhaseWaningCrescent": "殘月", + "meshtasticPower": "電力", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "現在", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "報告頁面", + "trendRange7d": "7 天", + "typhoonWarningAreas": "警戒區域:{areas}", + "rainIntervalSection": "統計時間", + "notifyTitle": "通知", + "meshtasticTxPower": "發射功率", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "類別", + "sponsorRestoring": "正在恢復購買…", + "sponsorIntro": "DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。", + "shelterAddressLabel": "地址", + "typhoonLabelStormAvg": "十級風平均暴風半徑", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "商業營業場所", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "縣市區域", + "homeRainTrendLightStopping": "預計 {minutes} 分鐘後停止下小雨", + "reportDetailInfo": "詳細資訊", + "mapNavWind": "風向", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "風場預報圖層選項", + "dataWeatherRankingSubtitle": "即時觀測排行", + "rainInterval6h": "6 時", + "homeRainTrendMinute": "{minute}分", + "restroomTypeUnspecified": "未設定", + "typhoonOverlayProbabilityHint": "會隱藏預測圓錐", "mapLayerSatelliteGlobalOutline": "國界", - "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", - "mapLayerSatelliteCloudClear": "晴空", - "mapLayerSatelliteCloudProbablyClear": "可能晴空", - "mapLayerSatelliteCloudProbablyCloudy": "可能有雲", + "mapNavTemperature": "溫度", + "typhoonLegendForecastPoint": "預測點", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "昨天", + "moreSectionLinks": "相關連結", + "feedOffline": "連接中斷", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "顯示", + "rainInterval3d": "3 日", + "defaultMapLayerSubtitle": "開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。", + "aedDescription": "備註", + "typhoonOverlayWeatherRadarTooltip": "雷達回波(對齊颱風報文時間)", + "onboardingPermLocationDesc": "依你所在位置推送本地警報。", + "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "目前沒有生效中的事件", + "typhoonLabelPosition": "中心位置", + "weatherRankingBy": "依", + "typhoonIntensityMild": "輕度颱風", + "windForecastGlobalOutlineHint": "各國國界外框", + "rainInterval1h": "1 時", + "eewLocalIntensity": "所在地預估", + "mapLayerRadar": "雷達合成回波圖", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "宗教禮儀場所", + "meshtasticRole": "角色", "mapLayerSatelliteCloudCloudy": "有雲", - "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,顯示底圖", - "mapLayerSatelliteTransparentReflectance": "低反射率/夜間 = 透明,顯示底圖", - "mapLayerSatelliteTransparentZero": "零差值 = 透明(無訊號)", - "mapLayerSatelliteTransparentNight": "夜間 = 透明,顯示底圖", - "mapLayerSatelliteTransparentNoData": "無資料(陸地) = 透明", - "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(無植被)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(無水體)", - "mapLayerSatelliteTransparentClear": "晴空 = 透明,顯示底圖", + "skyTimeSunrise": "日出", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "尚無訊息", + "onboardingPermNotifyDesc": "在地震、天氣與災害發生時,即時傳遞警報通知。", + "radarTownOutline": "鄉鎮界線", "mapLayerStyleSection": "顯示樣式", - "mapLayerStyleTooltip": "顯示樣式", - "mapLayerStyleGray": "灰階(JMA)", - "mapLayerStyleGrayTooltip": "氣象廳灰階慣例:溫度越低越白", - "mapLayerStyleJma": "雲頂強調(JMA)", - "mapLayerStyleJmaTooltip": "灰階為底,−40 °C 以下上色,凸顯雲頂高度", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階", - "mapLayerQpesums": "未來 1 小時降水預報", - "mapLayerLightning": "閃電", - "lightningLegendCg": "對地 · {minutes} 分內", - "lightningLegendCc": "雲間 · {minutes} 分內", - "mapTimelineNow": "現在", - "mapTimelinePast": "歷史", - "mapTimelineFuture": "未來", - "mapTimelineObserved": "觀測", - "mapTimelineForecast": "預報", - "mapTimelineDataTime": "資料時間 {time}", - "notifySettingsMenu": "通知設定", - "notifyTitle": "通知", - "notifyUnavailable": "推送尚未就緒,請稍後再試。", - "notifySetFailed": "設定失敗,請稍後再試。", - "notifySectionEew": "地震速報", - "notifySectionEarthquake": "地震", - "notifySectionWeather": "天氣", - "notifySectionTsunami": "海嘯", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "防災地圖圖層", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "近期聽到", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "西南側", + "typhoonForecastLead": "預測 +{hours} 小時", + "dpmDisasterTsunami": "海嘯", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "正式", + "mapLayerSatelliteTransparentClear": "晴空 = 透明,顯示底圖", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "參考圖層", + "mapLayerSatelliteB02": "ひまわり 可見光-綠(B02)", + "reportListLocalFelt": "小區域有感", + "weatherRankingEmpty": "目前沒有可排序的觀測", "notifySectionOther": "其他", - "notifyEew": "緊急地震速報", - "notifyMonitor": "強震監視器", - "notifyReport": "地震報告", - "notifyIntensity": "震度速報", - "notifyThunderstorm": "雷暴即時訊息", - "notifyAdvisory": "天氣警告及特報", - "notifyEvacuation": "防災資訊", - "notifyTsunami": "海嘯資訊", - "notifyAnnouncement": "公告", - "notifyOptOff": "關閉", - "notifyOptAll": "接收全部", + "weatherRankingMeta": "資料時間:{time}\n共 {count} 觀測點", + "onboardingTermsAgree": "我已閱讀並同意服務條款", + "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(無植被)", "notifyOptLocalIntensity4": "所在地震度4以上", - "notifyOptLocalIntensity1": "所在地震度1以上", - "notifyOptWeatherLocal": "接收所在地", - "notifyOptTsunamiWarning": "只接收海嘯警報", - "notifyOptTsunamiAll": "海嘯消息、海嘯警報", - "onboardingNext": "下一步", - "onboardingBack": "上一步", + "eewArrived": "已抵達", + "meshtasticNoDevices": "找不到 Meshtastic 裝置", + "mapLayerCategoryLife": "生活", + "reportFilterSortIntensity": "震度", + "typhoonMotion": "移動", + "meshtasticStateDisconnected": "未連線", + "typhoonIntensityIntense": "強烈颱風", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "調整圖層順序", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "是", + "meshtasticNoHistory": "歷史紀錄還不夠", + "reportDetailLocalIntensityUnavailable": "沒有震度訊息", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "公里", + "reportFilterDepth": "深度", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "向下捲動以繼續", - "onboardingIntroTitle": "歡迎使用 DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "預報", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "地圖", + "notifyAdvisory": "天氣警告及特報", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "重設", + "mapLayerSatelliteMndwi": "ひまわり 改良水體指數", + "typhoonOverlaySectionStorm": "暴風圈", + "moonPhaseFull": "滿月", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "虧凸月", + "weatherDynamicStateSubtitle": "覆寫主頁背景天氣", + "reportFilterIntensityInfoModernTitle": "新制(2020 起)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "資料時間\n{time}", + "restroomTypeAccessible": "無障礙廁所", + "moreSectionAbout": "關於", + "meshtasticSelectDevice": "選擇裝置", "onboardingIntroBody": "DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷暴即時訊息、天氣警告及特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。", - "onboardingTermsTitle": "服務條款", - "onboardingTermsBody": "使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網絡狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機會比通知早抵達用戶所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供本地化警報,本服務會在前景及背景收集並上傳您的概略位置與裝置推送識別碼,僅用於決定應向您推送之警報。\n\n點按下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。", - "onboardingTermsAgree": "我已閱讀並同意服務條款", - "onboardingAgreeContinue": "同意並繼續", - "onboardingPermsTitle": "權限授權", - "onboardingPermsBody": "為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中更改。", + "shelterCapacityLabel": "收容人數", + "reportDetailImage": "地震報告圖", + "meshtasticStateConfiguring": "設定中…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "七級風平均暴風半徑", "onboardingPermNotify": "通知", - "onboardingPermNotifyDesc": "在地震、天氣與災害發生時,即時傳遞警報通知。", - "onboardingPermCritical": "重大通知", - "onboardingPermCriticalDesc": "讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。", - "onboardingPermLocation": "定位", - "onboardingPermLocationDesc": "依你所在位置推送本地警報。", - "onboardingPermBackground": "背景定位", - "onboardingPermBackgroundDesc": "選擇「一律允許」,關閉 App 也能推送本地警報。", - "onboardingPermBattery": "省電白名單", - "onboardingPermBatteryDesc": "允許 DPIP 在背景持續運作,避免警報延遲或漏收。", - "onboardingGrant": "授權", - "onboardingGranted": "已授權", - "onboardingStart": "開始使用", - "language": "語言", - "languageSettings": "語言設定", - "languageSystem": "系統預設", - "locationBannerServiceOff": "定位服務已關閉,無法針對你的所在地推送警報。", - "locationBannerPermission": "尚未授權定位,無法針對你的所在地推送警報。", - "locationBannerFix": "開啟設定", - "notifyBannerDisabled": "通知已關閉,將收不到災害警報。", - "onboardingSkipTitle": "尚未完成授權", - "onboardingSkipBody": "未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。", - "onboardingSkipStay": "返回授權", - "onboardingSkipLeave": "仍要略過", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "清除訊息", + "meshtasticNotifyMessages": "新訊息通知", + "defaultMapLayerSettings": "地圖預設圖層", + "moreSectionNotify": "通知", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "推送尚未就緒,請稍後再試。", + "mapLayerOrderReset": "回復預設順序", + "dpmAddress": "地址", + "weatherRankingMergeCounty": "縣市", + "moreSectionApp": "取得 App", + "reportFilterIntensityInfoLegacyBody": "震度僅 0–7,沒有 5弱/5強/6弱/6強。", + "mapLayerSatelliteSst": "ひまわり 海表溫度", + "qpesumsOverlayMenuTooltip": "定量降水預報圖層選項", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "未來", + "typhoonLegendCircleAvg": "平均圓", + "reportFilterDepthKm": "{depth} 公里", + "typhoonLabelSe": "東南側", + "radarTownOutlineHint": "較細的分區", + "eewCountdown": "{seconds} 秒", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "瞬間最大陣風", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "使用條款", + "restroomTypeGenderNeutral": "性別友善廁所", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "雷暴即時訊息", + "skyTimeGolden": "黃金時刻", + "moonAge": "月齡", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "當下 {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "選擇地區後可查看預報", + "mapLayers": "圖層", + "meshtasticHardware": "硬體", + "languageSettings": "語言設定", + "dpmDisasterNuclear": "核子事故", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "語言", + "homeForecastFeelsLike": "體感 {temp}°", + "typhoonOverlayWeatherHint": "對齊報文時間", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "黎明", + "skyTimeAfternoon": "下午", + "meshtasticLastHeard": "最後聽到", + "typhoonWarningTitle": "颱風警報", "moreSourceCode": "原始碼", - "moreSectionApp": "取得 App", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "顯示設定", - "defaultMapLayerSettings": "地圖預設圖層", - "defaultMapLayerSubtitle": "開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。", - "mapNavRadar": "雷達", - "mapNavQpesums": "預報", - "mapNavSatellite": "衛星", - "mapNavLightning": "閃電", - "mapNavTyphoon": "颱風", + "mapLayerCategoryWeather": "氣象觀測", + "mapLayerSatelliteB09": "ひまわり 中層水氣(B09)", + "windForecastTownOutlineHint": "更細的網格", + "mapLayerSatelliteCloudmask": "ひまわり 雲遮罩", + "mapAppCopyCoordinates": "複製座標", + "reportFilterIntensityInfoIntro": "中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。", "mapNavEarthquake": "地震", - "mapNavTemperature": "溫度", - "mapNavHumidity": "濕度", - "mapNavPressure": "氣壓", - "mapNavWind": "風向", + "typhoonGust": "陣風", + "restroomGradeAverage": "普通級", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "ひまわり 卷雲/雲高", + "onboardingPermBackgroundDesc": "選擇「一律允許」,關閉 App 也能推送本地警報。", + "mapTimelineForecast": "預報", + "restroomTypeLabel": "廁所類型", + "navEarthquake": "地震", + "typhoonOverlayStormL10Tooltip": "十級暴風圈+平均圓(黃色)", + "moonPhaseWaxingGibbous": "盈凸月", + "reportDetailTitle": "地震報告", + "moreTremReport": "TREM 偵測報告", + "weatherDataTime": "{station} ∙ 資料時間 {time}", + "meshtasticNoNodes": "尚未聽到任何節點", + "meshtasticViaMqtt": "經 MQTT(網際網路)", + "radarCountyOutline": "縣市界線", + "onboardingGranted": "已授權", + "@mapAppCopyCoordinates": {}, + "commonClose": "關閉", + "restroomGradeLabel": "等級", + "rainIntervalNow": "今日", + "changelogCurrentVersion": "目前版本", + "typhoonOverlayForecastCalloutsTooltip": "放大時顯示預測點詳細卡片", + "typhoonLabelPressure": "中心氣壓", + "aedOpenRemark": "開放時間備註", + "onboardingPermsBody": "為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中更改。", + "typhoonOverlaySectionWeather": "天氣底圖", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "接收所在地", "mapNavRain": "雨量", - "mapNavDisaster": "防災", - "displayTheme": "主題", + "moonDays": "天", + "mapLegendUnit": "單位:{unit}", + "weatherModeClear": "晴天", + "meshtasticRadio": "電台", + "commonEmpty": "沒有資料", + "mapLayerSatelliteB01": "ひまわり 可見光-藍(B01)", + "meshtasticExternalPower": "外部供電", + "moonPhaseLastQuarter": "下弦月", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "升序", + "reportFilterApply": "套用", + "reportDetailImageUnavailable": "報告圖尚未提供", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "最高", + "reportDetailReplay": "重播", + "mapLayerRestroom": "公廁", + "restroomCategoryWelfare": "社福機構、集會場所", + "restroomGradeExcellent": "特優級", + "meshtasticLastSent": "最近送出", + "meshtasticName": "名稱", + "meshtasticScan": "掃描", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "數值預報", + "meshtasticChannelFailed": "無法設定 DPIP 頻道", "themeSystem": "跟隨系統", - "themeLight": "淺色", - "themeDark": "深色", - "moreSectionAbout": "關於", - "termsOfService": "服務條款", - "faq": "常見問題", - "openSourceLicenses": "引用套件", - "sponsorTitle": "支持 DPIP", - "sponsorIntro": "DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。", - "sponsorSubscriptions": "訂閱制", - "sponsorRecommended": "推薦", - "sponsorOneTime": "單次支援", - "sponsorPerMonth": "{price} / 月", - "sponsorRestore": "恢復購買", - "sponsorTerms": "使用條款", - "sponsorPrivacy": "私隱權政策", - "sponsorRestoring": "正在恢復購買…", - "sponsorRestoreUnavailable": "無法連線至商店,請稍後再試", - "commonClose": "關閉", + "mapLayerSatelliteNdvi": "ひまわり 植生指數", + "typhoonLegendForecast": "預測路徑", + "typhoonValueHpa": "{n} 百帕", + "weatherPrecipitation": "降水量", + "moonNextFullMoon": "下次滿月", + "dpmSheetEmpty": "點選地圖上的標記查看詳情", + "onboardingSkipLeave": "仍要略過", + "onboardingBack": "上一步", + "aedPlaceDesc": "放置位置說明", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "尚未完成授權", + "restroomTypeFamily": "親子廁所", + "typhoonValueKm": "{n} 公里", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "氣壓", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "省電白名單", + "typhoonLabelNw": "西北側", + "dpmDisasterFlood": "水災", + "moonPhaseWaxingCrescent": "眉月", + "restroomCategoryLeisure": "休閒娛樂場所", "mapLayerTemperature": "溫度", - "trendRange24h": "24 小時", - "trendRange7d": "7 天", - "trendNoData": "沒有趨勢資料", - "trendCumulativeTotal": "累計 {total} mm", - "chartHourLabel": "{hour}時", - "mapLayerHumidity": "濕度", - "mapLayerPressure": "氣壓", + "aedCategory": "場所分類", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "頻道", + "monitorWaiting": "等待資料…", + "typhoonOverlayForecastCallouts": "預測點資訊", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "震央座標", + "meshtasticVoltage": "電壓", + "mapLayerMeshtasticSubtitle": "電台聽到過的 LoRa 網狀網路節點", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "風向", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "雨量", - "rainIntervalMenu": "累積時段", - "rainIntervalNow": "今日", - "rainInterval10m": "10 分", - "rainInterval1h": "1 時", - "rainInterval3h": "3 時", - "rainInterval6h": "6 時", + "reportDetailMagnitude": "地震規模", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "各地震度", "rainInterval12h": "12 時", - "rainInterval24h": "24 時", - "rainInterval2d": "2 日", - "rainInterval3d": "3 日", - "mapLayerTyphoon": "颱風", - "typhoonNoActive": "目前無颱風", - "typhoonWind": "風速", - "typhoonGust": "陣風", - "typhoonPressure": "氣壓", - "typhoonMotion": "移動", - "mapLayerMonitor": "強震監視器", - "mapLayerAed": "AED", - "mapLayerDisasterMap": "防災地圖", - "disasterMapOverlayMenuTooltip": "防災地圖圖層", - "disasterMapOverlaySectionLayers": "圖層", - "disasterMapOverlayAedTooltip": "顯示 AED 位置", - "aedAddress": "地址", - "aedRegion": "縣市區域", - "aedCategory": "場所分類", - "aedType": "場所類型", - "aedPlaceDesc": "放置位置說明", - "aedDescription": "備註", - "aedHoursWeekday": "平日開放時間", - "aedHoursSaturday": "週六開放時間", - "aedHoursSunday": "週日開放時間", - "aedOpenRemark": "開放時間備註", - "aedEmergencyPhone": "緊急聯絡電話", - "mapLayerRestroom": "公廁", - "mapLayerShelter": "避難收容場所", - "disasterMapOverlayRestroomTooltip": "顯示公廁", - "disasterMapOverlayShelterTooltip": "顯示避難收容場所", - "dpmOpenInMaps": "開啟地圖", - "@dpmOpenInMaps": { + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "土石流", + "notifyMonitor": "強震監視器", + "onboardingStart": "開始使用", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / 月", + "mapLayerPressure": "氣壓", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "ひまわり 近紅外(B04)", + "mapLayerSatelliteTransparentZero": "零差值 = 透明(無訊號)", + "shelterIndoorLabel": "室內收容", + "notifyOptOff": "關閉", + "reportFilterSortTime": "時間", + "mapLayerSatelliteCloudProbablyClear": "可能晴空", + "weatherModeThunderstorm": "雷暴", + "homeViewOnMap": "前往地圖察看", + "reportFilterIntensityInfoLegacyTitle": "舊制(2020 以前)", + "typhoonLabelSpeed": "過去移動時速", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "無法開啟 {app}", + "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "已接收", + "weatherRankingExtremeLow": "今日最低", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "ひまわり 低層水氣(B10)", + "mapLayerSatelliteCloudProbablyCloudy": "可能有雲", + "shelterCategoryLabel": "適用災害", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(無水體)", + "meshtasticStateConnecting": "連線中…", + "moonTitle": "月亮", + "weatherRankingGust": "陣風", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "避難所災害類型", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "伺服器狀態", + "notifySectionWeather": "天氣", + "meshtasticPreset": "調變預設", + "dataSectionSeismic": "地震", + "changelogBodyEmpty": "此版本沒有說明。", + "radarGlobalOutline": "國界", + "notifyEew": "緊急地震速報", + "regionNationwide": "全國", + "moreNotifyLog": "DPIP 通知發送記錄", + "regionCurrent": "所在地", + "dpmFilterSectionRestroom": "場所類型", + "meshtasticNotConnected": "尚未連線至裝置", + "weatherModeSnow": "下雪", + "mapLayerMeshtastic": "Meshtastic 節點", + "moreDeveloper": "偵錯資訊", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "ひまわり 長波紅外線(B14)", + "meshtasticChannelUse": "頻道使用率", + "mapNavLightning": "閃電", + "homeForecastEmpty": "目前沒有預報資料", + "sponsorOneTime": "單次支援", + "mapLayerSatelliteBtdSplit": "ひまわり 分割視窗", + "onboardingPermBackground": "背景定位", + "aedEmergencyPhone": "緊急聯絡電話", + "dpmOpenInMaps": "開啟地圖", + "meshtasticNotifyNodes": "新節點通知", + "onboardingPermCriticalDesc": "讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,顯示底圖", + "meshtasticSent": "已送出", + "homeForecastTitle": "24小時預報", + "typhoonLegendWarningAreas": "警報區域", + "meshtasticExcludeMqttHidden": "已隱藏 {count} 個", + "notifyOptLocalIntensity1": "所在地震度1以上", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "歷史", + "restroomTypeFemale": "女廁所", + "reportListToday": "今天", + "meshtasticTapNode": "點選節點查看詳細資訊", + "commonLoading": "載入中…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "中度颱風", + "typhoonWind": "風速", + "mapLayerSatelliteAsh": "ひまわり 火山灰", + "rainInterval3h": "3 時", + "reportListSearch": "查詢", + "meshtasticChannelReady": "DPIP 頻道已就緒", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "衛星", + "reportFilterLocation": "地點", + "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", + "typhoonIntensityTd": "熱帶性低氣壓", + "reportFilterDate": "日期", + "sponsorRestoreUnavailable": "無法連線至商店,請稍後再試", + "homeForecastPop": "{pop}%", + "regionEmpty": "尚未新增常用地區", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "允許 DPIP 在背景持續運作,避免警報延遲或漏收。", + "mapNavDisaster": "防災", + "radarScanRangeSubtitle": "標示四座雷達實際觀測到的範圍。", + "aedHoursSunday": "週日開放時間", + "reportDetailOriginTime": "發震時間", + "trendNoData": "沒有趨勢資料", + "onboardingPermLocation": "定位", + "moreDiscord": "Discord 社群", + "mapNavPressure": "氣壓", + "mapLayerSatelliteB13": "ひまわり 紅外線(B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "目前沒有更新日誌", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "開始日:當日 00:00(台北時間)", + "eewTitle": "地震速報", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "zh_Hant_HK", + "regionSelectCount": "已選 {count}/{max}", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/雲相", + "meshtasticStateError": "錯誤", + "weatherModeOvercast": "陰天", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "震源深度", + "typhoonOverlayWarningTooltip": "標示警報區域縣市", + "reportFilterDatePick": "選擇日期", + "onboardingSkipStay": "返回授權", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "無法獲取資料,請稍後重試", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "室外收容", + "meshtasticStateConnected": "已連線", + "mapNavRadar": "雷達", + "mapLayerSatelliteCloudClear": "晴空", + "eewSummary": "規模 {magnitude}・深度 {depth} 公里", + "locationBannerPermission": "尚未授權定位,無法針對你的所在地推送警報。", + "typhoonOverlayWeatherNoneTooltip": "不疊雷達或紅外線", + "radarCountyOutlineHint": "畫在回波之上", + "windForecastCountyOutlineHint": "繪製於風場之上", + "homeRainTrendTitle": "近 1 小時降水趨勢", + "moonPhaseFirstQuarter": "上弦月", + "mapLayerCategoryTyphoon": "颱風", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "空中工時(24 小時)", + "restroomTypeMixed": "混合廁所", + "restroomGradeGood": "優等級", + "notifyTsunami": "海嘯資訊", + "navData": "資料", + "mapLayerSatelliteBtdWvirw": "ひまわり 過衝雲頂", + "meshtasticReadingAge": "數值時間", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "此裝置無法撥打電話", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "不限", + "weatherRankingMergeTo": "合併至", + "notifyIntensity": "震度速報", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "累積時段", + "reportDetailLocalFelt": "小區域有感地震", + "meshtasticDevice": "裝置", + "onboardingGrant": "授權", + "weatherModeRain": "雨天", + "shelterVulnerableOkLabel": "適合避難弱者安置", + "stationSheetEmpty": "點選任一測站查看觀測值", + "typhoonLegendProbability": "侵襲機率", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "規模", + "skyTimeMorning": "上午", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "實驗性功能", + "onboardingTermsBody": "使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網絡狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機會比通知早抵達用戶所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供本地化警報,本服務會在前景及背景收集並上傳您的概略位置與裝置推送識別碼,僅用於決定應向您推送之警報。\n\n點按下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。", + "reportFilterTitle": "篩選", + "onboardingPermCritical": "重大通知", + "trendCumulativeTotal": "累計 {total} mm", + "languageName": "繁體中文(香港)", + "reportListEmptyFiltered": "沒有符合條件的地震報告", + "meshtasticExcludeMqtt": "隱藏 MQTT 節點", + "mapNavTyphoon": "颱風", + "weatherModeSand": "沙塵", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "衛星雲圖", + "@dpmOpenInMaps": {}, + "notifyReport": "地震報告", + "mapAppCoordinatesCopied": "已複製座標", + "skyTimeNight": "夜晚", + "sponsorRecommended": "推薦", + "mapLayerSatelliteB15": "ひまわり 長波紅外線(B15)", + "weatherRankingWind": "風速", + "feedStale": "資料可能已過期", + "homeForecastWind": "{direction} · {level} 級", + "navHome": "主頁", + "meshtasticRegionLabel": "地區", + "mapLayerSatelliteCloudtop": "ひまわり 雲頂溫度", + "moonTimelineCaption": "月相", + "reportListMeta": "M{magnitude} · {depth} 公里", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "引用套件", + "weatherRankingLowest": "最低", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "深度", + "mapTimelineDataTime": "資料時間 {time}", + "radarScanRange": "顯示掃描範圍", + "meshtasticHopLimit": "跳數上限", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "溫差 {value}°C", + "weatherRankingExtremeHigh": "今日最高", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "版本資訊", + "sponsorPrivacy": "私隱權政策", + "reportDetailLocalIntensity": "所在地的震度", + "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", + "meshtasticAirtime": "發射佔空比", + "shelterCapacityValue": "{n} 人", + "lightningLegendCc": "雲間 · {minutes} 分內", + "meshtasticSendHint": "要廣播的訊息", + "monitorDelay": "延遲 {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "否", + "mapLayerSatelliteB08": "ひまわり 上層水氣(B08)", + "meshtasticReconnecting": "重新連線中…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "讓鄉鎮界線在雷達回波下仍然清楚。", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "紅外線(對齊颱風報文時間)", + "radarScanRangeHint": "框外空白代表未觀測", + "typhoonPickerTd": "熱帶性低氣壓 TD {no}", + "mapLayerSatelliteWatervapor": "ひまわり 水氣", + "regionAddButton": "新增地區", + "displaySettings": "顯示設定", + "restroomGradePoor": "不合格", + "restroomCategoryTourist": "觀光地區及風景區", + "locationBannerServiceOff": "定位服務已關閉,無法針對你的所在地推送警報。", + "mapLayerStyleTooltip": "顯示樣式", + "lightningLegendCg": "對地 · {minutes} 分內", + "skyTimeAuto": "自動", + "appLogs": "App 日誌", + "feedConnecting": "連接中…", + "notifyBannerDisabled": "通知已關閉,將收不到災害警報。", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "濕度", + "typhoonValueMs": "每秒 {n} 公尺", + "homeForecastHumidity": "濕度 {value}%", + "meshtasticBusyBody": "請先在另一個 Meshtastic App 中斷線。兩個 App 同時連同一台裝置會互相搶走訊息,導致部分訊息遺失。", + "meshtasticChannelNoSlot": "沒有可用的頻道空位 — 請先在裝置上空出一個", + "restroomCategoryTransport": "交通", + "reportFilterLocationHint": "例如:花蓮、東部海域", + "moonSubtitle": "月相與亮度 — 完全本地計算", + "meshtasticBattery": "電量", + "meshtasticDistance": "距離", + "meshtasticSnrTrend": "訊號趨勢 (SNR)", + "meshtasticBatteryTrend": "電量趨勢", + "typhoonOverlayMenuTooltip": "颱風圖層選項", + "mapLayerSatelliteBtdOzone": "ひまわり 對流層頂", + "meshtasticRegionMismatch": "裝置地區為 {region} — DPIP 需要 TW", + "notifySectionEarthquake": "地震", + "mapLayerDisasterMap": "防災地圖", + "weatherModeFog": "大霧", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "氣象廳灰階慣例:溫度越低越白", + "moreAnnouncements": "公告", + "mapLayerSatelliteTransparentNoData": "無資料(陸地) = 透明", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "民眾洽公場所", + "typhoonLegendCurrent": "目前中心", + "aedAddress": "地址", + "mapLayerAed": "AED", + "changelogTypePrerelease": "公測", + "reportFilterIntensityInfoModernBody": "震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。", + "typhoonOverlayWeatherNone": "無", + "mapLayerStyleGray": "灰階(JMA)", + "weatherModeAuto": "自動", + "typhoonLabelProbCircle": "70%機率圓", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "接收全部", + "displayTheme": "主題", + "mapLayerSatelliteB07": "ひまわり 短波紅外(B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "過去移動方向", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "常用地區", + "typhoonLegendCone": "預測圓錐", + "moreCwaEew": "中央氣象署強震即時警報", + "onboardingPermsTitle": "權限授權", + "mapLayerStyleJma": "雲頂強調(JMA)", + "rainInterval10m": "10 分", + "weatherRankingAnalysisLow": "最低 {value}", + "meshtasticConnectAnyway": "仍要連線", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "ひまわり 近紅外(B06)", + "mapLayerSatelliteTransparentReflectance": "低反射率/夜間 = 透明,顯示底圖", + "chartHourLabel": "{hour}時", + "mapLayerShelter": "避難收容場所", + "typhoonOverlayProbabilityTooltip": "顯示侵襲機率(會隱藏預測圓錐)", + "mapLayerSatelliteNdwi": "ひまわり 水體指數", + "disasterMapOverlayShelterTooltip": "顯示避難收容場所", + "mapNavHumidity": "濕度", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "依震度排序", + "homeRainTrendNoData": "無資料", + "mapLayerCategoryRadar": "雷達", + "meshtasticShortName": "簡稱", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "ひまわり 氣團", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "路徑詳情", + "dataSectionWeather": "氣象", + "aedHoursWeekday": "平日開放時間", + "homeActiveEventsTitle": "生效中事件", + "weatherRankingAnalysisHigh": "最高 {value}", + "faq": "常見問題", + "typhoonHistoryLive": "即時", + "eewSerial": "第 {serial} 報", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "排序方式", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "要將這台裝置切換為 TW 地區嗎?裝置會重新啟動並短暫斷線,上面的其他頻道也會一起改變。", + "dataEarthquakeSubtitle": "地震報告", + "typhoonNoActive": "目前無颱風", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "ひまわり 二氧化硫/雲相(B11)", + "navEvents": "事件", + "onboardingTermsTitle": "服務條款", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "鄉鎮名稱", + "notifySetFailed": "設定失敗,請稍後再試。", + "meshtasticDisconnect": "斷線", + "meshtasticUndecoded": "無法解密", + "notifyAnnouncement": "公告", + "onboardingIntroTitle": "歡迎使用 DPIP", + "regionCurrentUnavailable": "無法取得所在地位置資訊", + "languageSystem": "系統預設", + "skyTimeSunset": "日落", + "mapLayerSatelliteDust": "ひまわり 沙塵", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "修改", + "weatherDynamicState": "天氣動態狀態", + "mapPlaceholderDisabled": "地圖(暫時停用)", + "moonNow": "現在", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "外觀", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "月出月落", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "接下來", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "月曆", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "距離", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "公里", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "視直徑", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "月出", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "月落", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "下次新月", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "整日在地平線上", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "當日無", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "太陽", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "日出日沒、曙暮光與節氣", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "日照", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "曙暮光", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "光線", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "日晷", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "節氣", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "日出", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "日沒", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "正午", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "白晝長度", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "民用", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "航海", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "天文", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "晨間黃金時刻", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "昏間黃金時刻", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "藍調時刻", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "均時差", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "分", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "下一個節氣", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "行星", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "今晚在哪、有多亮", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "此刻", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "地平線上", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "地平線下", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "太近太陽", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "亮度", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "距日距角", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "時段", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "昏星", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "晨星", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "距離", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "天文單位", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "仰角", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "水星", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "金星", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "火星", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "木星", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "土星", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "天王星", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "海王星", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "春分", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "清明", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "穀雨", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "立夏", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "小滿", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "芒種", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "夏至", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "小暑", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "大暑", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "立秋", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "處暑", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "白露", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "秋分", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "寒露", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "霜降", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "立冬", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "小雪", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "大雪", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "冬至", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "小寒", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "大寒", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "立春", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "雨水", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "驚蟄", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "今夜", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "現在看得到什麼、什麼時候", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "觀測窗口", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "天文夜", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "整夜不全暗", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "暗窗", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "月亮整夜在天上", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "總暗時", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "月光", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "流星雨", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "輻射點不升起", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "顆/時", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "衛星過境", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "此刻可觀測目標", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "象限儀座", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "天琴座", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "寶瓶座η", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "寶瓶座δ", + "@showerDeltaAquariids": { + "description": "Meteor shower name" + }, + "showerPerseids": "英仙座", + "@showerPerseids": { + "description": "Meteor shower name" + }, + "showerOrionids": "獵戶座", + "@showerOrionids": { + "description": "Meteor shower name" + }, + "showerSouthernTaurids": "金牛座南", + "@showerSouthernTaurids": { + "description": "Meteor shower name" + }, + "showerLeonids": "獅子座", + "@showerLeonids": { + "description": "Meteor shower name" + }, + "showerGeminids": "雙子座", + "@showerGeminids": { + "description": "Meteor shower name" + }, + "showerUrsids": "小熊座", + "@showerUrsids": { + "description": "Meteor shower name" + }, + "deepSkyOpenCluster": "疏散星團", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" + }, + "deepSkyGlobularCluster": "球狀星團", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" + }, + "deepSkySpiralGalaxy": "螺旋星系", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyEllipticalGalaxy": "橢圓星系", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyIrregularGalaxy": "不規則星系", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" + }, + "deepSkyPlanetaryNebula": "行星狀星雲", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" + }, + "deepSkySupernovaRemnant": "超新星遺跡", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" + }, + "deepSkyEmissionNebula": "發射星雲", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyReflectionNebula": "反射星雲", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" + }, + "deepSkyAsterism": "星群", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" + }, + "almanacTitle": "曆法", + "@almanacTitle": { + "description": "Almanac page title" + }, + "almanacSubtitle": "農曆日期與未來的日月食", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" + }, + "almanacSectionToday": "今日", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" + }, + "almanacGregorian": "西曆", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "almanacLunar": "農曆", + "@almanacLunar": { + "description": "The lunisolar date" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "almanacYear": "歲次", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "mapAppDefault": "{app}(預設)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "almanacMonthLength": "月大小", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "mapAppCopyCoordinates": "複製座標", - "@mapAppCopyCoordinates": { + "almanacLongMonth": "三十日", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "mapAppCoordinatesCopied": "已複製座標", - "@mapAppCoordinatesCopied": { + "almanacShortMonth": "二十九日", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "mapAppOpenFailed": "無法開啟 {app}", - "@mapAppOpenFailed": { + "almanacLeapPrefix": "閏", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - - "mapAppCallFailed": "此裝置無法撥打電話", - - "mapOverlaySectionReference": "參考圖層", - "mapLayerCategoryEarthquake": "地震", - "mapLayerCategoryTyphoon": "颱風", - "mapLayerCategoryWeather": "氣象觀測", - "mapLayerCategorySatellite": "衛星", - "mapLayerCategoryRadar": "雷達", - "mapLayerCategoryLife": "生活", - "mapLayerCategoryForecast": "數值預報", "mapOverlaySectionMap": "地圖", - "rainIntervalSection": "統計時間", - - "mapTownLabels": "鄉鎮名稱", - "mapTownLabelsHint": "放大時顯示鄉鎮名稱", - - "mapTerrainRelief": "地形立體感", - "mapTerrainReliefHint": "在底圖上顯示立體地形陰影", - - "dpmSheetEmpty": "點選地圖上的標記查看詳情", - "dpmAddress": "地址", - "restroomTypeLabel": "廁所類型", - "restroomCategoryLabel": "類別", - "restroomGradeLabel": "等級", - "restroomTypeFemale": "女廁所", - "restroomTypeMale": "男廁所", - "restroomTypeMixed": "混合廁所", - "restroomTypeAccessible": "無障礙廁所", - "restroomTypeGenderNeutral": "性別友善廁所", - "restroomTypeFamily": "親子廁所", - "restroomTypeUnspecified": "未設定", - "restroomCategoryTransport": "交通", - "restroomCategoryPark": "公園", - "restroomCategoryCommercial": "商業營業場所", - "restroomCategoryReligious": "宗教禮儀場所", - "restroomCategoryCultural": "文化育樂活動場所", - "restroomCategoryGovernment": "民眾洽公場所", - "restroomCategoryWelfare": "社福機構、集會場所", - "restroomCategoryTourist": "觀光地區及風景區", - "restroomCategoryLeisure": "休閒娛樂場所", - "restroomCategoryOther": "其他", - "restroomGradeExcellent": "特優級", - "restroomGradeGood": "優等級", - "restroomGradeAverage": "普通級", - "restroomGradePoor": "不合格", - "shelterAddressLabel": "地址", - "shelterCapacityLabel": "收容人數", - "shelterCapacityValue": "{n} 人", - "shelterCategoryLabel": "適用災害", - "shelterIndoorLabel": "室內收容", - "shelterOutdoorLabel": "室外收容", - "shelterVulnerableOkLabel": "適合避難弱者安置", - "dpmYes": "是", - "dpmNo": "否", - "stationSheetEmpty": "點選任一測站查看觀測值", - "monitorDelay": "延遲 {value} s", - "monitorWaiting": "等待資料…", - "mapLegendUnit": "單位:{unit}", - "typhoonLegendPast": "實際路徑", - "typhoonLegendForecast": "預測路徑", - "typhoonLegendForecastPoint": "預測點", - "typhoonLegendCurrent": "目前中心", - "typhoonLegendCone": "預測圓錐", - "mapLegendExpand": "圖例", - "mapLegendCollapse": "收合圖例", - "mapMyLocation": "我的位置", - "mapResetNorth": "回到北方", - "typhoonLegendCircle15": "七級風暴風圈", - "typhoonLegendCircle25": "十級風暴風圈", - "typhoonLegendProbability": "侵襲機率", - "typhoonLegendWarningAreas": "警報區域", - "typhoonWarningTitle": "颱風警報", - "typhoonWarningAreas": "警戒區域:{areas}", - "typhoonTrackDetail": "路徑詳情", - "typhoonHistoryTitle": "資料時間", - "typhoonHistoryLive": "即時", - "typhoonSatelliteTitle": "衛星雲圖", - "typhoonDataTime": "資料時間\n{time}", - "typhoonForecastLead": "預測 +{hours} 小時", - "typhoonIntensityIntense": "強烈颱風", - "typhoonIntensityMild": "輕度颱風", - "typhoonIntensityModerate": "中度颱風", - "typhoonIntensityTd": "熱帶性低氣壓", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "熱帶性低氣壓 TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonLabelDirection": "過去移動方向", - "typhoonLabelGaleAvg": "七級風平均暴風半徑", - "typhoonLabelGust": "瞬間最大陣風", - "typhoonLabelNe": "東北側", - "typhoonLabelNw": "西北側", - "typhoonLabelPosition": "中心位置", - "typhoonLabelPressure": "中心氣壓", - "typhoonLabelProbCircle": "70%機率圓", - "typhoonLabelSe": "東南側", - "typhoonLabelSpeed": "過去移動時速", - "typhoonLabelStormAvg": "十級風平均暴風半徑", - "typhoonLabelSw": "西南側", - "typhoonLabelWind": "近中心最大風速", - "typhoonLegendCircleAvg": "平均圓", - "typhoonOverlayMenuTooltip": "颱風圖層選項", - "typhoonOverlayProbabilityHint": "會隱藏預測圓錐", - "typhoonOverlayProbabilityTooltip": "顯示侵襲機率(會隱藏預測圓錐)", - "typhoonOverlaySectionExtra": "覆蓋層", - "typhoonOverlaySectionStorm": "暴風圈", - "typhoonOverlaySectionWeather": "天氣底圖", - "typhoonOverlayStormBandSubtitle": "含平均圓", - "typhoonOverlayStormL10Tooltip": "十級暴風圈+平均圓(黃色)", - "typhoonOverlayStormL7Tooltip": "七級暴風圈+平均圓(紫色)", - "typhoonOverlayWarningTooltip": "標示警報區域縣市", - "typhoonOverlayWeatherHint": "對齊報文時間", - "typhoonOverlayWeatherNone": "無", - "typhoonOverlayWeatherNoneTooltip": "不疊雷達或紅外線", - "typhoonOverlayWeatherRadarTooltip": "雷達回波(對齊颱風報文時間)", - "typhoonOverlayWeatherSatelliteTooltip": "紅外線(對齊颱風報文時間)", - "typhoonStormRadii": "東北 {ne} · 東南 {se} · 西南 {sw} · 西北 {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonValueHpa": "{n} 百帕", - "typhoonValueKm": "{n} 公里", - "typhoonValueLat": "北緯 {lat} 度", - "typhoonValueLon": "東經 {lon} 度", - "typhoonValueMs": "每秒 {n} 公尺", - "typhoonOverlayForecastCallouts": "預測點資訊", - "typhoonOverlayForecastCalloutsTooltip": "放大時顯示預測點詳細卡片", - "dpmFilterSectionRestroom": "場所類型", - "dpmFilterSectionRestroomType": "廁所類型", - "dpmFilterSectionShelter": "避難所災害類型", - "dpmDisasterFlood": "水災", - "dpmDisasterEarthquake": "震災", - "dpmDisasterLandslide": "土石流", - "dpmDisasterTsunami": "海嘯", - "dpmDisasterSlope": "坡地災害", - "dpmDisasterNuclear": "核子事故", - "skyTime": "天空時間", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "almanacSectionLunarEclipses": "月食", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "skyTimeAuto": "自動", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "almanacSectionSolarEclipses": "日食", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "skyTimeDawn": "黎明", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "almanacNoSolarEclipse": "範圍內無", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "skyTimeSunrise": "日出", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "eclipseTotal": "全食", + "@eclipseTotal": { + "description": "Eclipse type" }, - "skyTimeMorning": "上午", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "eclipsePartial": "偏食", + "@eclipsePartial": { + "description": "Eclipse type" }, - "skyTimeNoon": "正午", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "eclipseAnnular": "環食", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "skyTimeAfternoon": "下午", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "eclipsePenumbral": "半影食", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "skyTimeGolden": "黃金時刻", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "zodiacRat": "鼠", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "skyTimeSunset": "日落", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "zodiacOx": "牛", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "skyTimeDusk": "暮色", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "zodiacTiger": "虎", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "skyTimeNight": "夜晚", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "zodiacRabbit": "兔", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "weatherModeCloudy": "多雲", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "zodiacDragon": "龍", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "weatherModeOvercast": "陰天", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "zodiacSnake": "蛇", + "@zodiacSnake": { + "description": "Chinese zodiac animal" }, - "weatherModeSnow": "下雪", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "zodiacHorse": "馬", + "@zodiacHorse": { + "description": "Chinese zodiac animal" }, - "weatherModeSand": "沙塵", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "zodiacGoat": "羊", + "@zodiacGoat": { + "description": "Chinese zodiac animal" }, - "radarScanRange": "顯示掃描範圍", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacMonkey": "猴", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" }, - "radarScanRangeSubtitle": "標示四座雷達實際觀測到的範圍。", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "zodiacRooster": "雞", + "@zodiacRooster": { + "description": "Chinese zodiac animal" }, - "radarScanRangeHint": "框外空白代表未觀測", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "zodiacDog": "狗", + "@zodiacDog": { + "description": "Chinese zodiac animal" }, - "radarOverlayMenuTooltip": "雷達圖層選項", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "zodiacPig": "豬", + "@zodiacPig": { + "description": "Chinese zodiac animal" }, - "radarCountyOutline": "縣市界線", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tideTitle": "潮汐", + "@tideTitle": { + "description": "Tide page title" }, - "radarGlobalOutline": "國界", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "tideSubtitle": "大潮、小潮與月球引力", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" }, - "radarGlobalOutlineHint": "各國國界外框", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "tideDisclaimer": "僅為天文引潮力,非港口潮汐表。水位請參考氣象署公布之潮汐預報。", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" }, - "radarCountyOutlineHint": "畫在回波之上", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "tideSectionNow": "此刻", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" }, - "radarCountyOutlineSubtitle": "讓縣市界線在雷達回波下仍然清楚。", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "tidePhase": "週期", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" }, - "radarTownOutline": "鄉鎮界線", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideSpring": "大潮", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" }, - "radarTownOutlineHint": "較細的分區", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "tideNeap": "小潮", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" }, - "radarTownOutlineSubtitle": "讓鄉鎮界線在雷達回波下仍然清楚。", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "tideMiddling": "中潮", + "@tideMiddling": { + "description": "Between spring and neap" }, - "qpesumsOverlayMenuTooltip": "定量降水預報圖層選項", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "tideLunarDistanceFactor": "月球引力", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" }, - "windForecastOverlayMenuTooltip": "風場預報圖層選項", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "tideEquilibrium": "平衡潮高", + "@tideEquilibrium": { + "description": "The equilibrium tide height" }, - "windForecastCountyOutlineHint": "繪製於風場之上", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "tideMetres": "公尺", + "@tideMetres": { + "description": "Unit: metres" }, - "windForecastGlobalOutlineHint": "各國國界外框", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "tidePerigeanSpring": "下次近地點大潮", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" }, - "windForecastTownOutlineHint": "更細的網格", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "tideSectionTurningPoints": "轉折點", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" }, - "eewSerial": "第 {serial} 報", - "eewMaxIntensity": "最大震度", - "eewLocalIntensity": "所在地預估", - "eewSWave": "震波", - "eewArrived": "已抵達", - "eewCountdown": "{seconds} 秒" + "tideHigh": "高", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "低", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "星圖", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "頭頂上肉眼可見的天空", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "北", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "東", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "南", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "西", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "軌道資料 {days} 天前", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}{month} 月 {day} 日", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "目前無流星雨", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "48 小時內無可見過境", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "無法讀取軌道資料", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "無足夠高度的目標", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "無法讀取星表", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb index 3a7d9aa2b..fd5ac21e7 100644 --- a/lib/l10n/app_zh_TW.arb +++ b/lib/l10n/app_zh_TW.arb @@ -1,679 +1,1739 @@ { - "@@locale": "zh_TW", - "languageName": "繁體中文(臺灣)", - "navHome": "首頁", - "navEvents": "事件", - "navMap": "地圖", - "navData": "資料", - "navEarthquake": "地震", - "dataSectionSeismic": "地震", - "dataEarthquakeSubtitle": "地震報告", - "dataSectionWeather": "氣象", - "dataWeatherRankingSubtitle": "即時觀測排行", - "weatherRankingTitle": "觀測排行", - "weatherRankingMeta": "資料時間:{time}\n共 {count} 觀測點", - "weatherRankingEmpty": "目前沒有可排序的觀測", - "weatherRankingBy": "依", - "weatherRankingHighest": "最高", - "weatherRankingLowest": "最低", - "weatherRankingMergeTo": "合併至", - "weatherRankingMergeTown": "鄉鎮", - "weatherRankingMergeCounty": "縣市", - "weatherRankingWind": "風速", - "weatherRankingGust": "陣風", + "typhoonValueLat": "北緯 {lat} 度", + "onboardingSkipBody": "未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。", + "@mapAppCoordinatesCopied": {}, + "@meshtasticLayerOptions": { + "description": "Tooltip for the mesh layer's options chip" + }, + "rainInterval24h": "24 時", + "homeRainTrendHeavyStopping": "預計 {minutes} 分鐘後停止下大雨", + "mapTimelineObserved": "觀測", + "regionSelectTitle": "選擇地區", + "skyTimeNoon": "正午", + "radarCountyOutlineSubtitle": "讓縣市界線在雷達回波下仍然清楚。", + "@meshtasticRegionLabel": { + "description": "LoRa region" + }, + "dpmFilterSectionRestroomType": "廁所類型", + "mapLayerSatelliteB03": "ひまわり 可見光-紅(B03)", + "reportFilterIntensity": "震度", + "mapLayerLightning": "閃電", + "restroomTypeMale": "男廁所", + "meshtasticLastReceived": "最近接收", + "reportDetailSortByCounty": "依縣市排序", + "@moonSubtitle": { + "description": "Moon entry card subtitle in the data catalogue" + }, + "@moonDays": { + "description": "Day unit for the moon age" + }, + "homeRainTrendScattered": "可能會有零星降雨", + "meshtasticUptime": "運行時間", "weatherRankingTempExtremes": "溫度極值", - "weatherRankingExtremeHigh": "今日最高", - "weatherRankingExtremeLow": "今日最低", + "themeLight": "淺色", + "mapTerrainReliefHint": "在底圖上顯示立體地形陰影", + "meshtasticEmptyMessage": "(空白訊息)", + "moreSectionRegion": "地區", + "dpmDisasterEarthquake": "震災", + "mapLayerSatellite": "ひまわり 紅外線(B13)", + "@meshtasticTapNode": { + "description": "Resting state of the map node sheet" + }, + "aedHoursSaturday": "週六開放時間", + "dpmDisasterSlope": "坡地災害", + "moonPhaseNew": "新月", + "notifySectionEew": "地震速報", + "mapResetNorth": "回到北方", + "rainInterval2d": "2 日", + "mapTownLabelsHint": "放大時顯示鄉鎮名稱", + "commonCancel": "取消", + "notifyOptTsunamiWarning": "只接收海嘯警報", + "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", + "@meshtasticSelectDevice": { + "description": "Device picker sheet title" + }, + "moreSectionAdvanced": "進階", + "@meshtasticLastHeard": { + "description": "When a node last transmitted" + }, "weatherRankingExtremeRange": "日溫差", + "notifySettingsMenu": "通知設定", + "typhoonHistoryTitle": "資料時間", + "mapAppDefault": "{app}(預設)", + "trendRange24h": "24 小時", + "mapLayerStyleJmaTooltip": "灰階為底,−40 °C 以下上色,凸顯雲頂高度", "weatherRankingRecordedAt": "記錄於 {time}", - "weatherRankingAnalysisCurrent": "當下 {value}°C", - "weatherRankingAnalysisHigh": "最高 {value}", - "weatherRankingAnalysisLow": "最低 {value}", - "weatherRankingAnalysisRange": "溫差 {value}°C", - "reportListEmpty": "目前沒有地震報告", - "reportListEmptyFiltered": "沒有符合條件的地震報告", - "reportListMeta": "M{magnitude} · {depth} 公里", - "reportListMagnitude": "M{magnitude}", - "reportListDepthUnit": "公里", - "reportListLocalFelt": "小區域有感", - "reportListToday": "今天", - "reportListYesterday": "昨天", - "reportListDayCount": "{count}", - "reportListEnd": "已到最後一頁", - "reportFilterTitle": "篩選", - "reportFilterSort": "排序方式", - "reportFilterSortTime": "時間", - "reportFilterSortIntensity": "震度", - "reportFilterSortMagnitude": "規模", - "reportFilterSortDepth": "深度", + "mapLayerRain": "雨量", + "mapLayerQpesums": "未來 1 小時降水預報", + "@weatherModeSnow": { + "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + }, + "@dataSectionAstronomy": { + "description": "Astronomy section header in the data catalogue" + }, + "mapOverlaySectionMap": "地圖", + "mapTerrainRelief": "地形立體感", + "eewMaxIntensity": "最大震度", + "mapLegendCollapse": "收合圖例", + "changelogTitle": "更新日誌", "reportFilterOrderDesc": "降序", - "reportFilterOrderAsc": "升序", - "reportFilterIntensity": "震度", + "meshtasticExcludeMqttSubtitle": "經網際網路橋接、並非無線電聽到的節點", "reportFilterIntensityInfoTitle": "震度新制與舊制", - "reportFilterIntensityInfoIntro": "中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。", - "reportFilterIntensityInfoLegacyTitle": "舊制(2020 以前)", - "reportFilterIntensityInfoLegacyBody": "震度僅 0–7,沒有 5弱/5強/6弱/6強。", - "reportFilterIntensityInfoModernTitle": "新制(2020 起)", - "reportFilterIntensityInfoModernBody": "震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。", - "reportFilterMagnitude": "規模", - "reportFilterDepth": "深度", - "reportFilterDepthKm": "{depth} 公里", - "reportFilterDate": "日期", - "reportFilterDatePick": "選擇日期", - "reportFilterDateStartNote": "開始日:當日 00:00(臺北時間)", + "mapLayerTyphoon": "颱風", + "radarOverlayMenuTooltip": "雷達圖層選項", + "mapMyLocation": "我的位置", + "@meshtasticChannelUse": { + "description": "Share of airtime seen busy" + }, + "meshtasticNodes": "節點", + "meshtasticSend": "傳送", + "typhoonOverlayStormL7Tooltip": "七級暴風圈+平均圓(紫色)", + "aedType": "場所類型", + "termsOfService": "服務條款", + "typhoonLegendCircle25": "十級風暴風圈", + "sponsorTitle": "支持 DPIP", + "mapNavSatellite": "衛星", + "homeRainTrendUpdated": "更新 {time}", + "onboardingNext": "下一步", + "weatherRankingMergeTown": "鄉鎮", + "mapLayerMonitor": "強震監視器", + "moreYoutube": "YouTube", + "sponsorSubscriptions": "訂閱制", + "typhoonValueLon": "東經 {lon} 度", + "skyTime": "天空時間", + "weatherModeCloudy": "多雲", + "skyTimeDusk": "暮色", + "@meshtasticExcludeMqttSubtitle": { + "description": "What an MQTT node is" + }, + "meshtasticFirmware": "韌體", + "@mapLayerMeshtastic": { + "description": "Map layer name: mesh nodes" + }, "reportFilterDateEndNote": "結束日:當日 24:00(臺北時間)", - "reportFilterRange": "{start} – {end}", - "reportFilterLocation": "地點", - "reportFilterLocationHint": "例如:花蓮、東部海域", - "reportFilterAny": "不限", - "reportFilterApply": "套用", - "reportFilterReset": "重設", - "reportListSearch": "查詢", - "reportDetailTitle": "地震報告", - "reportDetailNumbered": "編號 {number} 顯著有感地震", - "reportDetailLocalFelt": "小區域有感地震", - "reportDetailInfo": "詳細資訊", - "reportDetailOriginTime": "發震時間", - "reportDetailEpicenter": "震央座標", - "reportDetailMagnitude": "地震規模", - "reportDetailDepth": "震源深度", - "reportDetailAreaIntensity": "各地震度", - "reportDetailLocalIntensity": "所在地的震度", - "reportDetailLocalIntensityUnavailable": "沒有震度訊息", - "reportDetailSortByIntensity": "依震度排序", - "reportDetailSortByCounty": "依縣市排序", - "reportDetailImage": "地震報告圖", - "reportDetailImageUnavailable": "報告圖尚未提供", - "reportDetailOpenReport": "報告頁面", - "reportDetailReplay": "重播", - "navMore": "更多", - "appLogs": "App 日誌", - "changelogTitle": "更新日誌", - "changelogEmpty": "目前沒有更新日誌", - "changelogTypePrerelease": "公測", - "changelogTypeStable": "正式", - "changelogCurrentVersion": "目前版本", - "changelogVersionDetails": "版本資訊", - "changelogBodyEmpty": "此版本沒有說明。", - "mapPlaceholderDisabled": "地圖(暫時停用)", - "moreSectionRegion": "地區", - "moreSectionNotify": "通知", - "moreSectionDisplay": "顯示", - "regionManageTitle": "常用地區", - "regionAddButton": "新增地區", - "regionEmpty": "尚未新增常用地區", - "regionSelectTitle": "選擇地區", - "regionSelectCount": "已選 {count}/{max}", - "regionSelectFull": "最多只能選擇 {max} 個地區", - "regionEdit": "修改", - "moreSectionAdvanced": "進階", - "moreDeveloper": "除錯資訊", - "experimentalFeatures": "實驗性功能", - "moreSectionLinks": "相關連結", - "moreCwaEew": "中央氣象署強震即時警報", - "moreTremReport": "TREM 檢知報告", - "moreServerStatus": "伺服器狀態", - "moreAnnouncements": "公告", - "moreDiscord": "Discord 社群", - "moreNotifyLog": "DPIP 通知發送記錄", - "moreLinkOpenFailed": "無法開啟連結", - "weatherDynamicState": "天氣動態狀態", - "weatherDynamicStateSubtitle": "覆寫主頁背景天氣", - "weatherModeAuto": "自動", - "weatherModeClear": "晴天", - "weatherModeRain": "雨天", - "weatherModeFog": "大霧", - "weatherModeThunderstorm": "雷雨", - "commonLoading": "載入中…", - "commonRetry": "重試", - "commonError": "發生錯誤", - "commonFetchFailed": "無法獲取資料,請稍後重試", - "commonEmpty": "沒有資料", - "feedConnecting": "連線中…", - "feedStale": "資料可能已過期", - "feedOffline": "連線中斷", - "eewTitle": "地震速報", - "eewNone": "目前沒有地震速報", - "eewSummary": "規模 {magnitude}・深度 {depth} 公里", - "regionNationwide": "全國", - "regionCurrent": "所在地", - "regionCurrentUnavailable": "無法取得所在地位置資訊", - "weatherPrecipitation": "降水量", - "weatherHumidity": "濕度", - "weatherDataTime": "{station} ∙ 資料時間 {time}", - "homeViewOnMap": "前往地圖察看", - "homeForecastTitle": "24小時預報", + "meshtasticSilent": "已靜默", + "reportFilterSortMagnitude": "規模", + "mapLayerCategoryEarthquake": "地震", + "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", + "typhoonLegendPast": "實際路徑", + "restroomCategoryOther": "其他", + "@meshtasticRegionConfirm": { + "description": "Confirmation before rebooting the radio" + }, + "@skyTimeSunset": { + "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + }, "homeForecastHighLow": "高 {high}° · 低 {low}°", - "homeForecastPop": "{pop}%", - "homeForecastFeelsLike": "體感 {temp}°", - "homeForecastHumidity": "濕度 {value}%", - "homeForecastWind": "{direction} · {level} 級", - "homeForecastUnavailable": "選擇鄉鎮後可查看預報", - "homeForecastEmpty": "目前沒有預報資料", - "homeActiveEventsTitle": "生效中事件", - "homeActiveEventsEmpty": "目前沒有生效中的事件", - "homeRainTrendTitle": "近 1 小時降水趨勢", - "homeRainTrendMinute": "{minute}分", - "homeRainTrendUpdated": "更新 {time}", - "homeRainTrendNoData": "無資料", - - "homeRainTrendScattered": "可能會有零星降雨", - "homeRainTrendLightSustained": "未來 1 小時會有持續小雨", - "homeRainTrendLightStopping": "預計 {minutes} 分鐘後停止下小雨", + "@meshtasticChannelFailed": { + "description": "The radio rejected the channel write" + }, + "locationBannerFix": "開啟設定", + "mapLegendExpand": "圖例", + "eewNone": "目前沒有地震速報", + "typhoonTyNo": "TY {no}", + "notifyOptTsunamiAll": "海嘯消息、海嘯警報", + "@windForecastGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + }, + "@skyTimeNight": { + "description": "Label for the skyTimeNight option in the experimental backdrop settings." + }, + "@radarCountyOutlineHint": { + "description": "Hint under the county-border toggle in the radar overlay menu." + }, + "meshtasticLayerOptions": "節點選項", + "onboardingAgreeContinue": "同意並繼續", + "meshtasticNodeId": "節點 ID", + "commonRetry": "重試", + "reportDetailNumbered": "編號 {number} 顯著有感地震", + "typhoonOverlayStormBandSubtitle": "含平均圓", + "disasterMapOverlayRestroomTooltip": "顯示公廁", + "weatherRankingTitle": "觀測排行", "homeRainTrendHeavySustained": "未來 1 小時會有持續大雨", - "homeRainTrendHeavyStopping": "預計 {minutes} 分鐘後停止下大雨", - "mapLayers": "圖層", - "mapLayerOrderTitle": "調整圖層順序", - "mapLayerOrderReset": "回復預設順序", - "mapLayerRadar": "雷達合成回波圖", - "mapLayerSatellite": "ひまわり 紅外線(B13)", - "mapLayerSatelliteB01": "ひまわり 可見光-藍(B01)", - "mapLayerSatelliteB02": "ひまわり 可見光-綠(B02)", - "mapLayerSatelliteB03": "ひまわり 可見光-紅(B03)", - "mapLayerSatelliteB04": "ひまわり 近紅外(B04)", + "notifySectionTsunami": "海嘯", + "restroomCategoryPark": "公園", + "moreLinkOpenFailed": "無法開啟連結", + "themeDark": "深色", + "sponsorRestore": "恢復購買", + "@meshtasticSilent": { + "description": "Legend: node known but not heard recently" + }, + "meshtasticChannelWorking": "正在設定 DPIP 頻道…", + "meshtasticRegionSwitch": "切換為 TW", + "@meshtasticLastReceived": { + "description": "Age of the last received packet" + }, + "meshtasticTraffic": "流量", + "@meshtasticDpipChannel": { + "description": "Which channel DPIP payloads use" + }, + "mapLayerStyleBdTooltip": "Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階", + "disasterMapOverlayAedTooltip": "顯示 AED 位置", + "@moonTitle": { + "description": "Moon page title" + }, + "mapLayerHumidity": "濕度", + "mapLayerSatelliteTransparentNight": "夜間 = 透明,顯示底圖", + "meshtasticScanning": "掃描中…", + "@meshtasticDevice": { + "description": "Section: device identity" + }, + "regionSelectFull": "最多只能選擇 {max} 個地區", + "meshtasticTitle": "Meshtastic", + "navMore": "更多", + "meshtasticDpipChannel": "DPIP 頻道", + "disasterMapOverlaySectionLayers": "圖層", + "@moonPhaseWaningCrescent": { + "description": "Phase: waning crescent" + }, "mapLayerSatelliteB05": "ひまわり 近紅外(B05)", - "mapLayerSatelliteB06": "ひまわり 近紅外(B06)", - "mapLayerSatelliteB07": "ひまわり 短波紅外(B07)", - "mapLayerSatelliteB08": "ひまわり 上層水氣(B08)", - "mapLayerSatelliteB09": "ひまわり 中層水氣(B09)", - "mapLayerSatelliteB10": "ひまわり 低層水氣(B10)", - "mapLayerSatelliteB11": "ひまわり 二氧化硫/雲相(B11)", - "mapLayerSatelliteB12": "ひまわり 臭氧(B12)", - "mapLayerSatelliteB13": "ひまわり 紅外線(B13)", - "mapLayerSatelliteB14": "ひまわり 長波紅外線(B14)", - "mapLayerSatelliteB15": "ひまわり 長波紅外線(B15)", - "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "typhoonStormRadii": "東北 {ne} · 東南 {se} · 西南 {sw} · 西北 {nw} km", + "@meshtasticNotConnected": { + "description": "Empty message log while not connected" + }, + "@weatherModeCloudy": { + "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + }, + "typhoonLabelNe": "東北側", + "meshtasticCopied": "已複製訊息", + "reportListEmpty": "目前沒有地震報告", + "reportListEnd": "已到最後一頁", "mapLayerSatelliteTruecolor": "ひまわり 真彩色", - "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", - "mapLayerSatelliteAsh": "ひまわり 火山灰", - "mapLayerSatelliteDust": "ひまわり 沙塵", - "mapLayerSatelliteAirmass": "ひまわり 氣團", - "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", - "mapLayerSatelliteWatervapor": "ひまわり 水氣", - "mapLayerSatelliteBtdSplit": "ひまわり 分割視窗", - "mapLayerSatelliteBtdFog": "ひまわり 夜間霧", - "mapLayerSatelliteBtdWvirw": "ひまわり 過衝雲頂", - "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/雲相", - "mapLayerSatelliteBtdCo2": "ひまわり 卷雲/雲高", - "mapLayerSatelliteBtdOzone": "ひまわり 對流層頂", - "mapLayerSatelliteCloudtop": "ひまわり 雲頂溫度", - "mapLayerSatelliteCloudmask": "ひまわり 雲遮罩", - "mapLayerSatelliteSst": "ひまわり 海表溫度", - "mapLayerSatelliteNdvi": "ひまわり 植生指數", - "mapLayerSatelliteNdwi": "ひまわり 水體指數", - "mapLayerSatelliteMndwi": "ひまわり 改良水體指數", + "typhoonOverlaySectionExtra": "覆蓋層", + "eewSWave": "震波", + "meshtasticBusyTitle": "另一個 App 正在使用這台裝置", + "restroomCategoryCultural": "文化育樂活動場所", + "typhoonLabelWind": "近中心最大風速", + "radarGlobalOutlineHint": "各國國界外框", + "notifyEvacuation": "防災資訊", + "typhoonLegendCircle15": "七級風暴風圈", + "@radarGlobalOutline": { + "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + }, + "@meshtasticRadioSettings": { + "description": "Section: LoRa settings" + }, + "dataSectionAstronomy": "天文", + "homeRainTrendLightSustained": "未來 1 小時會有持續小雨", + "commonError": "發生錯誤", + "moonPhaseWaningCrescent": "殘月", + "meshtasticPower": "電力", + "@meshtasticChannelWorking": { + "description": "Creating/verifying the DPIP channel" + }, + "mapTimelineNow": "現在", + "reportFilterRange": "{start} – {end}", + "reportDetailOpenReport": "報告頁面", + "trendRange7d": "7 天", + "typhoonWarningAreas": "警戒區域:{areas}", + "rainIntervalSection": "統計時間", + "notifyTitle": "通知", + "meshtasticTxPower": "發射功率", + "@radarTownOutlineHint": { + "description": "Hint under the township-border toggle in the radar overlay menu." + }, + "restroomCategoryLabel": "類別", + "sponsorRestoring": "正在恢復購買…", + "sponsorIntro": "DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。", + "shelterAddressLabel": "地址", + "typhoonLabelStormAvg": "十級風平均暴風半徑", + "@meshtasticHardware": { + "description": "Board model" + }, + "restroomCategoryCommercial": "商業營業場所", + "@meshtasticAirtime": { + "description": "Share of airtime this radio transmitted" + }, + "aedRegion": "縣市區域", + "homeRainTrendLightStopping": "預計 {minutes} 分鐘後停止下小雨", + "reportDetailInfo": "詳細資訊", + "mapNavWind": "風向", + "@meshtasticReceived": { + "description": "Packets received this session" + }, + "windForecastOverlayMenuTooltip": "風場預報圖層選項", + "dataWeatherRankingSubtitle": "即時觀測排行", + "rainInterval6h": "6 時", + "homeRainTrendMinute": "{minute}分", + "restroomTypeUnspecified": "未設定", + "typhoonOverlayProbabilityHint": "會隱藏預測圓錐", "mapLayerSatelliteGlobalOutline": "國界", - "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", - "mapLayerSatelliteCloudClear": "晴空", - "mapLayerSatelliteCloudProbablyClear": "可能晴空", - "mapLayerSatelliteCloudProbablyCloudy": "可能有雲", + "mapNavTemperature": "溫度", + "typhoonLegendForecastPoint": "預測點", + "@meshtasticBattery": { + "description": "Battery charge" + }, + "reportListYesterday": "昨天", + "moreSectionLinks": "相關連結", + "feedOffline": "連線中斷", + "mapLayerStyleBd": "Dvorak BD", + "moreSectionDisplay": "顯示", + "rainInterval3d": "3 日", + "defaultMapLayerSubtitle": "開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。", + "aedDescription": "備註", + "typhoonOverlayWeatherRadarTooltip": "雷達回波(對齊颱風報文時間)", + "onboardingPermLocationDesc": "依你所在位置推送在地警報。", + "mapLayerSatelliteB16": "ひまわり 二氧化碳(B16)", + "@meshtasticClearMessages": { + "description": "Menu action clearing the message log" + }, + "homeActiveEventsEmpty": "目前沒有生效中的事件", + "typhoonLabelPosition": "中心位置", + "weatherRankingBy": "依", + "typhoonIntensityMild": "輕度颱風", + "windForecastGlobalOutlineHint": "各國國界外框", + "rainInterval1h": "1 時", + "eewLocalIntensity": "所在地預估", + "mapLayerRadar": "雷達合成回波圖", + "@radarScanRange": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "restroomCategoryReligious": "宗教禮儀場所", + "meshtasticRole": "角色", "mapLayerSatelliteCloudCloudy": "有雲", - "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,顯示底圖", - "mapLayerSatelliteTransparentReflectance": "低反射率/夜間 = 透明,顯示底圖", - "mapLayerSatelliteTransparentZero": "零差值 = 透明(無訊號)", - "mapLayerSatelliteTransparentNight": "夜間 = 透明,顯示底圖", - "mapLayerSatelliteTransparentNoData": "無資料(陸地) = 透明", - "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(無植被)", - "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(無水體)", - "mapLayerSatelliteTransparentClear": "晴空 = 透明,顯示底圖", + "skyTimeSunrise": "日出", + "@mapLayerMeshtasticSubtitle": { + "description": "Map layer switcher subtitle" + }, + "meshtasticNoMessages": "尚無訊息", + "onboardingPermNotifyDesc": "在地震、天氣與災害發生時,即時傳遞警報通知。", + "radarTownOutline": "鄉鎮界線", "mapLayerStyleSection": "顯示樣式", - "mapLayerStyleTooltip": "顯示樣式", - "mapLayerStyleGray": "灰階(JMA)", - "mapLayerStyleGrayTooltip": "氣象廳灰階慣例:溫度越低越白", - "mapLayerStyleJma": "雲頂強調(JMA)", - "mapLayerStyleJmaTooltip": "灰階為底,−40 °C 以下上色,凸顯雲頂高度", - "mapLayerStyleBd": "Dvorak BD", - "mapLayerStyleBdTooltip": "Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階", - "mapLayerQpesums": "未來 1 小時降水預報", - "mapLayerLightning": "閃電", - "lightningLegendCg": "對地 · {minutes} 分內", - "lightningLegendCc": "雲間 · {minutes} 分內", - "mapTimelineNow": "現在", - "mapTimelinePast": "歷史", - "mapTimelineFuture": "未來", - "mapTimelineObserved": "觀測", - "mapTimelineForecast": "預報", - "mapTimelineDataTime": "資料時間 {time}", - "notifySettingsMenu": "通知設定", - "notifyTitle": "通知", - "notifyUnavailable": "推播尚未就緒,請稍後再試。", - "notifySetFailed": "設定失敗,請稍後再試。", - "notifySectionEew": "地震速報", - "notifySectionEarthquake": "地震", - "notifySectionWeather": "天氣", - "notifySectionTsunami": "海嘯", + "@moonPhaseNew": { + "description": "Phase: new moon" + }, + "disasterMapOverlayMenuTooltip": "防災地圖圖層", + "moreGooglePlay": "Google Play", + "meshtasticOnline": "近期聽到", + "@meshtasticSendHint": { + "description": "Message input hint" + }, + "typhoonLabelSw": "西南側", + "typhoonForecastLead": "預測 +{hours} 小時", + "dpmDisasterTsunami": "海嘯", + "@mapAppOpenFailed": {}, + "changelogTypeStable": "正式", + "mapLayerSatelliteTransparentClear": "晴空 = 透明,顯示底圖", + "@skyTimeAuto": { + "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + }, + "@meshtasticBusyTitle": { + "description": "Another app holds the BLE link" + }, + "@windForecastCountyOutlineHint": { + "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + }, + "mapOverlaySectionReference": "參考圖層", + "mapLayerSatelliteB02": "ひまわり 可見光-綠(B02)", + "reportListLocalFelt": "小區域有感", + "weatherRankingEmpty": "目前沒有可排序的觀測", "notifySectionOther": "其他", - "notifyEew": "緊急地震速報", - "notifyMonitor": "強震監視器", - "notifyReport": "地震報告", - "notifyIntensity": "震度速報", - "notifyThunderstorm": "雷雨即時訊息", - "notifyAdvisory": "天氣警特報", - "notifyEvacuation": "防災資訊", - "notifyTsunami": "海嘯資訊", - "notifyAnnouncement": "公告", - "notifyOptOff": "關閉", - "notifyOptAll": "接收全部", + "weatherRankingMeta": "資料時間:{time}\n共 {count} 觀測點", + "onboardingTermsAgree": "我已閱讀並同意服務條款", + "mapLayerSatelliteTransparentNoVegetation": "< 0.1 = 透明(無植被)", "notifyOptLocalIntensity4": "所在地震度4以上", - "notifyOptLocalIntensity1": "所在地震度1以上", - "notifyOptWeatherLocal": "接收所在地", - "notifyOptTsunamiWarning": "只接收海嘯警報", - "notifyOptTsunamiAll": "海嘯消息、海嘯警報", - "onboardingNext": "下一步", - "onboardingBack": "上一步", + "eewArrived": "已抵達", + "meshtasticNoDevices": "找不到 Meshtastic 裝置", + "mapLayerCategoryLife": "生活", + "reportFilterSortIntensity": "震度", + "typhoonMotion": "移動", + "meshtasticStateDisconnected": "未連線", + "typhoonIntensityIntense": "強烈颱風", + "@meshtasticSend": { + "description": "Send message button" + }, + "mapLayerOrderTitle": "調整圖層順序", + "@skyTimeNoon": { + "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + }, + "@meshtasticShortName": { + "description": "The radio's short name" + }, + "dpmYes": "是", + "meshtasticNoHistory": "歷史紀錄還不夠", + "reportDetailLocalIntensityUnavailable": "沒有震度訊息", + "mapLayerWindForecastGfs": "GFS", + "reportListDepthUnit": "公里", + "reportFilterDepth": "深度", + "@meshtasticNoHistory": { + "description": "Chart placeholder before two samples exist" + }, "onboardingScrollHint": "往下捲動以繼續", - "onboardingIntroTitle": "歡迎使用 DPIP", + "@meshtasticRadio": { + "description": "Radio diagnostics sheet title" + }, + "mapNavQpesums": "預報", + "@meshtasticStateError": { + "description": "Connection state label" + }, + "@meshtasticVoltage": { + "description": "Battery voltage" + }, + "navMap": "地圖", + "notifyAdvisory": "天氣警特報", + "@meshtasticNoMessages": { + "description": "Empty message log while connected" + }, + "reportFilterReset": "重設", + "mapLayerSatelliteMndwi": "ひまわり 改良水體指數", + "typhoonOverlaySectionStorm": "暴風圈", + "moonPhaseFull": "滿月", + "@meshtasticEmptyMessage": { + "description": "Placeholder for a text packet with no body" + }, + "@radarGlobalOutlineHint": { + "description": "Hint under the national-border toggle in the radar overlay menu." + }, + "moonPhaseWaningGibbous": "虧凸月", + "weatherDynamicStateSubtitle": "覆寫主頁背景天氣", + "reportFilterIntensityInfoModernTitle": "新制(2020 起)", + "@mapAppGoogleMaps": {}, + "typhoonDataTime": "資料時間\n{time}", + "restroomTypeAccessible": "無障礙廁所", + "moreSectionAbout": "關於", + "meshtasticSelectDevice": "選擇裝置", "onboardingIntroBody": "DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷雨即時訊息、天氣警特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。", - "onboardingTermsTitle": "服務條款", - "onboardingTermsBody": "使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。", - "onboardingTermsAgree": "我已閱讀並同意服務條款", - "onboardingAgreeContinue": "同意並繼續", - "onboardingPermsTitle": "權限授權", - "onboardingPermsBody": "為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。", + "shelterCapacityLabel": "收容人數", + "reportDetailImage": "地震報告圖", + "meshtasticStateConfiguring": "設定中…", + "@moonPhaseLastQuarter": { + "description": "Phase: last quarter" + }, + "typhoonLabelGaleAvg": "七級風平均暴風半徑", "onboardingPermNotify": "通知", - "onboardingPermNotifyDesc": "在地震、天氣與災害發生時,即時傳遞警報通知。", - "onboardingPermCritical": "重大通知", - "onboardingPermCriticalDesc": "讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。", - "onboardingPermLocation": "定位", - "onboardingPermLocationDesc": "依你所在位置推送在地警報。", - "onboardingPermBackground": "背景定位", - "onboardingPermBackgroundDesc": "選擇「一律允許」,關閉 App 也能推送在地警報。", - "onboardingPermBattery": "省電白名單", - "onboardingPermBatteryDesc": "允許 DPIP 在背景持續運作,避免警報延遲或漏收。", - "onboardingGrant": "授權", - "onboardingGranted": "已授權", - "onboardingStart": "開始使用", - "language": "語言", - "languageSettings": "語言設定", - "languageSystem": "系統預設", - "locationBannerServiceOff": "定位服務已關閉,無法針對你的所在地推送警報。", - "locationBannerPermission": "尚未授權定位,無法針對你的所在地推送警報。", - "locationBannerFix": "開啟設定", - "notifyBannerDisabled": "通知已關閉,將收不到災害警報。", - "onboardingSkipTitle": "尚未完成授權", - "onboardingSkipBody": "未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。", - "onboardingSkipStay": "返回授權", - "onboardingSkipLeave": "仍要略過", - "moreYoutube": "YouTube", + "meshtasticClearMessages": "清除訊息", + "meshtasticNotifyMessages": "新訊息通知", + "defaultMapLayerSettings": "地圖預設圖層", + "moreSectionNotify": "通知", + "@moonPhaseFull": { + "description": "Phase: full moon" + }, + "notifyUnavailable": "推播尚未就緒,請稍後再試。", + "mapLayerOrderReset": "回復預設順序", + "dpmAddress": "地址", + "weatherRankingMergeCounty": "縣市", + "moreSectionApp": "取得 App", + "reportFilterIntensityInfoLegacyBody": "震度僅 0–7,沒有 5弱/5強/6弱/6強。", + "mapLayerSatelliteSst": "ひまわり 海表溫度", + "qpesumsOverlayMenuTooltip": "定量降水預報圖層選項", + "@skyTimeAfternoon": { + "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + }, + "mapTimelineFuture": "未來", + "typhoonLegendCircleAvg": "平均圓", + "reportFilterDepthKm": "{depth} 公里", + "typhoonLabelSe": "東南側", + "radarTownOutlineHint": "較細的分區", + "eewCountdown": "{seconds} 秒", + "@meshtasticDisconnect": { + "description": "Disconnect from the radio" + }, + "typhoonLabelGust": "瞬間最大陣風", + "mapAppGoogleMaps": "Google Maps", + "sponsorTerms": "使用條款", + "restroomTypeGenderNeutral": "性別友善廁所", + "@skyTimeDusk": { + "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + }, + "notifyThunderstorm": "雷雨即時訊息", + "skyTimeGolden": "黃金時刻", + "moonAge": "月齡", + "@windForecastTownOutlineHint": { + "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + }, + "meshtasticRadioSettings": "LoRa", + "weatherRankingAnalysisCurrent": "當下 {value}°C", + "@meshtasticNotifyMessages": { + "description": "Toggle: local notification for an incoming mesh message" + }, "moreGithub": "ExpTech GitHub", + "homeForecastUnavailable": "選擇鄉鎮後可查看預報", + "mapLayers": "圖層", + "meshtasticHardware": "硬體", + "languageSettings": "語言設定", + "dpmDisasterNuclear": "核子事故", + "@moonNextFullMoon": { + "description": "Next full moon date label" + }, + "language": "語言", + "homeForecastFeelsLike": "體感 {temp}°", + "typhoonOverlayWeatherHint": "對齊報文時間", + "@meshtasticHopLimit": { + "description": "How many hops a packet may take" + }, + "skyTimeDawn": "黎明", + "skyTimeAfternoon": "下午", + "meshtasticLastHeard": "最後聽到", + "typhoonWarningTitle": "颱風警報", "moreSourceCode": "原始碼", - "moreSectionApp": "取得 App", - "moreGooglePlay": "Google Play", - "moreAppStore": "App Store", - "displaySettings": "顯示設定", - "defaultMapLayerSettings": "地圖預設圖層", - "defaultMapLayerSubtitle": "開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。", - "mapNavRadar": "雷達", - "mapNavQpesums": "預報", - "mapNavSatellite": "衛星", - "mapNavLightning": "閃電", - "mapNavTyphoon": "颱風", + "mapLayerCategoryWeather": "氣象觀測", + "mapLayerSatelliteB09": "ひまわり 中層水氣(B09)", + "windForecastTownOutlineHint": "更細的網格", + "mapLayerSatelliteCloudmask": "ひまわり 雲遮罩", + "mapAppCopyCoordinates": "複製座標", + "reportFilterIntensityInfoIntro": "中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。", "mapNavEarthquake": "地震", - "mapNavTemperature": "溫度", - "mapNavHumidity": "濕度", - "mapNavPressure": "氣壓", - "mapNavWind": "風向", + "typhoonGust": "陣風", + "restroomGradeAverage": "普通級", + "@meshtasticNodes": { + "description": "Mesh nodes section header" + }, + "mapLayerSatelliteBtdCo2": "ひまわり 卷雲/雲高", + "onboardingPermBackgroundDesc": "選擇「一律允許」,關閉 App 也能推送在地警報。", + "mapTimelineForecast": "預報", + "restroomTypeLabel": "廁所類型", + "navEarthquake": "地震", + "typhoonOverlayStormL10Tooltip": "十級暴風圈+平均圓(黃色)", + "moonPhaseWaxingGibbous": "盈凸月", + "reportDetailTitle": "地震報告", + "moreTremReport": "TREM 檢知報告", + "weatherDataTime": "{station} ∙ 資料時間 {time}", + "meshtasticNoNodes": "尚未聽到任何節點", + "meshtasticViaMqtt": "經 MQTT(網際網路)", + "radarCountyOutline": "縣市界線", + "onboardingGranted": "已授權", + "@mapAppCopyCoordinates": {}, + "commonClose": "關閉", + "restroomGradeLabel": "等級", + "rainIntervalNow": "今日", + "changelogCurrentVersion": "目前版本", + "typhoonOverlayForecastCalloutsTooltip": "放大時顯示預測點詳細卡片", + "typhoonLabelPressure": "中心氣壓", + "aedOpenRemark": "開放時間備註", + "onboardingPermsBody": "為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。", + "typhoonOverlaySectionWeather": "天氣底圖", + "@meshtasticStateConnected": { + "description": "Connection state label" + }, + "notifyOptWeatherLocal": "接收所在地", "mapNavRain": "雨量", - "mapNavDisaster": "防災", - "displayTheme": "主題", + "moonDays": "天", + "mapLegendUnit": "單位:{unit}", + "weatherModeClear": "晴天", + "meshtasticRadio": "電台", + "commonEmpty": "沒有資料", + "mapLayerSatelliteB01": "ひまわり 可見光-藍(B01)", + "meshtasticExternalPower": "外部供電", + "moonPhaseLastQuarter": "下弦月", + "@meshtasticName": { + "description": "The radio's long name" + }, + "reportFilterOrderAsc": "升序", + "reportFilterApply": "套用", + "reportDetailImageUnavailable": "報告圖尚未提供", + "@weatherModeSand": { + "description": "Label for the weatherModeSand option in the experimental backdrop settings." + }, + "weatherRankingHighest": "最高", + "reportDetailReplay": "重播", + "mapLayerRestroom": "公廁", + "restroomCategoryWelfare": "社福機構、集會場所", + "restroomGradeExcellent": "特優級", + "meshtasticLastSent": "最近送出", + "meshtasticName": "名稱", + "meshtasticScan": "掃描", + "@radarOverlayMenuTooltip": { + "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + }, + "mapLayerCategoryForecast": "數值預報", + "meshtasticChannelFailed": "無法設定 DPIP 頻道", "themeSystem": "跟隨系統", - "themeLight": "淺色", - "themeDark": "深色", - "moreSectionAbout": "關於", - "termsOfService": "服務條款", - "faq": "常見問題", - "openSourceLicenses": "引用套件", - "sponsorTitle": "支持 DPIP", - "sponsorIntro": "DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。", - "sponsorSubscriptions": "訂閱制", - "sponsorRecommended": "推薦", - "sponsorOneTime": "單次支援", - "sponsorPerMonth": "{price} / 月", - "sponsorRestore": "恢復購買", - "sponsorTerms": "使用條款", - "sponsorPrivacy": "隱私權政策", - "sponsorRestoring": "正在恢復購買…", - "sponsorRestoreUnavailable": "無法連線至商店,請稍後再試", - "commonClose": "關閉", + "mapLayerSatelliteNdvi": "ひまわり 植生指數", + "typhoonLegendForecast": "預測路徑", + "typhoonValueHpa": "{n} 百帕", + "weatherPrecipitation": "降水量", + "moonNextFullMoon": "下次滿月", + "dpmSheetEmpty": "點選地圖上的標記查看詳情", + "onboardingSkipLeave": "仍要略過", + "onboardingBack": "上一步", + "aedPlaceDesc": "放置位置說明", + "@weatherModeOvercast": { + "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + }, + "onboardingSkipTitle": "尚未完成授權", + "restroomTypeFamily": "親子廁所", + "typhoonValueKm": "{n} 公里", + "@radarCountyOutlineSubtitle": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "typhoonPressure": "氣壓", + "@meshtasticCopied": { + "description": "Toast shown after copying a message" + }, + "onboardingPermBattery": "省電白名單", + "typhoonLabelNw": "西北側", + "dpmDisasterFlood": "水災", + "moonPhaseWaxingCrescent": "眉月", + "restroomCategoryLeisure": "休閒娛樂場所", "mapLayerTemperature": "溫度", - "trendRange24h": "24 小時", - "trendRange7d": "7 天", - "trendNoData": "沒有趨勢資料", - "trendCumulativeTotal": "累計 {total} mm", - "chartHourLabel": "{hour}時", - "mapLayerHumidity": "濕度", - "mapLayerPressure": "氣壓", + "aedCategory": "場所分類", + "@moonTimelineCaption": { + "description": "Moon phase timeline caption" + }, + "meshtasticChannels": "頻道", + "monitorWaiting": "等待資料…", + "typhoonOverlayForecastCallouts": "預測點資訊", + "@meshtasticTitle": { + "description": "Meshtastic test page title" + }, + "reportDetailEpicenter": "震央座標", + "meshtasticVoltage": "電壓", + "mapLayerMeshtasticSubtitle": "電台聽到過的 LoRa 網狀網路節點", + "@meshtasticSent": { + "description": "Packets sent this session" + }, "mapLayerWind": "風向", - "mapLayerWindForecastEcmwf": "ECMWF", - "mapLayerWindForecastGfs": "GFS", - "mapLayerRain": "雨量", - "rainIntervalMenu": "累積時段", - "rainIntervalNow": "今日", - "rainInterval10m": "10 分", - "rainInterval1h": "1 時", - "rainInterval3h": "3 時", - "rainInterval6h": "6 時", + "reportDetailMagnitude": "地震規模", + "@meshtasticRole": { + "description": "Device role (client, router...)" + }, + "reportDetailAreaIntensity": "各地震度", "rainInterval12h": "12 時", - "rainInterval24h": "24 時", - "rainInterval2d": "2 日", - "rainInterval3d": "3 日", - "mapLayerTyphoon": "颱風", - "typhoonNoActive": "目前無颱風", - "typhoonWind": "風速", - "typhoonGust": "陣風", - "typhoonPressure": "氣壓", - "typhoonMotion": "移動", - "typhoonLabelPosition": "中心位置", - "typhoonLabelDirection": "過去移動方向", + "reportListMagnitude": "M{magnitude}", + "dpmDisasterLandslide": "土石流", + "notifyMonitor": "強震監視器", + "onboardingStart": "開始使用", + "@meshtasticExternalPower": { + "description": "Battery value when mains powered" + }, + "@skyTime": { + "description": "Label for the experimental sky time-of-day override." + }, + "sponsorPerMonth": "{price} / 月", + "mapLayerPressure": "氣壓", + "@radarTownOutlineSubtitle": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "mapLayerSatelliteB04": "ひまわり 近紅外(B04)", + "mapLayerSatelliteTransparentZero": "零差值 = 透明(無訊號)", + "shelterIndoorLabel": "室內收容", + "notifyOptOff": "關閉", + "reportFilterSortTime": "時間", + "mapLayerSatelliteCloudProbablyClear": "可能晴空", + "weatherModeThunderstorm": "雷雨", + "homeViewOnMap": "前往地圖察看", + "reportFilterIntensityInfoLegacyTitle": "舊制(2020 以前)", "typhoonLabelSpeed": "過去移動時速", - "typhoonLabelPressure": "中心氣壓", - "typhoonLabelWind": "近中心最大風速", - "typhoonLabelGust": "瞬間最大陣風", - "typhoonLabelGaleAvg": "七級風平均暴風半徑", - "typhoonLabelStormAvg": "十級風平均暴風半徑", - "typhoonLabelProbCircle": "70%機率圓", - "typhoonForecastLead": "預測 +{hours} 小時", - "typhoonLabelNw": "西北側", - "typhoonLabelNe": "東北側", - "typhoonLabelSw": "西南側", - "typhoonLabelSe": "東南側", - "typhoonValueLat": "北緯 {lat} 度", - "typhoonValueLon": "東經 {lon} 度", - "typhoonValueKm": "{n} 公里", - "typhoonValueHpa": "{n} 百帕", + "@meshtasticReconnecting": { + "description": "The link dropped and is being re-established" + }, + "mapAppOpenFailed": "無法開啟 {app}", + "mapLayerSatelliteRgbComposite": "RGB 合成(JMA 配方)", + "@meshtasticStateDisconnected": { + "description": "Connection state label" + }, + "meshtasticReceived": "已接收", + "weatherRankingExtremeLow": "今日最低", + "@meshtasticRegionSwitch": { + "description": "Button applying the DPIP LoRa region" + }, + "mapLayerSatelliteB10": "ひまわり 低層水氣(B10)", + "mapLayerSatelliteCloudProbablyCloudy": "可能有雲", + "shelterCategoryLabel": "適用災害", + "mapLayerSatelliteTransparentNoWater": "≤ 0 = 透明(無水體)", + "meshtasticStateConnecting": "連線中…", + "moonTitle": "月亮", + "weatherRankingGust": "陣風", + "moreAppStore": "App Store", + "@meshtasticUndecoded": { + "description": "Packets the radio could not decrypt" + }, + "dpmFilterSectionShelter": "避難所災害類型", + "@commonCancel": { + "description": "Dismisses a dialog without acting" + }, + "moreServerStatus": "伺服器狀態", + "notifySectionWeather": "天氣", + "meshtasticPreset": "調變預設", + "dataSectionSeismic": "地震", + "changelogBodyEmpty": "此版本沒有說明。", + "radarGlobalOutline": "國界", + "notifyEew": "緊急地震速報", + "regionNationwide": "全國", + "moreNotifyLog": "DPIP 通知發送記錄", + "regionCurrent": "所在地", + "dpmFilterSectionRestroom": "場所類型", + "meshtasticNotConnected": "尚未連線至裝置", + "weatherModeSnow": "下雪", + "mapLayerMeshtastic": "Meshtastic 節點", + "moreDeveloper": "除錯資訊", + "@qpesumsOverlayMenuTooltip": { + "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + }, + "mapLayerSatelliteB14": "ひまわり 長波紅外線(B14)", + "meshtasticChannelUse": "頻道使用率", + "mapNavLightning": "閃電", + "homeForecastEmpty": "目前沒有預報資料", + "sponsorOneTime": "單次支援", + "mapLayerSatelliteBtdSplit": "ひまわり 分割視窗", + "onboardingPermBackground": "背景定位", + "aedEmergencyPhone": "緊急聯絡電話", + "dpmOpenInMaps": "開啟地圖", + "meshtasticNotifyNodes": "新節點通知", + "onboardingPermCriticalDesc": "讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。", + "@mapAppDefault": { + "placeholders": { + "app": { + "type": "String" + } + } + }, + "mapLayerSatelliteTransparentWarm": "晴空(暖端) = 透明,顯示底圖", + "meshtasticSent": "已送出", + "homeForecastTitle": "24小時預報", + "typhoonLegendWarningAreas": "警報區域", + "meshtasticExcludeMqttHidden": "已隱藏 {count} 個", + "notifyOptLocalIntensity1": "所在地震度1以上", + "@skyTimeGolden": { + "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + }, + "@meshtasticChannelReady": { + "description": "The DPIP channel exists on the radio" + }, + "mapTimelinePast": "歷史", + "restroomTypeFemale": "女廁所", + "reportListToday": "今天", + "meshtasticTapNode": "點選節點查看詳細資訊", + "commonLoading": "載入中…", + "@meshtasticStateConnecting": { + "description": "Connection state label" + }, + "typhoonIntensityModerate": "中度颱風", + "typhoonWind": "風速", + "mapLayerSatelliteAsh": "ひまわり 火山灰", + "rainInterval3h": "3 時", + "reportListSearch": "查詢", + "meshtasticChannelReady": "DPIP 頻道已就緒", + "@meshtasticNotifyNodes": { + "description": "Toggle: local notification when a new node is heard" + }, + "mapLayerCategorySatellite": "衛星", + "reportFilterLocation": "地點", + "mapLayerSatelliteNightmicrophysics": "ひまわり 夜間微物理", + "typhoonIntensityTd": "熱帶性低氣壓", + "reportFilterDate": "日期", + "sponsorRestoreUnavailable": "無法連線至商店,請稍後再試", + "homeForecastPop": "{pop}%", + "regionEmpty": "尚未新增常用地區", + "@radarScanRangeSubtitle": { + "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + }, + "@moonAge": { + "description": "Moon age label" + }, + "onboardingPermBatteryDesc": "允許 DPIP 在背景持續運作,避免警報延遲或漏收。", + "mapNavDisaster": "防災", + "radarScanRangeSubtitle": "標示四座雷達實際觀測到的範圍。", + "aedHoursSunday": "週日開放時間", + "reportDetailOriginTime": "發震時間", + "trendNoData": "沒有趨勢資料", + "onboardingPermLocation": "定位", + "moreDiscord": "Discord 社群", + "mapNavPressure": "氣壓", + "mapLayerSatelliteB13": "ひまわり 紅外線(B13)", + "typhoonTdNo": "TD {no}", + "changelogEmpty": "目前沒有更新日誌", + "@skyTimeDawn": { + "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + }, + "@meshtasticViaMqtt": { + "description": "Legend: node reported over an MQTT bridge" + }, + "reportFilterDateStartNote": "開始日:當日 00:00(臺北時間)", + "eewTitle": "地震速報", + "mapLayerWindForecastEcmwf": "ECMWF", + "@@locale": "zh_TW", + "regionSelectCount": "已選 {count}/{max}", + "@meshtasticRegionMismatch": { + "description": "Radio is on another LoRa region than DPIP needs", + "placeholders": { + "region": { + "type": "String" + } + } + }, + "mapLayerSatelliteBtdSo2": "ひまわり 二氧化硫/雲相", + "meshtasticStateError": "錯誤", + "weatherModeOvercast": "陰天", + "@meshtasticScan": { + "description": "Start scanning for Meshtastic radios" + }, + "reportDetailDepth": "震源深度", + "typhoonOverlayWarningTooltip": "標示警報區域縣市", + "reportFilterDatePick": "選擇日期", + "onboardingSkipStay": "返回授權", + "@moonPhaseWaxingCrescent": { + "description": "Phase: waxing crescent" + }, + "@meshtasticOnline": { + "description": "Legend: node heard within the online window" + }, + "commonFetchFailed": "無法獲取資料,請稍後重試", + "@meshtasticTxPower": { + "description": "Transmit power" + }, + "shelterOutdoorLabel": "室外收容", + "meshtasticStateConnected": "已連線", + "mapNavRadar": "雷達", + "mapLayerSatelliteCloudClear": "晴空", + "eewSummary": "規模 {magnitude}・深度 {depth} 公里", + "locationBannerPermission": "尚未授權定位,無法針對你的所在地推送警報。", + "typhoonOverlayWeatherNoneTooltip": "不疊雷達或紅外線", + "radarCountyOutlineHint": "畫在回波之上", + "windForecastCountyOutlineHint": "繪製於風場之上", + "homeRainTrendTitle": "近 1 小時降水趨勢", + "moonPhaseFirstQuarter": "上弦月", + "mapLayerCategoryTyphoon": "颱風", + "@windForecastOverlayMenuTooltip": { + "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + }, + "@meshtasticNodeId": { + "description": "The radio's node number" + }, + "meshtasticUtilization": "空中工時(24 小時)", + "restroomTypeMixed": "混合廁所", + "restroomGradeGood": "優等級", + "notifyTsunami": "海嘯資訊", + "navData": "資料", + "mapLayerSatelliteBtdWvirw": "ひまわり 過衝雲頂", + "meshtasticReadingAge": "數值時間", + "@moonPhaseWaningGibbous": { + "description": "Phase: waning gibbous" + }, + "mapAppCallFailed": "此裝置無法撥打電話", + "@meshtasticPower": { + "description": "Section: battery and uptime" + }, + "reportFilterAny": "不限", + "weatherRankingMergeTo": "合併至", + "notifyIntensity": "震度速報", + "typhoonTimeChip": "{day}日{hour}時", + "rainIntervalMenu": "累積時段", + "reportDetailLocalFelt": "小區域有感地震", + "meshtasticDevice": "裝置", + "onboardingGrant": "授權", + "weatherModeRain": "雨天", + "shelterVulnerableOkLabel": "適合避難弱者安置", + "stationSheetEmpty": "點選任一測站查看觀測值", + "typhoonLegendProbability": "侵襲機率", + "@meshtasticExcludeMqtt": { + "description": "Toggle hiding internet-bridged nodes" + }, + "@radarScanRangeHint": { + "description": "Hint under the radar scan-range toggle in the radar overlay menu." + }, + "reportFilterMagnitude": "規模", + "skyTimeMorning": "上午", + "@meshtasticNoDevices": { + "description": "Empty scan result" + }, + "experimentalFeatures": "實驗性功能", + "onboardingTermsBody": "使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。", + "reportFilterTitle": "篩選", + "onboardingPermCritical": "重大通知", + "trendCumulativeTotal": "累計 {total} mm", + "languageName": "繁體中文(臺灣)", + "reportListEmptyFiltered": "沒有符合條件的地震報告", + "meshtasticExcludeMqtt": "隱藏 MQTT 節點", + "mapNavTyphoon": "颱風", + "weatherModeSand": "沙塵", + "@moonPhaseFirstQuarter": { + "description": "Phase: first quarter" + }, + "typhoonSatelliteTitle": "衛星雲圖", + "@dpmOpenInMaps": {}, + "notifyReport": "地震報告", + "mapAppCoordinatesCopied": "已複製座標", + "skyTimeNight": "夜晚", + "sponsorRecommended": "推薦", + "mapLayerSatelliteB15": "ひまわり 長波紅外線(B15)", + "weatherRankingWind": "風速", + "feedStale": "資料可能已過期", + "homeForecastWind": "{direction} · {level} 級", + "navHome": "首頁", + "meshtasticRegionLabel": "地區", + "mapLayerSatelliteCloudtop": "ひまわり 雲頂溫度", + "moonTimelineCaption": "月相", + "reportListMeta": "M{magnitude} · {depth} 公里", + "@meshtasticChannelNoSlot": { + "description": "Every secondary channel slot is taken" + }, + "@meshtasticBusyBody": { + "description": "Why two clients on one radio is a problem" + }, + "openSourceLicenses": "引用套件", + "weatherRankingLowest": "最低", + "@meshtasticConnectAnyway": { + "description": "Connect despite the other app" + }, + "reportFilterSortDepth": "深度", + "mapTimelineDataTime": "資料時間 {time}", + "radarScanRange": "顯示掃描範圍", + "meshtasticHopLimit": "跳數上限", + "@meshtasticUptime": { + "description": "Time since the radio booted" + }, + "weatherRankingAnalysisRange": "溫差 {value}°C", + "weatherRankingExtremeHigh": "今日最高", + "@meshtasticUtilization": { + "description": "Section title for the 24h airtime chart" + }, + "changelogVersionDetails": "版本資訊", + "sponsorPrivacy": "隱私權政策", + "reportDetailLocalIntensity": "所在地的震度", + "mapLayerSatelliteNaturalcolor": "ひまわり 自然色", + "meshtasticAirtime": "發射佔空比", + "shelterCapacityValue": "{n} 人", + "lightningLegendCc": "雲間 · {minutes} 分內", + "meshtasticSendHint": "要廣播的訊息", + "monitorDelay": "延遲 {value} s", + "@meshtasticFirmware": { + "description": "Firmware version" + }, + "dpmNo": "否", + "mapLayerSatelliteB08": "ひまわり 上層水氣(B08)", + "meshtasticReconnecting": "重新連線中…", + "@mapAppAppleMaps": {}, + "@meshtasticReadingAge": { + "description": "How old the battery/airtime numbers are" + }, + "radarTownOutlineSubtitle": "讓鄉鎮界線在雷達回波下仍然清楚。", + "@moonPhaseWaxingGibbous": { + "description": "Phase: waxing gibbous" + }, + "typhoonOverlayWeatherSatelliteTooltip": "紅外線(對齊颱風報文時間)", + "radarScanRangeHint": "框外空白代表未觀測", + "typhoonPickerTd": "熱帶性低氣壓 TD {no}", + "mapLayerSatelliteWatervapor": "ひまわり 水氣", + "regionAddButton": "新增地區", + "displaySettings": "顯示設定", + "restroomGradePoor": "不合格", + "restroomCategoryTourist": "觀光地區及風景區", + "locationBannerServiceOff": "定位服務已關閉,無法針對你的所在地推送警報。", + "mapLayerStyleTooltip": "顯示樣式", + "lightningLegendCg": "對地 · {minutes} 分內", + "skyTimeAuto": "自動", + "appLogs": "App 日誌", + "feedConnecting": "連線中…", + "notifyBannerDisabled": "通知已關閉,將收不到災害警報。", + "@meshtasticNoNodes": { + "description": "Empty node list" + }, + "weatherHumidity": "濕度", "typhoonValueMs": "每秒 {n} 公尺", - "typhoonDataTime": "資料時間\n{time}", - "mapLayerMonitor": "強震監視器", - "mapLayerAed": "AED", + "homeForecastHumidity": "濕度 {value}%", + "meshtasticBusyBody": "請先在另一個 Meshtastic App 中斷線。兩個 App 同時連同一台裝置會互相搶走訊息,導致部分訊息遺失。", + "meshtasticChannelNoSlot": "沒有可用的頻道空位 — 請先在裝置上空出一個", + "restroomCategoryTransport": "交通", + "reportFilterLocationHint": "例如:花蓮、東部海域", + "moonSubtitle": "月相與亮度 — 完全本地計算", + "meshtasticBattery": "電量", + "meshtasticDistance": "距離", + "meshtasticSnrTrend": "訊號趨勢 (SNR)", + "meshtasticBatteryTrend": "電量趨勢", + "typhoonOverlayMenuTooltip": "颱風圖層選項", + "mapLayerSatelliteBtdOzone": "ひまわり 對流層頂", + "meshtasticRegionMismatch": "裝置地區為 {region} — DPIP 需要 TW", + "notifySectionEarthquake": "地震", "mapLayerDisasterMap": "防災地圖", - "disasterMapOverlayMenuTooltip": "防災地圖圖層", - "disasterMapOverlaySectionLayers": "圖層", - "disasterMapOverlayAedTooltip": "顯示 AED 位置", + "weatherModeFog": "大霧", + "typhoonPickerNamed": "{name} TY {no}", + "mapLayerStyleGrayTooltip": "氣象廳灰階慣例:溫度越低越白", + "moreAnnouncements": "公告", + "mapLayerSatelliteTransparentNoData": "無資料(陸地) = 透明", + "@meshtasticScanning": { + "description": "Scan in progress" + }, + "restroomCategoryGovernment": "民眾洽公場所", + "typhoonLegendCurrent": "目前中心", "aedAddress": "地址", - "aedRegion": "縣市區域", - "aedCategory": "場所分類", - "aedType": "場所類型", - "aedPlaceDesc": "放置位置說明", - "aedDescription": "備註", - "aedHoursWeekday": "平日開放時間", - "aedHoursSaturday": "週六開放時間", - "aedHoursSunday": "週日開放時間", - "aedOpenRemark": "開放時間備註", - "aedEmergencyPhone": "緊急聯絡電話", - "mapLayerRestroom": "公廁", + "mapLayerAed": "AED", + "changelogTypePrerelease": "公測", + "reportFilterIntensityInfoModernBody": "震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。", + "typhoonOverlayWeatherNone": "無", + "mapLayerStyleGray": "灰階(JMA)", + "weatherModeAuto": "自動", + "typhoonLabelProbCircle": "70%機率圓", + "@radarCountyOutline": { + "description": "County-border overlay toggle in the map's radar overlay menu." + }, + "notifyOptAll": "接收全部", + "displayTheme": "主題", + "mapLayerSatelliteB07": "ひまわり 短波紅外(B07)", + "@skyTimeSunrise": { + "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + }, + "typhoonLabelDirection": "過去移動方向", + "@meshtasticLastSent": { + "description": "Age of the last sent packet" + }, + "regionManageTitle": "常用地區", + "typhoonLegendCone": "預測圓錐", + "moreCwaEew": "中央氣象署強震即時警報", + "onboardingPermsTitle": "權限授權", + "mapLayerStyleJma": "雲頂強調(JMA)", + "rainInterval10m": "10 分", + "weatherRankingAnalysisLow": "最低 {value}", + "meshtasticConnectAnyway": "仍要連線", + "reportListDayCount": "{count}", + "mapLayerSatelliteB06": "ひまわり 近紅外(B06)", + "mapLayerSatelliteTransparentReflectance": "低反射率/夜間 = 透明,顯示底圖", + "chartHourLabel": "{hour}時", "mapLayerShelter": "避難收容場所", - "disasterMapOverlayRestroomTooltip": "顯示公廁", + "typhoonOverlayProbabilityTooltip": "顯示侵襲機率(會隱藏預測圓錐)", + "mapLayerSatelliteNdwi": "ひまわり 水體指數", "disasterMapOverlayShelterTooltip": "顯示避難收容場所", - "dpmOpenInMaps": "開啟地圖", - "@dpmOpenInMaps": { + "mapNavHumidity": "濕度", + "@meshtasticTraffic": { + "description": "Section: packet counters" + }, + "reportDetailSortByIntensity": "依震度排序", + "homeRainTrendNoData": "無資料", + "mapLayerCategoryRadar": "雷達", + "meshtasticShortName": "簡稱", + "@meshtasticStateConfiguring": { + "description": "Connection state label" + }, + "mapLayerSatelliteAirmass": "ひまわり 氣團", + "@meshtasticPreset": { + "description": "LoRa modem preset" + }, + "typhoonTrackDetail": "路徑詳情", + "dataSectionWeather": "氣象", + "aedHoursWeekday": "平日開放時間", + "homeActiveEventsTitle": "生效中事件", + "weatherRankingAnalysisHigh": "最高 {value}", + "faq": "常見問題", + "typhoonHistoryLive": "即時", + "eewSerial": "第 {serial} 報", + "@radarTownOutline": { + "description": "Township-border overlay toggle in the map's radar overlay menu." + }, + "reportFilterSort": "排序方式", + "@skyTimeMorning": { + "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + }, + "meshtasticRegionConfirm": "要將這台裝置切換為 TW 地區嗎?裝置會重新啟動並短暫斷線,上面的其他頻道也會一起改變。", + "dataEarthquakeSubtitle": "地震報告", + "typhoonNoActive": "目前無颱風", + "@meshtasticExcludeMqttHidden": { + "description": "How many nodes the filter is hiding", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapLayerSatelliteB11": "ひまわり 二氧化硫/雲相(B11)", + "navEvents": "事件", + "onboardingTermsTitle": "服務條款", + "@meshtasticChannels": { + "description": "Section: the radio's channel table" + }, + "mapTownLabels": "鄉鎮名稱", + "notifySetFailed": "設定失敗,請稍後再試。", + "meshtasticDisconnect": "斷線", + "meshtasticUndecoded": "無法解密", + "notifyAnnouncement": "公告", + "onboardingIntroTitle": "歡迎使用 DPIP", + "regionCurrentUnavailable": "無法取得所在地位置資訊", + "languageSystem": "系統預設", + "skyTimeSunset": "日落", + "mapLayerSatelliteDust": "ひまわり 沙塵", + "mapAppAppleMaps": "Apple Maps", + "regionEdit": "修改", + "weatherDynamicState": "天氣動態狀態", + "mapPlaceholderDisabled": "地圖(暫時停用)", + "moonNow": "現在", + "@moonNow": { + "description": "Returns the moon page to the present moment" + }, + "moonSectionAppearance": "外觀", + "@moonSectionAppearance": { + "description": "Section header: how the Moon looks at the chosen moment" + }, + "moonSectionRiseSet": "月出月沒", + "@moonSectionRiseSet": { + "description": "Section header: moonrise and moonset for the user's township" + }, + "moonSectionUpcoming": "接下來", + "@moonSectionUpcoming": { + "description": "Section header: the next full and new moons" + }, + "moonSectionCalendar": "月曆", + "@moonSectionCalendar": { + "description": "Section header: the month-at-a-glance phase calendar" + }, + "moonDistance": "距離", + "@moonDistance": { + "description": "Earth-Moon centre-to-centre distance" + }, + "moonKilometres": "公里", + "@moonKilometres": { + "description": "Unit suffix for the lunar distance" + }, + "moonApparentSize": "視直徑", + "@moonApparentSize": { + "description": "The Moon's apparent angular diameter" + }, + "moonRise": "月出", + "@moonRise": { + "description": "Time the Moon rises" + }, + "moonSet": "月沒", + "@moonSet": { + "description": "Time the Moon sets" + }, + "moonNextNewMoon": "下次新月", + "@moonNextNewMoon": { + "description": "Date and time of the next new moon" + }, + "moonAlwaysUp": "整日在地平線上", + "@moonAlwaysUp": { + "description": "Shown when the Moon neither rises nor sets and stays above the horizon" + }, + "moonNoEvent": "當日無", + "@moonNoEvent": { + "description": "Shown when a calendar day has no moonrise or no moonset" + }, + "sunTitle": "太陽", + "@sunTitle": { + "description": "Sun page title" + }, + "sunSubtitle": "日出日沒、曙暮光與節氣", + "@sunSubtitle": { + "description": "Sun page one-line summary on the data hub" + }, + "sunSectionDaylight": "日照", + "@sunSectionDaylight": { + "description": "Section header: sunrise, noon, sunset, day length" + }, + "sunSectionTwilight": "曙暮光", + "@sunSectionTwilight": { + "description": "Section header: the three twilight bands" + }, + "sunSectionLight": "光線", + "@sunSectionLight": { + "description": "Section header: golden and blue hour" + }, + "sunSectionSundial": "日晷", + "@sunSectionSundial": { + "description": "Section header: equation of time and the next solar term" + }, + "sunSectionTerms": "節氣", + "@sunSectionTerms": { + "description": "Section header: the year's twenty-four solar terms" + }, + "sunRise": "日出", + "@sunRise": { + "description": "Time the Sun rises" + }, + "sunSet": "日沒", + "@sunSet": { + "description": "Time the Sun sets" + }, + "sunNoon": "正午", + "@sunNoon": { + "description": "Solar noon, the Sun's upper transit" + }, + "sunDayLength": "白晝長度", + "@sunDayLength": { + "description": "How long the Sun is above the horizon, as hours:minutes" + }, + "sunTwilightCivil": "民用", + "@sunTwilightCivil": { + "description": "Civil twilight, the Sun 6 degrees below the horizon" + }, + "sunTwilightNautical": "航海", + "@sunTwilightNautical": { + "description": "Nautical twilight, 12 degrees below" + }, + "sunTwilightAstronomical": "天文", + "@sunTwilightAstronomical": { + "description": "Astronomical twilight, 18 degrees below" + }, + "sunGoldenHourMorning": "晨間黃金時刻", + "@sunGoldenHourMorning": { + "description": "Morning golden hour span" + }, + "sunGoldenHourEvening": "昏間黃金時刻", + "@sunGoldenHourEvening": { + "description": "Evening golden hour span" + }, + "sunBlueHour": "藍調時刻", + "@sunBlueHour": { + "description": "Blue hour span after sunset" + }, + "sunEquationOfTime": "均時差", + "@sunEquationOfTime": { + "description": "Apparent solar time minus mean solar time" + }, + "sunMinutes": "分", + "@sunMinutes": { + "description": "Unit suffix for the equation of time" + }, + "solarTermNext": "下一個節氣", + "@solarTermNext": { + "description": "The next of the twenty-four solar terms" + }, + "planetsTitle": "行星", + "@planetsTitle": { + "description": "Planets page title" + }, + "planetsSubtitle": "今晚在哪、有多亮", + "@planetsSubtitle": { + "description": "Planets page one-line summary on the data hub" + }, + "planetsSectionTonight": "此刻", + "@planetsSectionTonight": { + "description": "Section header: the planets right now" + }, + "planetUp": "地平線上", + "@planetUp": { + "description": "Badge: the planet is above the horizon" + }, + "planetDown": "地平線下", + "@planetDown": { + "description": "Badge: the planet is below the horizon" + }, + "planetInGlare": "太近太陽", + "@planetInGlare": { + "description": "Badge: too close to the Sun to be seen" + }, + "planetMagnitude": "亮度", + "@planetMagnitude": { + "description": "Apparent visual magnitude" + }, + "planetElongation": "距日距角", + "@planetElongation": { + "description": "Angular distance from the Sun" + }, + "planetSky": "時段", + "@planetSky": { + "description": "Label for whether the planet is an evening or morning object" + }, + "planetEvening": "昏星", + "@planetEvening": { + "description": "Sets after the Sun, so visible in the evening" + }, + "planetMorning": "晨星", + "@planetMorning": { + "description": "Rises before the Sun, so visible before dawn" + }, + "planetDistance": "距離", + "@planetDistance": { + "description": "Distance from the Earth" + }, + "planetAu": "天文單位", + "@planetAu": { + "description": "Unit suffix: astronomical units" + }, + "planetAltitude": "仰角", + "@planetAltitude": { + "description": "Height above the horizon right now" + }, + "planetMercury": "水星", + "@planetMercury": { + "description": "Planet name" + }, + "planetVenus": "金星", + "@planetVenus": { + "description": "Planet name" + }, + "planetMars": "火星", + "@planetMars": { + "description": "Planet name" + }, + "planetJupiter": "木星", + "@planetJupiter": { + "description": "Planet name" + }, + "planetSaturn": "土星", + "@planetSaturn": { + "description": "Planet name" + }, + "planetUranus": "天王星", + "@planetUranus": { + "description": "Planet name" + }, + "planetNeptune": "海王星", + "@planetNeptune": { + "description": "Planet name" + }, + "solarTermVernalEquinox": "春分", + "@solarTermVernalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermPureBrightness": "清明", + "@solarTermPureBrightness": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainRain": "穀雨", + "@solarTermGrainRain": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSummer": "立夏", + "@solarTermStartOfSummer": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainFull": "小滿", + "@solarTermGrainFull": { + "description": "One of the twenty-four solar terms" + }, + "solarTermGrainInEar": "芒種", + "@solarTermGrainInEar": { + "description": "One of the twenty-four solar terms" + }, + "solarTermSummerSolstice": "夏至", + "@solarTermSummerSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorHeat": "小暑", + "@solarTermMinorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorHeat": "大暑", + "@solarTermMajorHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfAutumn": "立秋", + "@solarTermStartOfAutumn": { + "description": "One of the twenty-four solar terms" + }, + "solarTermEndOfHeat": "處暑", + "@solarTermEndOfHeat": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWhiteDew": "白露", + "@solarTermWhiteDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAutumnalEquinox": "秋分", + "@solarTermAutumnalEquinox": { + "description": "One of the twenty-four solar terms" + }, + "solarTermColdDew": "寒露", + "@solarTermColdDew": { + "description": "One of the twenty-four solar terms" + }, + "solarTermFrostDescent": "霜降", + "@solarTermFrostDescent": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfWinter": "立冬", + "@solarTermStartOfWinter": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorSnow": "小雪", + "@solarTermMinorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorSnow": "大雪", + "@solarTermMajorSnow": { + "description": "One of the twenty-four solar terms" + }, + "solarTermWinterSolstice": "冬至", + "@solarTermWinterSolstice": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMinorCold": "小寒", + "@solarTermMinorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermMajorCold": "大寒", + "@solarTermMajorCold": { + "description": "One of the twenty-four solar terms" + }, + "solarTermStartOfSpring": "立春", + "@solarTermStartOfSpring": { + "description": "One of the twenty-four solar terms" + }, + "solarTermRainWater": "雨水", + "@solarTermRainWater": { + "description": "One of the twenty-four solar terms" + }, + "solarTermAwakeningOfInsects": "驚蟄", + "@solarTermAwakeningOfInsects": { + "description": "One of the twenty-four solar terms" + }, + "tonightTitle": "今夜", + "@tonightTitle": { + "description": "Tonight page title" + }, + "tonightSubtitle": "現在看得到什麼、什麼時候", + "@tonightSubtitle": { + "description": "Tonight page summary on the data hub" + }, + "tonightSectionDark": "觀測窗口", + "@tonightSectionDark": { + "description": "Section header: the observing window" + }, + "tonightAstronomicalNight": "天文夜", + "@tonightAstronomicalNight": { + "description": "Dusk to dawn with the Sun 18 degrees down" + }, + "tonightNeverDark": "整夜不全暗", + "@tonightNeverDark": { + "description": "Shown when the Sun never gets 18 degrees below the horizon" + }, + "tonightDarkWindow": "暗窗", + "@tonightDarkWindow": { + "description": "The longest stretch with no Sun and no Moon" + }, + "tonightMoonAllNight": "月亮整夜在天上", + "@tonightMoonAllNight": { + "description": "Shown when the Moon is up for the whole night" + }, + "tonightDarkTotal": "總暗時", + "@tonightDarkTotal": { + "description": "Total dark time, hours:minutes" + }, + "tonightMoonlight": "月光", + "@tonightMoonlight": { + "description": "The Moon's illuminated fraction tonight" + }, + "tonightSectionShowers": "流星雨", + "@tonightSectionShowers": { + "description": "Section header: meteor showers running now" + }, + "tonightRadiantDown": "輻射點不升起", + "@tonightRadiantDown": { + "description": "The shower's radiant never rises here" + }, + "tonightPerHour": "顆/時", + "@tonightPerHour": { + "description": "Unit: meteors per hour" + }, + "tonightSectionSatellites": "衛星過境", + "@tonightSectionSatellites": { + "description": "Section header: visible satellite passes" + }, + "tonightSectionTargets": "此刻可觀測目標", + "@tonightSectionTargets": { + "description": "Section header: deep-sky objects high enough to observe" + }, + "showerQuadrantids": "象限儀座", + "@showerQuadrantids": { + "description": "Meteor shower name" + }, + "showerLyrids": "天琴座", + "@showerLyrids": { + "description": "Meteor shower name" + }, + "showerEtaAquariids": "寶瓶座η", + "@showerEtaAquariids": { + "description": "Meteor shower name" + }, + "showerDeltaAquariids": "寶瓶座δ", + "@showerDeltaAquariids": { + "description": "Meteor shower name" }, - "mapAppGoogleMaps": "Google Maps", - "@mapAppGoogleMaps": { + "showerPerseids": "英仙座", + "@showerPerseids": { + "description": "Meteor shower name" }, - "mapAppAppleMaps": "Apple Maps", - "@mapAppAppleMaps": { + "showerOrionids": "獵戶座", + "@showerOrionids": { + "description": "Meteor shower name" }, - "mapAppDefault": "{app}(預設)", - "@mapAppDefault": { - "placeholders": { - "app": {"type": "String"} - } + "showerSouthernTaurids": "金牛座南", + "@showerSouthernTaurids": { + "description": "Meteor shower name" }, - "mapAppCopyCoordinates": "複製座標", - "@mapAppCopyCoordinates": { + "showerLeonids": "獅子座", + "@showerLeonids": { + "description": "Meteor shower name" }, - "mapAppCoordinatesCopied": "已複製座標", - "@mapAppCoordinatesCopied": { + "showerGeminids": "雙子座", + "@showerGeminids": { + "description": "Meteor shower name" }, - "mapAppOpenFailed": "無法開啟 {app}", - "@mapAppOpenFailed": { + "showerUrsids": "小熊座", + "@showerUrsids": { + "description": "Meteor shower name" }, - - "mapAppCallFailed": "此裝置無法撥打電話", - - "mapOverlaySectionReference": "參考圖層", - "mapLayerCategoryEarthquake": "地震", - "mapLayerCategoryTyphoon": "颱風", - "mapLayerCategoryWeather": "氣象觀測", - "mapLayerCategorySatellite": "衛星", - "mapLayerCategoryRadar": "雷達", - "mapLayerCategoryLife": "生活", - "mapLayerCategoryForecast": "數值預報", "mapOverlaySectionMap": "地圖", - "rainIntervalSection": "統計時間", - - "mapTownLabels": "鄉鎮名稱", - "mapTownLabelsHint": "放大時顯示鄉鎮名稱", - - "mapTerrainRelief": "地形立體感", - "mapTerrainReliefHint": "在底圖上顯示立體地形陰影", - - "dpmSheetEmpty": "點選地圖上的標記查看詳情", - "dpmAddress": "地址", - "restroomTypeLabel": "廁所類型", - "restroomCategoryLabel": "類別", - "restroomGradeLabel": "等級", - "restroomTypeFemale": "女廁所", - "restroomTypeMale": "男廁所", - "restroomTypeMixed": "混合廁所", - "restroomTypeAccessible": "無障礙廁所", - "restroomTypeGenderNeutral": "性別友善廁所", - "restroomTypeFamily": "親子廁所", - "restroomTypeUnspecified": "未設定", - "restroomCategoryTransport": "交通", - "restroomCategoryPark": "公園", - "restroomCategoryCommercial": "商業營業場所", - "restroomCategoryReligious": "宗教禮儀場所", - "restroomCategoryCultural": "文化育樂活動場所", - "restroomCategoryGovernment": "民眾洽公場所", - "restroomCategoryWelfare": "社福機構、集會場所", - "restroomCategoryTourist": "觀光地區及風景區", - "restroomCategoryLeisure": "休閒娛樂場所", - "restroomCategoryOther": "其他", - "restroomGradeExcellent": "特優級", - "restroomGradeGood": "優等級", - "restroomGradeAverage": "普通級", - "restroomGradePoor": "不合格", - "shelterAddressLabel": "地址", - "shelterCapacityLabel": "收容人數", - "shelterCapacityValue": "{n} 人", - "shelterCategoryLabel": "適用災害", - "shelterIndoorLabel": "室內收容", - "shelterOutdoorLabel": "室外收容", - "shelterVulnerableOkLabel": "適合避難弱者安置", - "dpmYes": "是", - "dpmNo": "否", - "stationSheetEmpty": "點選任一測站查看觀測值", - "monitorDelay": "延遲 {value} s", - "monitorWaiting": "等待資料…", - "mapLegendUnit": "單位:{unit}", - "typhoonLegendPast": "實際路徑", - "typhoonIntensityTd": "熱帶性低氣壓", - "typhoonPickerNamed": "{name} TY {no}", - "typhoonPickerTd": "熱帶性低氣壓 TD {no}", - "typhoonTyNo": "TY {no}", - "typhoonTdNo": "TD {no}", - "typhoonIntensityMild": "輕度颱風", - "typhoonIntensityModerate": "中度颱風", - "typhoonIntensityIntense": "強烈颱風", - "typhoonLegendForecast": "預測路徑", - "typhoonLegendForecastPoint": "預測點", - "typhoonLegendCurrent": "目前中心", - "typhoonLegendCone": "預測圓錐", - "mapLegendExpand": "圖例", - "mapLegendCollapse": "收合圖例", - "mapMyLocation": "我的位置", - "mapResetNorth": "回到北方", - "typhoonLegendCircle15": "七級風暴風圈", - "typhoonLegendCircleAvg": "平均圓", - "typhoonLegendCircle25": "十級風暴風圈", - "typhoonStormRadii": "東北 {ne} · 東南 {se} · 西南 {sw} · 西北 {nw} km", - "typhoonTimeChip": "{day}日{hour}時", - "typhoonLegendProbability": "侵襲機率", - "typhoonLegendWarningAreas": "警報區域", - "typhoonOverlayMenuTooltip": "颱風圖層選項", - "typhoonOverlaySectionStorm": "暴風圈", - "typhoonOverlaySectionExtra": "覆蓋層", - "typhoonOverlayStormBandSubtitle": "含平均圓", - "typhoonOverlayProbabilityHint": "會隱藏預測圓錐", - "typhoonOverlayProbabilityTooltip": "顯示侵襲機率(會隱藏預測圓錐)", - "typhoonOverlayWarningTooltip": "標示警報區域縣市", - "typhoonOverlayStormL7Tooltip": "七級暴風圈+平均圓(紫色)", - "typhoonOverlayStormL10Tooltip": "十級暴風圈+平均圓(黃色)", - "typhoonOverlaySectionWeather": "天氣底圖", - "typhoonOverlayWeatherNone": "無", - "typhoonOverlayWeatherHint": "對齊報文時間", - "typhoonOverlayWeatherNoneTooltip": "不疊雷達或紅外線", - "typhoonOverlayWeatherRadarTooltip": "雷達回波(對齊颱風報文時間)", - "typhoonOverlayWeatherSatelliteTooltip": "紅外線(對齊颱風報文時間)", - "typhoonWarningTitle": "颱風警報", - "typhoonWarningAreas": "警戒區域:{areas}", - "typhoonTrackDetail": "路徑詳情", - "typhoonHistoryTitle": "資料時間", - "typhoonHistoryLive": "即時", - "typhoonSatelliteTitle": "衛星雲圖", - "typhoonOverlayForecastCallouts": "預測點資訊", - "typhoonOverlayForecastCalloutsTooltip": "放大時顯示預測點詳細卡片", - "dpmFilterSectionRestroom": "場所類型", - "dpmFilterSectionRestroomType": "廁所類型", - "dpmFilterSectionShelter": "避難所災害類型", - "dpmDisasterFlood": "水災", - "dpmDisasterEarthquake": "震災", - "dpmDisasterLandslide": "土石流", - "dpmDisasterTsunami": "海嘯", - "dpmDisasterSlope": "坡地災害", - "dpmDisasterNuclear": "核子事故", - "skyTime": "天空時間", - "@skyTime": { - "description": "Label for the experimental sky time-of-day override." + "deepSkyOpenCluster": "疏散星團", + "@deepSkyOpenCluster": { + "description": "Deep-sky object type" }, - "skyTimeAuto": "自動", - "@skyTimeAuto": { - "description": "Label for the skyTimeAuto option in the experimental backdrop settings." + "deepSkyGlobularCluster": "球狀星團", + "@deepSkyGlobularCluster": { + "description": "Deep-sky object type" }, - "skyTimeDawn": "黎明", - "@skyTimeDawn": { - "description": "Label for the skyTimeDawn option in the experimental backdrop settings." + "deepSkySpiralGalaxy": "螺旋星系", + "@deepSkySpiralGalaxy": { + "description": "Deep-sky object type" }, - "skyTimeSunrise": "日出", - "@skyTimeSunrise": { - "description": "Label for the skyTimeSunrise option in the experimental backdrop settings." + "deepSkyEllipticalGalaxy": "橢圓星系", + "@deepSkyEllipticalGalaxy": { + "description": "Deep-sky object type" }, - "skyTimeMorning": "上午", - "@skyTimeMorning": { - "description": "Label for the skyTimeMorning option in the experimental backdrop settings." + "deepSkyIrregularGalaxy": "不規則星系", + "@deepSkyIrregularGalaxy": { + "description": "Deep-sky object type" }, - "skyTimeNoon": "正午", - "@skyTimeNoon": { - "description": "Label for the skyTimeNoon option in the experimental backdrop settings." + "deepSkyPlanetaryNebula": "行星狀星雲", + "@deepSkyPlanetaryNebula": { + "description": "Deep-sky object type" }, - "skyTimeAfternoon": "下午", - "@skyTimeAfternoon": { - "description": "Label for the skyTimeAfternoon option in the experimental backdrop settings." + "deepSkySupernovaRemnant": "超新星遺跡", + "@deepSkySupernovaRemnant": { + "description": "Deep-sky object type" }, - "skyTimeGolden": "黃金時刻", - "@skyTimeGolden": { - "description": "Label for the skyTimeGolden option in the experimental backdrop settings." + "deepSkyEmissionNebula": "發射星雲", + "@deepSkyEmissionNebula": { + "description": "Deep-sky object type" }, - "skyTimeSunset": "日落", - "@skyTimeSunset": { - "description": "Label for the skyTimeSunset option in the experimental backdrop settings." + "deepSkyReflectionNebula": "反射星雲", + "@deepSkyReflectionNebula": { + "description": "Deep-sky object type" }, - "skyTimeDusk": "暮色", - "@skyTimeDusk": { - "description": "Label for the skyTimeDusk option in the experimental backdrop settings." + "deepSkyAsterism": "星群", + "@deepSkyAsterism": { + "description": "Deep-sky object type: a star pattern, not a single object" }, - "skyTimeNight": "夜晚", - "@skyTimeNight": { - "description": "Label for the skyTimeNight option in the experimental backdrop settings." + "almanacTitle": "曆法", + "@almanacTitle": { + "description": "Almanac page title" }, - "weatherModeCloudy": "多雲", - "@weatherModeCloudy": { - "description": "Label for the weatherModeCloudy option in the experimental backdrop settings." + "almanacSubtitle": "農曆日期與未來的日月食", + "@almanacSubtitle": { + "description": "Almanac page summary on the data hub" }, - "weatherModeOvercast": "陰天", - "@weatherModeOvercast": { - "description": "Label for the weatherModeOvercast option in the experimental backdrop settings." + "almanacSectionToday": "今日", + "@almanacSectionToday": { + "description": "Section header: today's date in both calendars" }, - "weatherModeSnow": "下雪", - "@weatherModeSnow": { - "description": "Label for the weatherModeSnow option in the experimental backdrop settings." + "almanacGregorian": "西曆", + "@almanacGregorian": { + "description": "The Gregorian date" }, - "weatherModeSand": "沙塵", - "@weatherModeSand": { - "description": "Label for the weatherModeSand option in the experimental backdrop settings." + "almanacLunar": "農曆", + "@almanacLunar": { + "description": "The lunisolar date" }, - "radarScanRange": "顯示掃描範圍", - "@radarScanRange": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "almanacYear": "歲次", + "@almanacYear": { + "description": "The sexagenary year and its zodiac animal" }, - "radarScanRangeSubtitle": "標示四座雷達實際觀測到的範圍。", - "@radarScanRangeSubtitle": { - "description": "Radar scan-range overlay toggle in the map's radar overlay menu." + "almanacMonthLength": "月大小", + "@almanacMonthLength": { + "description": "Whether this lunar month has 29 or 30 days" }, - "radarScanRangeHint": "框外空白代表未觀測", - "@radarScanRangeHint": { - "description": "Hint under the radar scan-range toggle in the radar overlay menu." + "almanacLongMonth": "三十日", + "@almanacLongMonth": { + "description": "A 30-day lunar month" }, - "radarOverlayMenuTooltip": "雷達圖層選項", - "@radarOverlayMenuTooltip": { - "description": "Tooltip for the radar overlay-options chip beside the layer switcher" + "almanacShortMonth": "二十九日", + "@almanacShortMonth": { + "description": "A 29-day lunar month" }, - "radarCountyOutline": "縣市界線", - "@radarCountyOutline": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "almanacLeapPrefix": "閏", + "@almanacLeapPrefix": { + "description": "Prefix marking an intercalary lunar month" }, - "radarGlobalOutline": "國界", - "@radarGlobalOutline": { - "description": "World-country-border overlay toggle in the map's reference-layer overlay menus." + "almanacSectionLunarEclipses": "月食", + "@almanacSectionLunarEclipses": { + "description": "Section header: upcoming lunar eclipses" }, - "radarGlobalOutlineHint": "各國國界外框", - "@radarGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the radar overlay menu." + "almanacSectionSolarEclipses": "日食", + "@almanacSectionSolarEclipses": { + "description": "Section header: solar eclipses visible from here" }, - "radarCountyOutlineHint": "畫在回波之上", - "@radarCountyOutlineHint": { - "description": "Hint under the county-border toggle in the radar overlay menu." + "almanacNoSolarEclipse": "範圍內無", + "@almanacNoSolarEclipse": { + "description": "No solar eclipse is visible from here in the search window" }, - "radarCountyOutlineSubtitle": "讓縣市界線在雷達回波下仍然清楚。", - "@radarCountyOutlineSubtitle": { - "description": "County-border overlay toggle in the map's radar overlay menu." + "eclipseTotal": "全食", + "@eclipseTotal": { + "description": "Eclipse type" }, - "radarTownOutline": "鄉鎮界線", - "@radarTownOutline": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "eclipsePartial": "偏食", + "@eclipsePartial": { + "description": "Eclipse type" }, - "radarTownOutlineHint": "較細的分區", - "@radarTownOutlineHint": { - "description": "Hint under the township-border toggle in the radar overlay menu." + "eclipseAnnular": "環食", + "@eclipseAnnular": { + "description": "Eclipse type: a ring of Sun remains" }, - "radarTownOutlineSubtitle": "讓鄉鎮界線在雷達回波下仍然清楚。", - "@radarTownOutlineSubtitle": { - "description": "Township-border overlay toggle in the map's radar overlay menu." + "eclipsePenumbral": "半影食", + "@eclipsePenumbral": { + "description": "Eclipse type: the Moon only enters the outer shadow" }, - "qpesumsOverlayMenuTooltip": "定量降水預報圖層選項", - "@qpesumsOverlayMenuTooltip": { - "description": "Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher." + "zodiacRat": "鼠", + "@zodiacRat": { + "description": "Chinese zodiac animal" }, - "windForecastOverlayMenuTooltip": "風場預報圖層選項", - "@windForecastOverlayMenuTooltip": { - "description": "Tooltip for the wind-forecast overlay-options chip beside the layer switcher." + "zodiacOx": "牛", + "@zodiacOx": { + "description": "Chinese zodiac animal" }, - "windForecastCountyOutlineHint": "繪製於風場之上", - "@windForecastCountyOutlineHint": { - "description": "Hint under the county-border toggle in the wind-forecast overlay menu." + "zodiacTiger": "虎", + "@zodiacTiger": { + "description": "Chinese zodiac animal" }, - "windForecastGlobalOutlineHint": "各國國界外框", - "@windForecastGlobalOutlineHint": { - "description": "Hint under the national-border toggle in the wind-forecast overlay menu." + "zodiacRabbit": "兔", + "@zodiacRabbit": { + "description": "Chinese zodiac animal" }, - "windForecastTownOutlineHint": "更細的網格", - "@windForecastTownOutlineHint": { - "description": "Hint under the township-border toggle in the wind-forecast overlay menu." + "zodiacDragon": "龍", + "@zodiacDragon": { + "description": "Chinese zodiac animal" }, - "eewSerial": "第 {serial} 報", - "eewMaxIntensity": "最大震度", - "eewLocalIntensity": "所在地預估", - "eewSWave": "震波", - "eewArrived": "已抵達", - "eewCountdown": "{seconds} 秒" + "zodiacSnake": "蛇", + "@zodiacSnake": { + "description": "Chinese zodiac animal" + }, + "zodiacHorse": "馬", + "@zodiacHorse": { + "description": "Chinese zodiac animal" + }, + "zodiacGoat": "羊", + "@zodiacGoat": { + "description": "Chinese zodiac animal" + }, + "zodiacMonkey": "猴", + "@zodiacMonkey": { + "description": "Chinese zodiac animal" + }, + "zodiacRooster": "雞", + "@zodiacRooster": { + "description": "Chinese zodiac animal" + }, + "zodiacDog": "狗", + "@zodiacDog": { + "description": "Chinese zodiac animal" + }, + "zodiacPig": "豬", + "@zodiacPig": { + "description": "Chinese zodiac animal" + }, + "tideTitle": "潮汐", + "@tideTitle": { + "description": "Tide page title" + }, + "tideSubtitle": "大潮、小潮與月球引力", + "@tideSubtitle": { + "description": "Tide page summary on the data hub" + }, + "tideDisclaimer": "僅為天文引潮力,非港口潮汐表。水位請參考氣象署公布之潮汐預報。", + "@tideDisclaimer": { + "description": "Says plainly that this is the astronomical forcing, not a harbour tide table" + }, + "tideSectionNow": "此刻", + "@tideSectionNow": { + "description": "Section header: the tide-raising force right now" + }, + "tidePhase": "週期", + "@tidePhase": { + "description": "Where in the spring-neap cycle the tide sits" + }, + "tideSpring": "大潮", + "@tideSpring": { + "description": "Spring tide: Sun and Moon aligned" + }, + "tideNeap": "小潮", + "@tideNeap": { + "description": "Neap tide: Sun and Moon at right angles" + }, + "tideMiddling": "中潮", + "@tideMiddling": { + "description": "Between spring and neap" + }, + "tideLunarDistanceFactor": "月球引力", + "@tideLunarDistanceFactor": { + "description": "How much stronger the Moon's pull is than at mean distance" + }, + "tideEquilibrium": "平衡潮高", + "@tideEquilibrium": { + "description": "The equilibrium tide height" + }, + "tideMetres": "公尺", + "@tideMetres": { + "description": "Unit: metres" + }, + "tidePerigeanSpring": "下次近地點大潮", + "@tidePerigeanSpring": { + "description": "The next spring tide at lunar perigee - the highest water" + }, + "tideSectionTurningPoints": "轉折點", + "@tideSectionTurningPoints": { + "description": "Section header: when the forcing peaks and troughs" + }, + "tideHigh": "高", + "@tideHigh": { + "description": "A high point of the tidal forcing" + }, + "tideLow": "低", + "@tideLow": { + "description": "A low point of the tidal forcing" + }, + "skyChartTitle": "星圖", + "@skyChartTitle": { + "description": "Sky chart page title" + }, + "skyChartSubtitle": "頭頂上肉眼可見的天空", + "@skyChartSubtitle": { + "description": "Sky chart page summary on the data hub" + }, + "skyChartNorth": "北", + "@skyChartNorth": { + "description": "Compass point on the sky chart" + }, + "skyChartEast": "東", + "@skyChartEast": { + "description": "Compass point on the sky chart" + }, + "skyChartSouth": "南", + "@skyChartSouth": { + "description": "Compass point on the sky chart" + }, + "skyChartWest": "西", + "@skyChartWest": { + "description": "Compass point on the sky chart" + }, + "tonightElementAge": "軌道資料 {days} 天前", + "@tonightElementAge": { + "description": "How old the bundled satellite element set is, in days", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "almanacLunarDate": "{leap}{month} 月 {day} 日", + "@almanacLunarDate": { + "description": "A lunisolar date: an optional leap marker, the month and the day", + "placeholders": { + "leap": { + "type": "String" + }, + "month": { + "type": "int" + }, + "day": { + "type": "int" + } + } + }, + "tonightNoShowers": "目前無流星雨", + "@tonightNoShowers": { + "description": "Shown when no meteor shower is running today" + }, + "tonightNoPasses": "48 小時內無可見過境", + "@tonightNoPasses": { + "description": "Shown when no satellite pass is visible in the next two days" + }, + "tonightSatellitesUnavailable": "無法讀取軌道資料", + "@tonightSatellitesUnavailable": { + "description": "Shown when the bundled element set could not be read" + }, + "tonightNoTargets": "無足夠高度的目標", + "@tonightNoTargets": { + "description": "Shown when nothing in the catalogue is high enough tonight" + }, + "skyChartUnavailable": "無法讀取星表", + "@skyChartUnavailable": { + "description": "Shown when the bundled star catalogue could not be read" + } } diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index b3eac7c78..0bc31f653 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -117,923 +117,929 @@ abstract class AppLocalizations { Locale('zh', 'TW'), ]; - /// This language's own name, shown in the in-app language picker. Each locale's ARB names itself; the picker is built from these, never a hardcoded list. + /// No description provided for @typhoonValueLat. /// /// In en, this message translates to: - /// **'English'** - String get languageName; + /// **'{lat}°N'** + String typhoonValueLat(String lat); - /// Bottom-nav label and page title for the Home tab + /// Body of the skip-permissions confirmation dialog /// /// In en, this message translates to: - /// **'Home'** - String get navHome; + /// **'Without location and notifications, DPIP can\'t alert you to earthquakes and disasters near you in real time. You can still grant them later in Settings.'** + String get onboardingSkipBody; - /// Bottom-nav label and page title for the Events tab + /// No description provided for @rainInterval24h. /// /// In en, this message translates to: - /// **'Events'** - String get navEvents; + /// **'24 h'** + String get rainInterval24h; - /// Bottom-nav label and page title for the Map tab + /// Home rain trend subtitle: heavy rain forecast to stop partway through the hour /// /// In en, this message translates to: - /// **'Map'** - String get navMap; + /// **'Heavy rain likely to stop in {minutes} minutes'** + String homeRainTrendHeavyStopping(int minutes); - /// Bottom-nav label and page title for the Data hub tab + /// Label above the map timeline's date (the radar observation time), e.g. Observed / 2026/07/14 /// /// In en, this message translates to: - /// **'Data'** - String get navData; + /// **'Observed'** + String get mapTimelineObserved; - /// Earthquake report catalogue title (entry under the Data hub) + /// Title of the region picker (city list) page /// /// In en, this message translates to: - /// **'Earthquake'** - String get navEarthquake; + /// **'Select a region'** + String get regionSelectTitle; - /// Section header on the Data hub for earthquake-related entries + /// Label for the skyTimeNoon option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Seismic'** - String get dataSectionSeismic; + /// **'Noon'** + String get skyTimeNoon; - /// Subtitle under the Earthquake tile on the Data hub + /// County-border overlay toggle in the map's radar overlay menu. /// /// In en, this message translates to: - /// **'Earthquake reports'** - String get dataEarthquakeSubtitle; + /// **'Keeps county borders legible under the radar echo.'** + String get radarCountyOutlineSubtitle; - /// Section header on the Data hub for weather observation rankings + /// Filter section title in the disaster-map sheet: restroom toilet-kind categories /// /// In en, this message translates to: - /// **'Weather'** - String get dataSectionWeather; + /// **'Toilet types'** + String get dpmFilterSectionRestroomType; - /// Subtitle under weather ranking tiles on the Data hub + /// Himawari visible-red channel (B03, 0.64 µm) layer name /// /// In en, this message translates to: - /// **'Live station rankings'** - String get dataWeatherRankingSubtitle; + /// **'Himawari Red (B03)'** + String get mapLayerSatelliteB03; - /// App bar title for the weather station ranking page + /// Label for the felt-intensity range filter /// /// In en, this message translates to: - /// **'Observation rankings'** - String get weatherRankingTitle; + /// **'Intensity'** + String get reportFilterIntensity; - /// Snapshot time and station count above a ranking list + /// Map layer switcher label for the lightning strike timeline /// /// In en, this message translates to: - /// **'Data time: {time}\n{count} stations'** - String weatherRankingMeta(String time, int count); + /// **'Lightning'** + String get mapLayerLightning; - /// Empty state when a ranking list has no rows after filters + /// Restroom type: male restroom /// /// In en, this message translates to: - /// **'No observations to rank'** - String get weatherRankingEmpty; + /// **'Male'** + String get restroomTypeMale; - /// Label before highest/lowest (or desc/asc) chips on ranking + /// Age of the last received packet /// /// In en, this message translates to: - /// **'Sort by'** - String get weatherRankingBy; + /// **'Last received'** + String get meshtasticLastReceived; - /// Chip to rank temperature descending + /// Tooltip on the area-intensity sort toggle when tapping it switches to an alphabetical county list /// /// In en, this message translates to: - /// **'Highest'** - String get weatherRankingHighest; + /// **'Sort by county'** + String get reportDetailSortByCounty; - /// Chip to rank temperature ascending + /// Home rain trend subtitle: peak intensity below the light-rain threshold /// /// In en, this message translates to: - /// **'Lowest'** - String get weatherRankingLowest; + /// **'Light showers possible'** + String get homeRainTrendScattered; - /// Label before township/county merge chips on ranking + /// Time since the radio booted /// /// In en, this message translates to: - /// **'Merge to'** - String get weatherRankingMergeTo; + /// **'Uptime'** + String get meshtasticUptime; - /// Chip to keep one extreme station per township + /// Ranking tab for recorded daily high/low/range (not current temp) /// /// In en, this message translates to: - /// **'Township'** - String get weatherRankingMergeTown; + /// **'Daily extremes'** + String get weatherRankingTempExtremes; - /// Chip to keep one extreme station per county + /// Theme option: always light /// /// In en, this message translates to: - /// **'County'** - String get weatherRankingMergeCounty; + /// **'Light'** + String get themeLight; - /// Ranking tab/tile for sustained wind speed + /// Hint under the terrain-relief setting /// /// In en, this message translates to: - /// **'Wind speed'** - String get weatherRankingWind; + /// **'Show shaded terrain relief on the base map'** + String get mapTerrainReliefHint; - /// Ranking tab/tile for peak gust speed + /// Placeholder for a text packet with no body /// /// In en, this message translates to: - /// **'Gust'** - String get weatherRankingGust; + /// **'(empty message)'** + String get meshtasticEmptyMessage; - /// Ranking tab for recorded daily high/low/range (not current temp) + /// Section header on the More page for saved regions /// /// In en, this message translates to: - /// **'Daily extremes'** - String get weatherRankingTempExtremes; + /// **'Region'** + String get moreSectionRegion; - /// Chip to rank by recorded daily maximum temperature + /// Shelter disaster-type filter chip: earthquake /// /// In en, this message translates to: - /// **'Daily high'** - String get weatherRankingExtremeHigh; + /// **'Earthquake'** + String get dpmDisasterEarthquake; - /// Chip to rank by recorded daily minimum temperature + /// Name of the Himawari infrared layer in the layer picker /// /// In en, this message translates to: - /// **'Daily low'** - String get weatherRankingExtremeLow; + /// **'Himawari Infrared (B13)'** + String get mapLayerSatellite; - /// Chip to rank by daily high minus low + /// AED Saturday opening hours row label /// /// In en, this message translates to: - /// **'Diurnal range'** - String get weatherRankingExtremeRange; + /// **'Saturday hours'** + String get aedHoursSaturday; - /// Occurrence time for a gust or daily extreme + /// Shelter disaster-type filter chip: slope hazard /// /// In en, this message translates to: - /// **'Recorded at {time}'** - String weatherRankingRecordedAt(String time); + /// **'Slope hazard'** + String get dpmDisasterSlope; - /// Current temperature fragment in an extremes analysis line + /// Phase: new moon /// /// In en, this message translates to: - /// **'Now {value}°C'** - String weatherRankingAnalysisCurrent(String value); + /// **'New moon'** + String get moonPhaseNew; - /// Daily high fragment; value may include clock time + /// Notify page section header /// /// In en, this message translates to: - /// **'High {value}'** - String weatherRankingAnalysisHigh(String value); + /// **'Earthquake early warning'** + String get notifySectionEew; - /// Daily low fragment; value may include clock time + /// Map compass tooltip: re-points the camera to north-up /// /// In en, this message translates to: - /// **'Low {value}'** - String weatherRankingAnalysisLow(String value); + /// **'Reset north'** + String get mapResetNorth; - /// Diurnal range fragment in an extremes analysis line + /// No description provided for @rainInterval2d. /// /// In en, this message translates to: - /// **'Range {value}°C'** - String weatherRankingAnalysisRange(String value); + /// **'2 d'** + String get rainInterval2d; - /// Empty state when the report catalogue has no rows + /// Hint under the township-names setting /// /// In en, this message translates to: - /// **'No earthquake reports'** - String get reportListEmpty; + /// **'Show township names when zoomed in'** + String get mapTownLabelsHint; - /// Empty state when active filters yield no report rows + /// Dismisses a dialog without acting /// /// In en, this message translates to: - /// **'No earthquake reports match these filters'** - String get reportListEmptyFiltered; + /// **'Cancel'** + String get commonCancel; - /// Magnitude and depth line on a report list row + /// Notify option label /// /// In en, this message translates to: - /// **'M{magnitude} · {depth} km'** - String reportListMeta(String magnitude, String depth); + /// **'Tsunami warnings only'** + String get notifyOptTsunamiWarning; - /// Emphasized magnitude on a report list row + /// Himawari night fog / low-cloud brightness-temperature-difference layer name /// /// In en, this message translates to: - /// **'M{magnitude}'** - String reportListMagnitude(String magnitude); + /// **'Himawari Night Fog'** + String get mapLayerSatelliteBtdFog; - /// Depth unit label beside the depth value on a report list row + /// Section header on the More page grouping advanced/developer entries /// /// In en, this message translates to: - /// **'km'** - String get reportListDepthUnit; + /// **'Advanced'** + String get moreSectionAdvanced; - /// Label for …000 serial reports (small-area felt quake, no CWA number) + /// Chip to rank by daily high minus low /// /// In en, this message translates to: - /// **'Local felt'** - String get reportListLocalFelt; + /// **'Diurnal range'** + String get weatherRankingExtremeRange; - /// Date section header for reports that originated today (Taipei) + /// More-menu entry that opens the notification-settings page /// /// In en, this message translates to: - /// **'Today'** - String get reportListToday; + /// **'Notification settings'** + String get notifySettingsMenu; - /// Date section header for reports that originated yesterday (Taipei) + /// Typhoon UI: typhoonHistoryTitle /// /// In en, this message translates to: - /// **'Yesterday'** - String get reportListYesterday; + /// **'Dataset time'** + String get typhoonHistoryTitle; - /// Number of reports in a day section + /// Choice-sheet label suffix marking the platform home map app, with the app name /// /// In en, this message translates to: - /// **'{count}'** - String reportListDayCount(int count); + /// **'{app} (default)'** + String mapAppDefault(String app); - /// Footer when the report catalogue has no further pages + /// Trend chart range toggle: last 24 hours /// /// In en, this message translates to: - /// **'End of list'** - String get reportListEnd; + /// **'24h'** + String get trendRange24h; - /// Title of the earthquake report filter sheet + /// Explains the JMA cloud-top enhancement band rendering /// /// In en, this message translates to: - /// **'Filters'** - String get reportFilterTitle; + /// **'Grayscale base, tinted below −40 °C to highlight cloud-top height'** + String get mapLayerStyleJmaTooltip; - /// Section title for report list sort field + order + /// Occurrence time for a gust or daily extreme /// /// In en, this message translates to: - /// **'Sort'** - String get reportFilterSort; + /// **'Recorded at {time}'** + String weatherRankingRecordedAt(String time); - /// Sort reports by origin time + /// Map layer switcher label for the rainfall station layer /// /// In en, this message translates to: - /// **'Time'** - String get reportFilterSortTime; + /// **'Rainfall'** + String get mapLayerRain; - /// Sort reports by max intensity + /// Name of the QPESUMS next-1-hour precipitation forecast layer in the layer picker /// /// In en, this message translates to: - /// **'Intensity'** - String get reportFilterSortIntensity; + /// **'1h Precipitation Forecast'** + String get mapLayerQpesums; - /// Sort reports by magnitude + /// Section title in map overlay settings menus: base-map settings /// /// In en, this message translates to: - /// **'Magnitude'** - String get reportFilterSortMagnitude; + /// **'Map'** + String get mapOverlaySectionMap; - /// Sort reports by hypocentral depth + /// Map setting: show the base map's hillshade relief /// /// In en, this message translates to: - /// **'Depth'** - String get reportFilterSortDepth; + /// **'Terrain relief'** + String get mapTerrainRelief; - /// Sort order: newest / largest first + /// Label for an EEW alert's maximum felt intensity badge /// /// In en, this message translates to: - /// **'Descending'** - String get reportFilterOrderDesc; + /// **'Max intensity'** + String get eewMaxIntensity; - /// Sort order: oldest / smallest first + /// Tooltip on the control that collapses the map legend /// /// In en, this message translates to: - /// **'Ascending'** - String get reportFilterOrderAsc; + /// **'Hide legend'** + String get mapLegendCollapse; - /// Label for the felt-intensity range filter + /// More-menu entry and page title for GitHub release notes /// /// In en, this message translates to: - /// **'Intensity'** - String get reportFilterIntensity; + /// **'Changelog'** + String get changelogTitle; - /// Title of the dialog explaining CWA 新制 vs 舊制 intensity + /// Sort order: newest / largest first /// /// In en, this message translates to: - /// **'Intensity scales'** - String get reportFilterIntensityInfoTitle; + /// **'Descending'** + String get reportFilterOrderDesc; - /// Intro paragraph for the intensity-scale info dialog + /// What an MQTT node is /// /// In en, this message translates to: - /// **'CWA changed the felt-intensity scale on 1 Jan 2020 (Taipei time).'** - String get reportFilterIntensityInfoIntro; + /// **'Nodes bridged over the internet, not heard by radio'** + String get meshtasticExcludeMqttSubtitle; - /// No description provided for @reportFilterIntensityInfoLegacyTitle. + /// Title of the dialog explaining CWA 新制 vs 舊制 intensity /// /// In en, this message translates to: - /// **'Legacy (before 2020)'** - String get reportFilterIntensityInfoLegacyTitle; + /// **'Intensity scales'** + String get reportFilterIntensityInfoTitle; - /// No description provided for @reportFilterIntensityInfoLegacyBody. + /// Layer-switcher label for the typhoon map layer /// /// In en, this message translates to: - /// **'Only levels 0–7. No 5− / 5+ / 6− / 6+ split.'** - String get reportFilterIntensityInfoLegacyBody; + /// **'Typhoon'** + String get mapLayerTyphoon; - /// No description provided for @reportFilterIntensityInfoModernTitle. + /// Tooltip for the radar overlay-options chip beside the layer switcher /// /// In en, this message translates to: - /// **'Current (from 2020)'** - String get reportFilterIntensityInfoModernTitle; + /// **'Radar overlay options'** + String get radarOverlayMenuTooltip; - /// No description provided for @reportFilterIntensityInfoModernBody. + /// Map control that centers the camera on the device GPS fix /// /// In en, this message translates to: - /// **'Levels 0–4, 5−, 5+, 6−, 6+, and 7. The filter slider uses this scale; older events still show legacy labels in the list.'** - String get reportFilterIntensityInfoModernBody; + /// **'My location'** + String get mapMyLocation; - /// Label for the magnitude range filter + /// Mesh nodes section header /// /// In en, this message translates to: - /// **'Magnitude'** - String get reportFilterMagnitude; + /// **'Nodes'** + String get meshtasticNodes; - /// Label for the hypocentral-depth range filter + /// Send message button /// /// In en, this message translates to: - /// **'Depth'** - String get reportFilterDepth; + /// **'Send'** + String get meshtasticSend; - /// Depth value with unit in the filter sheet + /// Tooltip for the L7 storm-band radio option /// /// In en, this message translates to: - /// **'{depth} km'** - String reportFilterDepthKm(String depth); + /// **'Level-7 wind field + average circle (purple)'** + String get typhoonOverlayStormL7Tooltip; - /// Label for the origin-time date-range filter + /// AED venue type row label /// /// In en, this message translates to: - /// **'Date'** - String get reportFilterDate; + /// **'Type'** + String get aedType; - /// Button to open the date-range picker when none selected + /// More-menu link title for the Terms of Service /// /// In en, this message translates to: - /// **'Pick dates'** - String get reportFilterDatePick; + /// **'Terms of Service'** + String get termsOfService; - /// Explains that startTime covers from midnight on that calendar day + /// Typhoon UI: typhoonLegendCircle25 /// /// In en, this message translates to: - /// **'Start day: from 00:00 (Taipei)'** - String get reportFilterDateStartNote; + /// **'Storm circle (L10)'** + String get typhoonLegendCircle25; - /// Explains that endTime covers through the end of that calendar day + /// Support page title and the More-menu entry that opens it /// /// In en, this message translates to: - /// **'End day: through 24:00 (Taipei)'** - String get reportFilterDateEndNote; + /// **'Support DPIP'** + String get sponsorTitle; - /// Displays a selected filter range (intensity, magnitude, depth, or dates) + /// Short Map-tab bottom-nav / default-layer picker label for satellite /// /// In en, this message translates to: - /// **'{start} – {end}'** - String reportFilterRange(String start, String end); + /// **'Satellite'** + String get mapNavSatellite; - /// Label for the location keyword filter field + /// Data-update time beside the home rain trend title, Taipei wall clock HH:mm /// /// In en, this message translates to: - /// **'Location'** - String get reportFilterLocation; + /// **'Updated {time}'** + String homeRainTrendUpdated(String time); - /// Hint for the location keyword filter field + /// Onboarding next-step button /// /// In en, this message translates to: - /// **'e.g. Hualien, offshore'** - String get reportFilterLocationHint; + /// **'Next'** + String get onboardingNext; - /// Chip / slider label meaning no filter applied + /// Chip to keep one extreme station per township /// /// In en, this message translates to: - /// **'Any'** - String get reportFilterAny; + /// **'Township'** + String get weatherRankingMergeTown; - /// Primary button on the report filter sheet — saves draft and searches + /// Map layer switcher label for the real-time seismic monitor (RTS) /// /// In en, this message translates to: - /// **'Apply'** - String get reportFilterApply; + /// **'Seismic Monitor'** + String get mapLayerMonitor; - /// Clears all filters in the report filter sheet + /// More-menu link to the ExpTech YouTube channel /// /// In en, this message translates to: - /// **'Reset'** - String get reportFilterReset; + /// **'YouTube'** + String get moreYoutube; - /// Fetches the report list with the current draft filters + /// Support page section header for recurring subscription tiers /// /// In en, this message translates to: - /// **'Search'** - String get reportListSearch; + /// **'Subscriptions'** + String get sponsorSubscriptions; - /// Header title over the report detail map's back button + /// No description provided for @typhoonValueLon. /// /// In en, this message translates to: - /// **'Earthquake Report'** - String get reportDetailTitle; + /// **'{lon}°E'** + String typhoonValueLon(String lon); - /// Eyebrow label on the detail header for a numbered CWA report + /// Label for the experimental sky time-of-day override. /// /// In en, this message translates to: - /// **'No. {number} Significant Earthquake'** - String reportDetailNumbered(String number); + /// **'Sky time'** + String get skyTime; - /// Eyebrow label on the detail header for a …000 (unnumbered) report + /// Label for the weatherModeCloudy option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Local Felt Earthquake'** - String get reportDetailLocalFelt; + /// **'Cloudy'** + String get weatherModeCloudy; - /// Section header over origin time / epicenter / magnitude / depth + /// Label for the skyTimeDusk option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Details'** - String get reportDetailInfo; + /// **'Dusk'** + String get skyTimeDusk; - /// Row label for the report's origin date/time + /// Firmware version /// /// In en, this message translates to: - /// **'Origin time'** - String get reportDetailOriginTime; + /// **'Firmware'** + String get meshtasticFirmware; - /// Row label for the epicenter's latitude/longitude + /// Explains that endTime covers through the end of that calendar day /// /// In en, this message translates to: - /// **'Epicenter'** - String get reportDetailEpicenter; + /// **'End day: through 24:00 (Taipei)'** + String get reportFilterDateEndNote; - /// Row label for the report's magnitude + /// Sort reports by magnitude /// /// In en, this message translates to: /// **'Magnitude'** - String get reportDetailMagnitude; + String get reportFilterSortMagnitude; - /// Row label for the report's hypocentral depth + /// Legend: node known but not heard recently /// /// In en, this message translates to: - /// **'Depth'** - String get reportDetailDepth; + /// **'Silent'** + String get meshtasticSilent; - /// Section header over the per-area/town felt-intensity breakdown + /// Section title in map overlay lists: the seismic-monitor overlays /// /// In en, this message translates to: - /// **'Intensity by area'** - String get reportDetailAreaIntensity; + /// **'Earthquake'** + String get mapLayerCategoryEarthquake; - /// Section header over the per-location (GPS + saved townships) felt-intensity readout, shown above the area breakdown + /// Himawari ozone-band channel (B12, 9.6 µm) layer name /// /// In en, this message translates to: - /// **'Intensity at your locations'** - String get reportDetailLocalIntensity; + /// **'Himawari Ozone (B12)'** + String get mapLayerSatelliteB12; - /// Shown in place of an intensity badge when a location's county isn't in this report's felt-area list at all + /// Typhoon map legend: past/observed path /// /// In en, this message translates to: - /// **'No intensity data'** - String get reportDetailLocalIntensityUnavailable; + /// **'Observed track'** + String get typhoonLegendPast; - /// Tooltip on the area-intensity sort toggle when tapping it switches to grouping by intensity level + /// Restroom venue category: other /// /// In en, this message translates to: - /// **'Sort by intensity'** - String get reportDetailSortByIntensity; + /// **'Other'** + String get restroomCategoryOther; - /// Tooltip on the area-intensity sort toggle when tapping it switches to an alphabetical county list + /// 24h forecast series high and low air temperatures /// /// In en, this message translates to: - /// **'Sort by county'** - String get reportDetailSortByCounty; + /// **'H {high}° · L {low}°'** + String homeForecastHighLow(String high, String low); - /// Section header over the CWA-rendered report image + /// Action on the location banner to open system settings /// /// In en, this message translates to: - /// **'Report image'** - String get reportDetailImage; + /// **'Open settings'** + String get locationBannerFix; - /// Shown in place of the report image when it fails to load + /// Collapsed map-legend chip label / tooltip — tap to expand /// /// In en, this message translates to: - /// **'Report image not available'** - String get reportDetailImageUnavailable; + /// **'Legend'** + String get mapLegendExpand; - /// Button that opens the official CWA report page in a browser + /// Calm state of the earthquake monitor when the live feed reports no alert /// /// In en, this message translates to: - /// **'Report page'** - String get reportDetailOpenReport; + /// **'No active earthquake early warning'** + String get eewNone; - /// Button that opens the RTS/EEW replay starting from this report's origin time + /// Secondary badge on the typhoon sheet hero: the CWA typhoon serial number, e.g. TY 4 /// /// In en, this message translates to: - /// **'Replay'** - String get reportDetailReplay; + /// **'TY {no}'** + String typhoonTyNo(String no); - /// Bottom-nav label and page title for the More tab + /// Notify option label /// /// In en, this message translates to: - /// **'More'** - String get navMore; + /// **'Tsunami advisories and warnings'** + String get notifyOptTsunamiAll; - /// Title of the in-app log viewer and its entry in the More menu + /// Tooltip for the mesh layer's options chip /// /// In en, this message translates to: - /// **'App logs'** - String get appLogs; + /// **'Node options'** + String get meshtasticLayerOptions; - /// More-menu entry and page title for GitHub release notes + /// Terms page continue button /// /// In en, this message translates to: - /// **'Changelog'** - String get changelogTitle; + /// **'Agree and continue'** + String get onboardingAgreeContinue; - /// Empty state when the releases API returns nothing + /// Button that re-runs a failed request /// /// In en, this message translates to: - /// **'No release notes yet'** - String get changelogEmpty; + /// **'Retry'** + String get commonRetry; - /// Chip label for a pre-release + /// The radio's node number /// /// In en, this message translates to: - /// **'Beta'** - String get changelogTypePrerelease; + /// **'Node ID'** + String get meshtasticNodeId; - /// Chip label for a stable release + /// Eyebrow label on the detail header for a numbered CWA report /// /// In en, this message translates to: - /// **'Stable'** - String get changelogTypeStable; + /// **'No. {number} Significant Earthquake'** + String reportDetailNumbered(String number); - /// Chip/badge when a release matches the installed app version + /// Subtitle under each storm-band option (fill + dashed avg) /// /// In en, this message translates to: - /// **'Current'** - String get changelogCurrentVersion; + /// **'With average circle'** + String get typhoonOverlayStormBandSubtitle; - /// App bar title on a single release's detail page + /// Tooltip for the restroom toggle in the disaster-map overlay menu /// /// In en, this message translates to: - /// **'Release details'** - String get changelogVersionDetails; + /// **'Show public restrooms'** + String get disasterMapOverlayRestroomTooltip; - /// Placeholder when a GitHub release has an empty markdown body + /// App bar title for the weather station ranking page /// /// In en, this message translates to: - /// **'No notes for this release.'** - String get changelogBodyEmpty; + /// **'Observation rankings'** + String get weatherRankingTitle; - /// Placeholder shown in place of the map while MapLibre is disabled + /// Home rain trend subtitle: heavy rain that keeps up through the hour /// /// In en, this message translates to: - /// **'Map (temporarily disabled)'** - String get mapPlaceholderDisabled; + /// **'Heavy rain continuing for the next hour'** + String get homeRainTrendHeavySustained; - /// Section header on the More page for saved regions + /// Notify page section header /// /// In en, this message translates to: - /// **'Region'** - String get moreSectionRegion; + /// **'Tsunami'** + String get notifySectionTsunami; - /// Section header on the More page for notification settings + /// Restroom venue category: park /// /// In en, this message translates to: - /// **'Notifications'** - String get moreSectionNotify; + /// **'Park'** + String get restroomCategoryPark; - /// Section header on the More page for language and theme + /// Snackbar shown when an external link fails to open in the browser /// /// In en, this message translates to: - /// **'Display'** - String get moreSectionDisplay; + /// **'Couldn\'t open the link'** + String get moreLinkOpenFailed; - /// More-menu entry that opens the region picker + /// Theme option: always dark /// /// In en, this message translates to: - /// **'Saved regions'** - String get regionManageTitle; + /// **'Dark'** + String get themeDark; - /// Button to open the region picker to add a saved region + /// Footer action that restores previously bought purchases /// /// In en, this message translates to: - /// **'Add a region'** - String get regionAddButton; + /// **'Restore purchases'** + String get sponsorRestore; - /// Empty state on the saved-regions manage page + /// Creating/verifying the DPIP channel /// /// In en, this message translates to: - /// **'No saved regions yet'** - String get regionEmpty; + /// **'Setting up the DPIP channel…'** + String get meshtasticChannelWorking; - /// Title of the region picker (city list) page + /// Button applying the DPIP LoRa region /// /// In en, this message translates to: - /// **'Select a region'** - String get regionSelectTitle; + /// **'Switch to TW'** + String get meshtasticRegionSwitch; - /// Header showing how many saved-region slots are used + /// Section: packet counters /// /// In en, this message translates to: - /// **'{count}/{max} selected'** - String regionSelectCount(int count, int max); + /// **'Traffic'** + String get meshtasticTraffic; - /// Snackbar shown when trying to add a region beyond the cap + /// Explains the Dvorak BD band rendering /// /// In en, this message translates to: - /// **'You can save up to {max} regions'** - String regionSelectFull(int max); + /// **'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'** + String get mapLayerStyleBdTooltip; - /// Edit action on a saved-region bottom sheet + /// Tooltip for the AED toggle in the disaster-map overlay menu /// /// In en, this message translates to: - /// **'Edit'** - String get regionEdit; + /// **'Show AED locations'** + String get disasterMapOverlayAedTooltip; - /// Section header on the More page grouping advanced/developer entries + /// Map layer switcher label for the humidity layer /// /// In en, this message translates to: - /// **'Advanced'** - String get moreSectionAdvanced; + /// **'Humidity'** + String get mapLayerHumidity; - /// More-menu entry / title for the developer diagnostics page + /// Satellite legend note: the daytime RGB recipes fade out across the terminator and are transparent at night /// /// In en, this message translates to: - /// **'Debug info'** - String get moreDeveloper; + /// **'Night = transparent, the basemap shows'** + String get mapLayerSatelliteTransparentNight; - /// Title of the experimental-features settings page and its More-menu entry + /// Scan in progress /// /// In en, this message translates to: - /// **'Experimental features'** - String get experimentalFeatures; + /// **'Scanning…'** + String get meshtasticScanning; - /// Section header on the More page grouping external website links + /// Snackbar shown when trying to add a region beyond the cap /// /// In en, this message translates to: - /// **'Links'** - String get moreSectionLinks; + /// **'You can save up to {max} regions'** + String regionSelectFull(int max); - /// More-menu link to the CWA earthquake early warning publication log website + /// Meshtastic test page title /// /// In en, this message translates to: - /// **'CWA earthquake early warning'** - String get moreCwaEew; + /// **'Meshtastic'** + String get meshtasticTitle; - /// More-menu link to the TREM detection report website + /// Bottom-nav label and page title for the More tab /// /// In en, this message translates to: - /// **'TREM detection report'** - String get moreTremReport; + /// **'More'** + String get navMore; - /// More-menu link to the ExpTech server status website + /// Which channel DPIP payloads use /// /// In en, this message translates to: - /// **'Server status'** - String get moreServerStatus; + /// **'DPIP channel'** + String get meshtasticDpipChannel; - /// More-menu link to the ExpTech announcements website + /// Section header for DPM sub-layer toggles in the overlay menu /// /// In en, this message translates to: - /// **'Announcements'** - String get moreAnnouncements; + /// **'Layers'** + String get disasterMapOverlaySectionLayers; - /// More-menu link to the ExpTech Discord community + /// Himawari near-infrared channel (B05, 1.6 µm) layer name /// /// In en, this message translates to: - /// **'Discord community'** - String get moreDiscord; + /// **'Himawari Near-Infrared (B05)'** + String get mapLayerSatelliteB05; - /// More-menu link to the DPIP notification send-record website + /// Per-quadrant storm-wind radii (km) for a typhoon circle /// /// In en, this message translates to: - /// **'DPIP notification log'** - String get moreNotifyLog; + /// **'NE {ne} · SE {se} · SW {sw} · NW {nw} km'** + String typhoonStormRadii(String ne, String se, String sw, String nw); - /// Snackbar shown when an external link fails to open in the browser + /// No description provided for @typhoonLabelNe. /// /// In en, this message translates to: - /// **'Couldn\'t open the link'** - String get moreLinkOpenFailed; + /// **'NE'** + String get typhoonLabelNe; - /// Setting that forces the home weather backdrop to a fixed state + /// Toast shown after copying a message /// /// In en, this message translates to: - /// **'Weather animation'** - String get weatherDynamicState; + /// **'Message copied'** + String get meshtasticCopied; - /// Subtitle explaining the weather animation setting + /// Empty state when the report catalogue has no rows /// /// In en, this message translates to: - /// **'Override the home backdrop weather'** - String get weatherDynamicStateSubtitle; + /// **'No earthquake reports'** + String get reportListEmpty; - /// Weather animation follows real conditions + /// Footer when the report catalogue has no further pages /// /// In en, this message translates to: - /// **'Auto'** - String get weatherModeAuto; + /// **'End of list'** + String get reportListEnd; - /// Weather animation forced to a clear sky + /// Himawari True Color RGB composite layer name /// /// In en, this message translates to: - /// **'Clear'** - String get weatherModeClear; + /// **'Himawari True Color'** + String get mapLayerSatelliteTruecolor; - /// Weather animation forced to rain + /// Section header for optional typhoon overlays (probability, warning) /// /// In en, this message translates to: - /// **'Rain'** - String get weatherModeRain; + /// **'Overlays'** + String get typhoonOverlaySectionExtra; - /// Weather animation forced to heavy fog + /// Label for the S-wave arrival countdown tile /// /// In en, this message translates to: - /// **'Fog'** - String get weatherModeFog; + /// **'S-wave'** + String get eewSWave; - /// Weather animation forced to a thunderstorm + /// Another app holds the BLE link /// /// In en, this message translates to: - /// **'Thunderstorm'** - String get weatherModeThunderstorm; + /// **'Another app is using this radio'** + String get meshtasticBusyTitle; - /// Generic loading label for an async view + /// Restroom venue category: cultural / leisure activity venue /// /// In en, this message translates to: - /// **'Loading…'** - String get commonLoading; + /// **'Cultural'** + String get restroomCategoryCultural; - /// Button that re-runs a failed request + /// Bulletin table row label /// /// In en, this message translates to: - /// **'Retry'** - String get commonRetry; + /// **'Max. sustained wind near centre'** + String get typhoonLabelWind; - /// Generic headline when an async request fails + /// Hint under the national-border toggle in the radar overlay menu. /// /// In en, this message translates to: - /// **'Something went wrong'** - String get commonError; + /// **'Every country\'s outer frame'** + String get radarGlobalOutlineHint; - /// Error headline when a data request (AsyncView) fails, with a retry button + /// Notify channel title /// /// In en, this message translates to: - /// **'Couldn\'t load data. Please try again.'** - String get commonFetchFailed; + /// **'Disaster information'** + String get notifyEvacuation; - /// Generic message when a loaded list is empty + /// Typhoon UI: typhoonLegendCircle15 /// /// In en, this message translates to: - /// **'Nothing to show'** - String get commonEmpty; + /// **'Gale circle (L7)'** + String get typhoonLegendCircle15; - /// A realtime feed is establishing its first data + /// Astronomy section header in the data catalogue /// /// In en, this message translates to: - /// **'Connecting…'** - String get feedConnecting; + /// **'Astronomy'** + String get dataSectionAstronomy; - /// Banner over a realtime feed whose data has aged past the freshness threshold + /// Home rain trend subtitle: light rain that keeps up through the hour /// /// In en, this message translates to: - /// **'Data may be out of date'** - String get feedStale; + /// **'Light rain continuing for the next hour'** + String get homeRainTrendLightSustained; - /// Banner/headline when a realtime feed has gone offline + /// Generic headline when an async request fails /// /// In en, this message translates to: - /// **'Connection lost'** - String get feedOffline; + /// **'Something went wrong'** + String get commonError; - /// Header of the earthquake monitor when one or more alerts are active + /// Phase: waning crescent /// /// In en, this message translates to: - /// **'Earthquake early warning'** - String get eewTitle; + /// **'Waning crescent'** + String get moonPhaseWaningCrescent; - /// Calm state of the earthquake monitor when the live feed reports no alert + /// Section: battery and uptime /// /// In en, this message translates to: - /// **'No active earthquake early warning'** - String get eewNone; + /// **'Power'** + String get meshtasticPower; - /// One-line summary of an EEW alert's magnitude and depth + /// Label on the map timeline when the newest (latest) frame is selected /// /// In en, this message translates to: - /// **'M{magnitude} · depth {depth} km'** - String eewSummary(String magnitude, String depth); + /// **'Now'** + String get mapTimelineNow; - /// Region bar label for the whole-country view + /// Displays a selected filter range (intensity, magnitude, depth, or dates) /// /// In en, this message translates to: - /// **'Nationwide'** - String get regionNationwide; + /// **'{start} – {end}'** + String reportFilterRange(String start, String end); - /// Region bar label for the current GPS township + /// Button that opens the official CWA report page in a browser /// /// In en, this message translates to: - /// **'Current location'** - String get regionCurrent; + /// **'Report page'** + String get reportDetailOpenReport; - /// Shown when the current-location area is selected but GPS is off/unavailable + /// Trend chart range toggle: last 7 days /// /// In en, this message translates to: - /// **'Can\'t get current location'** - String get regionCurrentUnavailable; + /// **'7d'** + String get trendRange7d; - /// Label for the precipitation metric in the home weather header + /// List of counties under a typhoon warning /// /// In en, this message translates to: - /// **'Precipitation'** - String get weatherPrecipitation; + /// **'Areas: {areas}'** + String typhoonWarningAreas(String areas); - /// Label for the humidity metric in the home weather header + /// Section title in the rainfall menu: the accumulation-interval choices /// /// In en, this message translates to: - /// **'Humidity'** - String get weatherHumidity; - - /// Nearest-station name and observation time shown as small text under the home weather header name + /// **'Time window'** + String get rainIntervalSection; + + /// Title of the notification-settings page /// /// In en, this message translates to: - /// **'{station} · Data {time}'** - String weatherDataTime(String station, String time); + /// **'Notifications'** + String get notifyTitle; - /// Small home-header link that opens the map tab on the temperature layer at the nearest station + /// Transmit power /// /// In en, this message translates to: - /// **'View on map'** - String get homeViewOnMap; + /// **'TX power'** + String get meshtasticTxPower; - /// Section title for the home sheet township hourly forecast + /// Restroom detail row label for the venue category /// /// In en, this message translates to: - /// **'24-hour forecast'** - String get homeForecastTitle; + /// **'Category'** + String get restroomCategoryLabel; - /// 24h forecast series high and low air temperatures + /// Snackbar shown when a purchase restore has been requested /// /// In en, this message translates to: - /// **'H {high}° · L {low}°'** - String homeForecastHighLow(String high, String low); + /// **'Restoring purchases…'** + String get sponsorRestoring; - /// Probability of precipitation percent on a forecast hour chip + /// Support page intro paragraph explaining why donations help /// /// In en, this message translates to: - /// **'{pop}%'** - String homeForecastPop(String pop); + /// **'DPIP is dedicated to real-time disaster-prevention information, with no ads or other revenue model. Your support helps us keep the servers running and keep developing.'** + String get sponsorIntro; - /// Apparent temperature for the selected forecast hour + /// Shelter detail address row label /// /// In en, this message translates to: - /// **'Feels like {temp}°'** - String homeForecastFeelsLike(String temp); + /// **'Address'** + String get shelterAddressLabel; - /// Relative humidity for the selected forecast hour + /// Bulletin table row label /// /// In en, this message translates to: - /// **'Humidity {value}%'** - String homeForecastHumidity(String value); + /// **'Avg. radius of Beaufort 10 winds'** + String get typhoonLabelStormAvg; - /// Wind direction string and Beaufort force for the selected hour + /// Restroom venue category: commercial establishment /// /// In en, this message translates to: - /// **'{direction} · Force {level}'** - String homeForecastWind(String direction, String level); + /// **'Commercial'** + String get restroomCategoryCommercial; - /// Shown when no township code is available for the forecast API + /// AED city / district row label /// /// In en, this message translates to: - /// **'Select a township to see the forecast'** - String get homeForecastUnavailable; + /// **'Region'** + String get aedRegion; - /// Empty or failed forecast on the home sheet + /// Home rain trend subtitle: light rain forecast to stop partway through the hour /// /// In en, this message translates to: - /// **'No forecast available'** - String get homeForecastEmpty; + /// **'Light rain likely to stop in {minutes} minutes'** + String homeRainTrendLightStopping(int minutes); - /// Section title for currently active disaster notices on the collapsed home sheet + /// Section header over origin time / epicenter / magnitude / depth /// /// In en, this message translates to: - /// **'Active events'** - String get homeActiveEventsTitle; + /// **'Details'** + String get reportDetailInfo; - /// Empty state when the realtime event feed has nothing in effect + /// Short Map-tab bottom-nav / default-layer picker label for wind /// /// In en, this message translates to: - /// **'No active events'** - String get homeActiveEventsEmpty; + /// **'Wind'** + String get mapNavWind; - /// Section title for the home sheet 1-hour per-minute rainfall bar chart + /// Tooltip for the wind-forecast overlay-options chip beside the layer switcher. /// /// In en, this message translates to: - /// **'Next hour precipitation'** - String get homeRainTrendTitle; + /// **'Wind forecast overlay options'** + String get windForecastOverlayMenuTooltip; + + /// Subtitle under weather ranking tiles on the Data hub + /// + /// In en, this message translates to: + /// **'Live station rankings'** + String get dataWeatherRankingSubtitle; /// X-axis tick label on the home rain trend chart, minutes from now /// @@ -1041,185 +1047,1685 @@ abstract class AppLocalizations { /// **'{minute} min'** String homeRainTrendMinute(int minute); - /// Data-update time beside the home rain trend title, Taipei wall clock HH:mm + /// No description provided for @rainInterval6h. /// /// In en, this message translates to: - /// **'Updated {time}'** - String homeRainTrendUpdated(String time); + /// **'6 h'** + String get rainInterval6h; - /// Label on the home rain trend chart for minutes beyond the forecast window, and the empty-card hint + /// Restroom type: not specified /// /// In en, this message translates to: - /// **'No data'** - String get homeRainTrendNoData; + /// **'Unspecified'** + String get restroomTypeUnspecified; - /// Home rain trend subtitle: peak intensity below the light-rain threshold + /// Short hint under the strike-probability toggle /// /// In en, this message translates to: - /// **'Light showers possible'** - String get homeRainTrendScattered; + /// **'Hides the forecast cone'** + String get typhoonOverlayProbabilityHint; - /// Home rain trend subtitle: light rain that keeps up through the hour + /// Satellite legend row: the country/global border, drawn bright yellow over the imagery /// /// In en, this message translates to: - /// **'Light rain continuing for the next hour'** - String get homeRainTrendLightSustained; + /// **'Country border'** + String get mapLayerSatelliteGlobalOutline; - /// Home rain trend subtitle: light rain forecast to stop partway through the hour + /// Short Map-tab bottom-nav / default-layer picker label for temperature /// /// In en, this message translates to: - /// **'Light rain likely to stop in {minutes} minutes'** - String homeRainTrendLightStopping(int minutes); + /// **'Temperature'** + String get mapNavTemperature; - /// Home rain trend subtitle: heavy rain that keeps up through the hour + /// Typhoon map legend: forecast waypoint /// /// In en, this message translates to: - /// **'Heavy rain continuing for the next hour'** - String get homeRainTrendHeavySustained; + /// **'Forecast point'** + String get typhoonLegendForecastPoint; - /// Home rain trend subtitle: heavy rain forecast to stop partway through the hour + /// Date section header for reports that originated yesterday (Taipei) /// /// In en, this message translates to: - /// **'Heavy rain likely to stop in {minutes} minutes'** - String homeRainTrendHeavyStopping(int minutes); + /// **'Yesterday'** + String get reportListYesterday; - /// Title of the map layer-picker sheet + /// Section header on the More page grouping external website links /// /// In en, this message translates to: - /// **'Layers'** - String get mapLayers; + /// **'Links'** + String get moreSectionLinks; - /// Title of the layer-order editor, also the tooltip of the reorder button in the layer picker + /// Banner/headline when a realtime feed has gone offline /// /// In en, this message translates to: - /// **'Reorder layers'** - String get mapLayerOrderTitle; + /// **'Connection lost'** + String get feedOffline; - /// Button that restores the layer picker's default order + /// Colour-style option: Dvorak BD curve stepped grayscale /// /// In en, this message translates to: - /// **'Reset order'** - String get mapLayerOrderReset; + /// **'Dvorak BD'** + String get mapLayerStyleBd; - /// Name of the composite radar reflectivity layer in the layer picker + /// Section header on the More page for language and theme /// /// In en, this message translates to: - /// **'Composite Radar Reflectivity'** - String get mapLayerRadar; + /// **'Display'** + String get moreSectionDisplay; - /// Name of the Himawari infrared layer in the layer picker + /// No description provided for @rainInterval3d. /// /// In en, this message translates to: - /// **'Himawari Infrared (B13)'** - String get mapLayerSatellite; + /// **'3 d'** + String get rainInterval3d; - /// Himawari visible-blue channel (B01, 0.47 µm) layer name + /// Explanatory subtitle on the default-map-layer settings page /// /// In en, this message translates to: - /// **'Himawari Blue (B01)'** - String get mapLayerSatelliteB01; + /// **'The Map tab opens on this overlay. The bottom-navigation icon and label follow this choice.'** + String get defaultMapLayerSubtitle; - /// Himawari visible-green channel (B02, 0.51 µm) layer name + /// AED free-text description row label /// /// In en, this message translates to: - /// **'Himawari Green (B02)'** - String get mapLayerSatelliteB02; + /// **'Notes'** + String get aedDescription; - /// Himawari visible-red channel (B03, 0.64 µm) layer name + /// Tooltip for radar underlay (mutex with IR) /// /// In en, this message translates to: - /// **'Himawari Red (B03)'** - String get mapLayerSatelliteB03; + /// **'Radar echo closest to the typhoon bulletin time'** + String get typhoonOverlayWeatherRadarTooltip; - /// Himawari near-infrared channel (B04, 0.86 µm) layer name + /// Permission row description: location /// /// In en, this message translates to: - /// **'Himawari Near-Infrared (B04)'** - String get mapLayerSatelliteB04; + /// **'Target alerts to where you are.'** + String get onboardingPermLocationDesc; - /// Himawari near-infrared channel (B05, 1.6 µm) layer name + /// Himawari CO₂-band channel (B16, 13.3 µm) layer name /// /// In en, this message translates to: - /// **'Himawari Near-Infrared (B05)'** - String get mapLayerSatelliteB05; + /// **'Himawari CO₂ (B16)'** + String get mapLayerSatelliteB16; - /// Himawari near-infrared channel (B06, 2.3 µm) layer name + /// Empty state when the realtime event feed has nothing in effect /// /// In en, this message translates to: - /// **'Himawari Near-Infrared (B06)'** - String get mapLayerSatelliteB06; + /// **'No active events'** + String get homeActiveEventsEmpty; - /// Himawari shortwave-infrared channel (B07, 3.9 µm) layer name + /// Bulletin table row label /// /// In en, this message translates to: - /// **'Himawari Shortwave Infrared (B07)'** - String get mapLayerSatelliteB07; + /// **'Centre location'** + String get typhoonLabelPosition; - /// Himawari upper-level water-vapour channel (B08, 6.2 µm) layer name + /// Label before highest/lowest (or desc/asc) chips on ranking /// /// In en, this message translates to: - /// **'Himawari Upper Water Vapour (B08)'** - String get mapLayerSatelliteB08; + /// **'Sort by'** + String get weatherRankingBy; - /// Himawari mid-level water-vapour channel (B09, 6.9 µm) layer name + /// CWA class: mild typhoon (past-track colour) /// /// In en, this message translates to: - /// **'Himawari Mid Water Vapour (B09)'** - String get mapLayerSatelliteB09; + /// **'Mild typhoon'** + String get typhoonIntensityMild; - /// Himawari lower-level water-vapour channel (B10, 7.3 µm) layer name + /// Hint under the national-border toggle in the wind-forecast overlay menu. /// /// In en, this message translates to: - /// **'Himawari Lower Water Vapour (B10)'** - String get mapLayerSatelliteB10; + /// **'Every country\'s outer frame'** + String get windForecastGlobalOutlineHint; - /// Himawari SO₂ absorption channel (B11, 8.6 µm) layer name + /// No description provided for @rainInterval1h. /// /// In en, this message translates to: - /// **'Himawari SO₂ / Cloud Phase (B11)'** - String get mapLayerSatelliteB11; + /// **'1 h'** + String get rainInterval1h; - /// Himawari ozone-band channel (B12, 9.6 µm) layer name + /// Label for the estimated felt intensity at the user's location /// /// In en, this message translates to: - /// **'Himawari Ozone (B12)'** - String get mapLayerSatelliteB12; + /// **'Estimated at my location'** + String get eewLocalIntensity; - /// Himawari clean-infrared window channel (B13, 10.4 µm) layer name + /// Name of the composite radar reflectivity layer in the layer picker /// /// In en, this message translates to: - /// **'Himawari Infrared (B13)'** - String get mapLayerSatelliteB13; + /// **'Composite Radar Reflectivity'** + String get mapLayerRadar; - /// Himawari longwave-infrared channel (B14, 11.2 µm) layer name + /// Restroom venue category: religious / ceremonial venue /// /// In en, this message translates to: - /// **'Himawari Longwave Infrared (B14)'** - String get mapLayerSatelliteB14; + /// **'Religious'** + String get restroomCategoryReligious; - /// Himawari longwave-infrared channel (B15, 12.4 µm) layer name + /// Device role (client, router...) /// /// In en, this message translates to: - /// **'Himawari Longwave Infrared (B15)'** - String get mapLayerSatelliteB15; + /// **'Role'** + String get meshtasticRole; - /// Himawari CO₂-band channel (B16, 13.3 µm) layer name + /// Cloud-mask category: cloudy /// /// In en, this message translates to: - /// **'Himawari CO₂ (B16)'** - String get mapLayerSatelliteB16; + /// **'Cloudy'** + String get mapLayerSatelliteCloudCloudy; - /// Himawari True Color RGB composite layer name + /// Label for the skyTimeSunrise option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Himawari True Color'** - String get mapLayerSatelliteTruecolor; + /// **'Sunrise'** + String get skyTimeSunrise; - /// Himawari Natural Color RGB composite layer name + /// Empty message log while connected /// /// In en, this message translates to: - /// **'Himawari Natural Color'** - String get mapLayerSatelliteNaturalcolor; + /// **'No messages yet'** + String get meshtasticNoMessages; + + /// Permission row description: notifications + /// + /// In en, this message translates to: + /// **'Deliver earthquake, weather, and disaster alerts the moment they happen.'** + String get onboardingPermNotifyDesc; + + /// Township-border overlay toggle in the map's radar overlay menu. + /// + /// In en, this message translates to: + /// **'Township borders'** + String get radarTownOutline; + + /// Section header of the satellite band colour-style menu on the map + /// + /// In en, this message translates to: + /// **'Colour style'** + String get mapLayerStyleSection; + + /// Tooltip on the disaster-map overlay tune button + /// + /// In en, this message translates to: + /// **'Disaster map layers'** + String get disasterMapOverlayMenuTooltip; + + /// Google Play store link title (brand name) + /// + /// In en, this message translates to: + /// **'Google Play'** + String get moreGooglePlay; + + /// Legend: node heard within the online window + /// + /// In en, this message translates to: + /// **'Heard recently'** + String get meshtasticOnline; + + /// No description provided for @typhoonLabelSw. + /// + /// In en, this message translates to: + /// **'SW'** + String get typhoonLabelSw; + + /// Forecast lead time for a tapped track point + /// + /// In en, this message translates to: + /// **'Forecast +{hours} h'** + String typhoonForecastLead(String hours); + + /// Shelter disaster-type filter chip: tsunami + /// + /// In en, this message translates to: + /// **'Tsunami'** + String get dpmDisasterTsunami; + + /// Chip label for a stable release + /// + /// In en, this message translates to: + /// **'Stable'** + String get changelogTypeStable; + + /// Satellite legend note: the cloud-mask clear category is transparent so the basemap shows + /// + /// In en, this message translates to: + /// **'Clear sky = transparent, the basemap shows'** + String get mapLayerSatelliteTransparentClear; + + /// Section title in map overlay settings menus: the reference overlays + /// + /// In en, this message translates to: + /// **'Reference layers'** + String get mapOverlaySectionReference; + + /// Himawari visible-green channel (B02, 0.51 µm) layer name + /// + /// In en, this message translates to: + /// **'Himawari Green (B02)'** + String get mapLayerSatelliteB02; + + /// Label for …000 serial reports (small-area felt quake, no CWA number) + /// + /// In en, this message translates to: + /// **'Local felt'** + String get reportListLocalFelt; + + /// Empty state when a ranking list has no rows after filters + /// + /// In en, this message translates to: + /// **'No observations to rank'** + String get weatherRankingEmpty; + + /// Notify page section header + /// + /// In en, this message translates to: + /// **'Other'** + String get notifySectionOther; + + /// Snapshot time and station count above a ranking list + /// + /// In en, this message translates to: + /// **'Data time: {time}\n{count} stations'** + String weatherRankingMeta(String time, int count); + + /// Terms agreement checkbox label + /// + /// In en, this message translates to: + /// **'I have read and agree to the Terms of Service'** + String get onboardingTermsAgree; + + /// Satellite legend note: NDVI below the bare-soil threshold is transparent + /// + /// In en, this message translates to: + /// **'Below 0.1 = transparent (no vegetation)'** + String get mapLayerSatelliteTransparentNoVegetation; + + /// Notify option label + /// + /// In en, this message translates to: + /// **'Local intensity 4 or above'** + String get notifyOptLocalIntensity4; + + /// S-wave arrival countdown state once the wave has arrived + /// + /// In en, this message translates to: + /// **'Arrived'** + String get eewArrived; + + /// Empty scan result + /// + /// In en, this message translates to: + /// **'No Meshtastic devices found'** + String get meshtasticNoDevices; + + /// Section title in map overlay lists: everyday-life facility overlays + /// + /// In en, this message translates to: + /// **'Daily life'** + String get mapLayerCategoryLife; + + /// Sort reports by max intensity + /// + /// In en, this message translates to: + /// **'Intensity'** + String get reportFilterSortIntensity; + + /// No description provided for @typhoonMotion. + /// + /// In en, this message translates to: + /// **'Moving'** + String get typhoonMotion; + + /// Connection state label + /// + /// In en, this message translates to: + /// **'Disconnected'** + String get meshtasticStateDisconnected; + + /// CWA class: intense typhoon (past-track colour) + /// + /// In en, this message translates to: + /// **'Intense typhoon'** + String get typhoonIntensityIntense; + + /// Title of the layer-order editor, also the tooltip of the reorder button in the layer picker + /// + /// In en, this message translates to: + /// **'Reorder layers'** + String get mapLayerOrderTitle; + + /// Affirmative value in the disaster-map detail sheet + /// + /// In en, this message translates to: + /// **'Yes'** + String get dpmYes; + + /// Chart placeholder before two samples exist + /// + /// In en, this message translates to: + /// **'Not enough history yet'** + String get meshtasticNoHistory; + + /// Shown in place of an intensity badge when a location's county isn't in this report's felt-area list at all + /// + /// In en, this message translates to: + /// **'No intensity data'** + String get reportDetailLocalIntensityUnavailable; + + /// Map layer switcher label for the GFS wind-forecast layer + /// + /// In en, this message translates to: + /// **'GFS'** + String get mapLayerWindForecastGfs; + + /// Depth unit label beside the depth value on a report list row + /// + /// In en, this message translates to: + /// **'km'** + String get reportListDepthUnit; + + /// Label for the hypocentral-depth range filter + /// + /// In en, this message translates to: + /// **'Depth'** + String get reportFilterDepth; + + /// Hint shown until the user scrolls to the end + /// + /// In en, this message translates to: + /// **'Scroll down to continue'** + String get onboardingScrollHint; + + /// Short Map-tab bottom-nav / default-layer picker label for the 1h QPESUMS precipitation forecast + /// + /// In en, this message translates to: + /// **'Forecast'** + String get mapNavQpesums; + + /// Bottom-nav label and page title for the Map tab + /// + /// In en, this message translates to: + /// **'Map'** + String get navMap; + + /// Notify channel title + /// + /// In en, this message translates to: + /// **'Weather advisories'** + String get notifyAdvisory; + + /// Clears all filters in the report filter sheet + /// + /// In en, this message translates to: + /// **'Reset'** + String get reportFilterReset; + + /// Himawari modified normalised-difference water-index layer name + /// + /// In en, this message translates to: + /// **'Himawari MNDWI'** + String get mapLayerSatelliteMndwi; + + /// Section header for L7/L10 storm-band choices in the overlay menu + /// + /// In en, this message translates to: + /// **'Storm wind'** + String get typhoonOverlaySectionStorm; + + /// Phase: full moon + /// + /// In en, this message translates to: + /// **'Full moon'** + String get moonPhaseFull; + + /// Phase: waning gibbous + /// + /// In en, this message translates to: + /// **'Waning gibbous'** + String get moonPhaseWaningGibbous; + + /// Subtitle explaining the weather animation setting + /// + /// In en, this message translates to: + /// **'Override the home backdrop weather'** + String get weatherDynamicStateSubtitle; + + /// No description provided for @reportFilterIntensityInfoModernTitle. + /// + /// In en, this message translates to: + /// **'Current (from 2020)'** + String get reportFilterIntensityInfoModernTitle; + + /// Bulletin data time under the intensity chip (Taipei wall clock) + /// + /// In en, this message translates to: + /// **'Data time\n{time}'** + String typhoonDataTime(String time); + + /// Restroom type: accessible restroom + /// + /// In en, this message translates to: + /// **'Accessible'** + String get restroomTypeAccessible; + + /// More-menu section header for about / legal links + /// + /// In en, this message translates to: + /// **'About'** + String get moreSectionAbout; + + /// Device picker sheet title + /// + /// In en, this message translates to: + /// **'Select a radio'** + String get meshtasticSelectDevice; + + /// Onboarding intro page body + /// + /// In en, this message translates to: + /// **'DPIP is your disaster-prevention companion. It brings together earthquake early warnings, earthquake reports, weather, and hazard information, and alerts you the moment it matters.\n\n• Earthquakes: early warnings, intensity reports, and detailed reports\n• Weather: real-time thunderstorm messages and weather advisories\n• Tsunami and disaster information\n\nNext, we\'ll ask you to review the Terms of Service and grant a few permissions so DPIP can protect you in real time.'** + String get onboardingIntroBody; + + /// Shelter detail capacity row label + /// + /// In en, this message translates to: + /// **'Capacity'** + String get shelterCapacityLabel; + + /// Section header over the CWA-rendered report image + /// + /// In en, this message translates to: + /// **'Report image'** + String get reportDetailImage; + + /// Connection state label + /// + /// In en, this message translates to: + /// **'Configuring…'** + String get meshtasticStateConfiguring; + + /// Bulletin table row label + /// + /// In en, this message translates to: + /// **'Avg. radius of Beaufort 7 winds'** + String get typhoonLabelGaleAvg; + + /// Permission row: notifications + /// + /// In en, this message translates to: + /// **'Notifications'** + String get onboardingPermNotify; + + /// Menu action clearing the message log + /// + /// In en, this message translates to: + /// **'Clear messages'** + String get meshtasticClearMessages; + + /// Toggle: local notification for an incoming mesh message + /// + /// In en, this message translates to: + /// **'Notify on new messages'** + String get meshtasticNotifyMessages; + + /// More-menu entry and page title for choosing the Map tab's default overlay + /// + /// In en, this message translates to: + /// **'Default map layer'** + String get defaultMapLayerSettings; + + /// Section header on the More page for notification settings + /// + /// In en, this message translates to: + /// **'Notifications'** + String get moreSectionNotify; + + /// Shown on the notify page when there is no push token yet + /// + /// In en, this message translates to: + /// **'Push notifications aren\'t ready yet — try again shortly.'** + String get notifyUnavailable; + + /// Button that restores the layer picker's default order + /// + /// In en, this message translates to: + /// **'Reset order'** + String get mapLayerOrderReset; + + /// Address row label in the disaster-map restroom / shelter detail sheet + /// + /// In en, this message translates to: + /// **'Address'** + String get dpmAddress; + + /// Chip to keep one extreme station per county + /// + /// In en, this message translates to: + /// **'County'** + String get weatherRankingMergeCounty; + + /// More-page section header for the app-store download links + /// + /// In en, this message translates to: + /// **'Get the app'** + String get moreSectionApp; + + /// No description provided for @reportFilterIntensityInfoLegacyBody. + /// + /// In en, this message translates to: + /// **'Only levels 0–7. No 5− / 5+ / 6− / 6+ split.'** + String get reportFilterIntensityInfoLegacyBody; + + /// Himawari sea-surface-temperature (ACSPO L3C) layer name + /// + /// In en, this message translates to: + /// **'Himawari Sea Surface Temperature'** + String get mapLayerSatelliteSst; + + /// Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher. + /// + /// In en, this message translates to: + /// **'QPESUMS overlay options'** + String get qpesumsOverlayMenuTooltip; + + /// Label on the map timeline when the selected frame postdates the present + /// + /// In en, this message translates to: + /// **'Future'** + String get mapTimelineFuture; + + /// Legend for the purple dashed mean-radius storm circle + /// + /// In en, this message translates to: + /// **'Average circle'** + String get typhoonLegendCircleAvg; + + /// Depth value with unit in the filter sheet + /// + /// In en, this message translates to: + /// **'{depth} km'** + String reportFilterDepthKm(String depth); + + /// No description provided for @typhoonLabelSe. + /// + /// In en, this message translates to: + /// **'SE'** + String get typhoonLabelSe; + + /// Hint under the township-border toggle in the radar overlay menu. + /// + /// In en, this message translates to: + /// **'The finer mesh'** + String get radarTownOutlineHint; + + /// S-wave arrival countdown in seconds + /// + /// In en, this message translates to: + /// **'{seconds} s'** + String eewCountdown(int seconds); + + /// Bulletin table row label + /// + /// In en, this message translates to: + /// **'Peak gust'** + String get typhoonLabelGust; + + /// External map app choice: Google Maps + /// + /// In en, this message translates to: + /// **'Google Maps'** + String get mapAppGoogleMaps; + + /// Footer link to the Terms of Use + /// + /// In en, this message translates to: + /// **'Terms of Use'** + String get sponsorTerms; + + /// Restroom type: gender-neutral restroom + /// + /// In en, this message translates to: + /// **'Gender-neutral'** + String get restroomTypeGenderNeutral; + + /// Notify channel title + /// + /// In en, this message translates to: + /// **'Thunderstorm alerts'** + String get notifyThunderstorm; + + /// Label for the skyTimeGolden option in the experimental backdrop settings. + /// + /// In en, this message translates to: + /// **'Golden hour'** + String get skyTimeGolden; + + /// Moon age label + /// + /// In en, this message translates to: + /// **'Age'** + String get moonAge; + + /// Section: LoRa settings + /// + /// In en, this message translates to: + /// **'LoRa'** + String get meshtasticRadioSettings; + + /// Current temperature fragment in an extremes analysis line + /// + /// In en, this message translates to: + /// **'Now {value}°C'** + String weatherRankingAnalysisCurrent(String value); + + /// More-menu link to the ExpTech GitHub organisation + /// + /// In en, this message translates to: + /// **'ExpTech GitHub'** + String get moreGithub; + + /// Shown when no township code is available for the forecast API + /// + /// In en, this message translates to: + /// **'Select a township to see the forecast'** + String get homeForecastUnavailable; + + /// Title of the map layer-picker sheet + /// + /// In en, this message translates to: + /// **'Layers'** + String get mapLayers; + + /// Board model + /// + /// In en, this message translates to: + /// **'Hardware'** + String get meshtasticHardware; + + /// Label next to the language picker on the welcome screen + /// + /// In en, this message translates to: + /// **'Language'** + String get languageSettings; + + /// Shelter disaster-type filter chip: nuclear accident + /// + /// In en, this message translates to: + /// **'Nuclear accident'** + String get dpmDisasterNuclear; + + /// Language picker tooltip / label + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// Apparent temperature for the selected forecast hour + /// + /// In en, this message translates to: + /// **'Feels like {temp}°'** + String homeForecastFeelsLike(String temp); + + /// Subtitle: weather tile matches typhoon report time + /// + /// In en, this message translates to: + /// **'Aligned to bulletin time'** + String get typhoonOverlayWeatherHint; + + /// Label for the skyTimeDawn option in the experimental backdrop settings. + /// + /// In en, this message translates to: + /// **'Dawn'** + String get skyTimeDawn; + + /// Label for the skyTimeAfternoon option in the experimental backdrop settings. + /// + /// In en, this message translates to: + /// **'Afternoon'** + String get skyTimeAfternoon; + + /// When a node last transmitted + /// + /// In en, this message translates to: + /// **'Last heard'** + String get meshtasticLastHeard; + + /// Typhoon UI: typhoonWarningTitle + /// + /// In en, this message translates to: + /// **'Typhoon warning'** + String get typhoonWarningTitle; + + /// More-menu link to DPIP's source repository on GitHub + /// + /// In en, this message translates to: + /// **'Source code'** + String get moreSourceCode; + + /// Section title in map overlay lists: the weather-observation overlays + /// + /// In en, this message translates to: + /// **'Weather observations'** + String get mapLayerCategoryWeather; + + /// Himawari mid-level water-vapour channel (B09, 6.9 µm) layer name + /// + /// In en, this message translates to: + /// **'Himawari Mid Water Vapour (B09)'** + String get mapLayerSatelliteB09; + + /// Hint under the township-border toggle in the wind-forecast overlay menu. + /// + /// In en, this message translates to: + /// **'The finer mesh'** + String get windForecastTownOutlineHint; + + /// Himawari cloud-mask (Level-2 retrieval) layer name + /// + /// In en, this message translates to: + /// **'Himawari Cloud Mask'** + String get mapLayerSatelliteCloudmask; + + /// Choice-sheet action: copy the point's coordinates + /// + /// In en, this message translates to: + /// **'Copy coordinates'** + String get mapAppCopyCoordinates; + + /// Intro paragraph for the intensity-scale info dialog + /// + /// In en, this message translates to: + /// **'CWA changed the felt-intensity scale on 1 Jan 2020 (Taipei time).'** + String get reportFilterIntensityInfoIntro; + + /// Short Map-tab bottom-nav / default-layer picker label for RTS seismic monitor + /// + /// In en, this message translates to: + /// **'Earthquake'** + String get mapNavEarthquake; + + /// No description provided for @typhoonGust. + /// + /// In en, this message translates to: + /// **'Gust'** + String get typhoonGust; + + /// Restroom cleanliness grade: average + /// + /// In en, this message translates to: + /// **'Average'** + String get restroomGradeAverage; + + /// Himawari cirrus / cloud-height brightness-temperature-difference layer name + /// + /// In en, this message translates to: + /// **'Himawari Cirrus / Cloud Height'** + String get mapLayerSatelliteBtdCo2; + + /// Permission row description: background location + /// + /// In en, this message translates to: + /// **'Allow \"Always\" so alerts still target you when the app is closed.'** + String get onboardingPermBackgroundDesc; + + /// Label above the map timeline's date when the frame times are forecast times, e.g. Forecast / 2026/07/14 + /// + /// In en, this message translates to: + /// **'Forecast'** + String get mapTimelineForecast; + + /// Restroom detail row label for the toilet type + /// + /// In en, this message translates to: + /// **'Type'** + String get restroomTypeLabel; + + /// Earthquake report catalogue title (entry under the Data hub) + /// + /// In en, this message translates to: + /// **'Earthquake'** + String get navEarthquake; + + /// Tooltip for the L10 storm-band radio row + /// + /// In en, this message translates to: + /// **'Level-10 wind field + average circle (yellow)'** + String get typhoonOverlayStormL10Tooltip; + + /// Phase: waxing gibbous + /// + /// In en, this message translates to: + /// **'Waxing gibbous'** + String get moonPhaseWaxingGibbous; + + /// Header title over the report detail map's back button + /// + /// In en, this message translates to: + /// **'Earthquake Report'** + String get reportDetailTitle; + + /// More-menu link to the TREM detection report website + /// + /// In en, this message translates to: + /// **'TREM detection report'** + String get moreTremReport; + + /// Nearest-station name and observation time shown as small text under the home weather header name + /// + /// In en, this message translates to: + /// **'{station} · Data {time}'** + String weatherDataTime(String station, String time); + + /// Empty node list + /// + /// In en, this message translates to: + /// **'No nodes heard yet'** + String get meshtasticNoNodes; + + /// Legend: node reported over an MQTT bridge + /// + /// In en, this message translates to: + /// **'Via MQTT (internet)'** + String get meshtasticViaMqtt; + + /// County-border overlay toggle in the map's radar overlay menu. + /// + /// In en, this message translates to: + /// **'County borders'** + String get radarCountyOutline; + + /// Permission granted label + /// + /// In en, this message translates to: + /// **'Granted'** + String get onboardingGranted; + + /// Generic close button / action label + /// + /// In en, this message translates to: + /// **'Close'** + String get commonClose; + + /// Restroom detail row label for the cleanliness grade + /// + /// In en, this message translates to: + /// **'Grade'** + String get restroomGradeLabel; + + /// Rainfall accumulation since local midnight (API now) + /// + /// In en, this message translates to: + /// **'Today'** + String get rainIntervalNow; + + /// Chip/badge when a release matches the installed app version + /// + /// In en, this message translates to: + /// **'Current'** + String get changelogCurrentVersion; + + /// Bulletin table row label + /// + /// In en, this message translates to: + /// **'Central pressure'** + String get typhoonLabelPressure; + + /// Tooltip for the forecast callouts overlay toggle + /// + /// In en, this message translates to: + /// **'Show forecast-point detail cards when zoomed in'** + String get typhoonOverlayForecastCalloutsTooltip; + + /// AED opening-hours remark row label + /// + /// In en, this message translates to: + /// **'Hours note'** + String get aedOpenRemark; + + /// Onboarding permissions page intro + /// + /// In en, this message translates to: + /// **'So DPIP can alert you the moment disaster strikes, please grant the following. You can change these anytime in system settings.'** + String get onboardingPermsBody; + + /// Overlay-menu section for radar / IR under the typhoon vectors + /// + /// In en, this message translates to: + /// **'Weather underlay'** + String get typhoonOverlaySectionWeather; + + /// Notify option label + /// + /// In en, this message translates to: + /// **'Current location only'** + String get notifyOptWeatherLocal; + + /// Short Map-tab bottom-nav / default-layer picker label for rain + /// + /// In en, this message translates to: + /// **'Rain'** + String get mapNavRain; + + /// Day unit for the moon age + /// + /// In en, this message translates to: + /// **'days'** + String get moonDays; + + /// Unit footer under a map colour legend (e.g. Unit: dBZ) + /// + /// In en, this message translates to: + /// **'Unit: {unit}'** + String mapLegendUnit(String unit); + + /// Weather animation forced to a clear sky + /// + /// In en, this message translates to: + /// **'Clear'** + String get weatherModeClear; + + /// Radio diagnostics sheet title + /// + /// In en, this message translates to: + /// **'Radio'** + String get meshtasticRadio; + + /// Generic message when a loaded list is empty + /// + /// In en, this message translates to: + /// **'Nothing to show'** + String get commonEmpty; + + /// Himawari visible-blue channel (B01, 0.47 µm) layer name + /// + /// In en, this message translates to: + /// **'Himawari Blue (B01)'** + String get mapLayerSatelliteB01; + + /// Battery value when mains powered + /// + /// In en, this message translates to: + /// **'External power'** + String get meshtasticExternalPower; + + /// Phase: last quarter + /// + /// In en, this message translates to: + /// **'Last quarter'** + String get moonPhaseLastQuarter; + + /// Sort order: oldest / smallest first + /// + /// In en, this message translates to: + /// **'Ascending'** + String get reportFilterOrderAsc; + + /// Primary button on the report filter sheet — saves draft and searches + /// + /// In en, this message translates to: + /// **'Apply'** + String get reportFilterApply; + + /// Shown in place of the report image when it fails to load + /// + /// In en, this message translates to: + /// **'Report image not available'** + String get reportDetailImageUnavailable; + + /// Chip to rank temperature descending + /// + /// In en, this message translates to: + /// **'Highest'** + String get weatherRankingHighest; + + /// Button that opens the RTS/EEW replay starting from this report's origin time + /// + /// In en, this message translates to: + /// **'Replay'** + String get reportDetailReplay; + + /// Disaster-map overlay menu toggle for public restrooms + /// + /// In en, this message translates to: + /// **'Restrooms'** + String get mapLayerRestroom; + + /// Restroom venue category: social welfare institution / gathering place + /// + /// In en, this message translates to: + /// **'Welfare'** + String get restroomCategoryWelfare; + + /// Restroom cleanliness grade: excellent + /// + /// In en, this message translates to: + /// **'Excellent'** + String get restroomGradeExcellent; + + /// Age of the last sent packet + /// + /// In en, this message translates to: + /// **'Last sent'** + String get meshtasticLastSent; + + /// The radio's long name + /// + /// In en, this message translates to: + /// **'Name'** + String get meshtasticName; + + /// Start scanning for Meshtastic radios + /// + /// In en, this message translates to: + /// **'Scan'** + String get meshtasticScan; + + /// Section title in map overlay lists: numerical weather prediction (ECMWF/GFS) wind-field overlays + /// + /// In en, this message translates to: + /// **'Numerical forecast'** + String get mapLayerCategoryForecast; + + /// The radio rejected the channel write + /// + /// In en, this message translates to: + /// **'Couldn\'t set up the DPIP channel'** + String get meshtasticChannelFailed; + + /// Theme option: follow the system light/dark setting + /// + /// In en, this message translates to: + /// **'System'** + String get themeSystem; + + /// Himawari normalised-difference vegetation-index layer name + /// + /// In en, this message translates to: + /// **'Himawari NDVI'** + String get mapLayerSatelliteNdvi; + + /// Typhoon map legend: forecast path + /// + /// In en, this message translates to: + /// **'Forecast track'** + String get typhoonLegendForecast; + + /// No description provided for @typhoonValueHpa. + /// + /// In en, this message translates to: + /// **'{n} hPa'** + String typhoonValueHpa(String n); + + /// Label for the precipitation metric in the home weather header + /// + /// In en, this message translates to: + /// **'Precipitation'** + String get weatherPrecipitation; + + /// Next full moon date label + /// + /// In en, this message translates to: + /// **'Next full moon'** + String get moonNextFullMoon; + + /// Hint in the disaster-map detail sheet when nothing is selected + /// + /// In en, this message translates to: + /// **'Tap a marker on the map for details'** + String get dpmSheetEmpty; + + /// Proceed past onboarding without granting permissions + /// + /// In en, this message translates to: + /// **'Skip anyway'** + String get onboardingSkipLeave; + + /// Onboarding back button + /// + /// In en, this message translates to: + /// **'Back'** + String get onboardingBack; + + /// AED placement description row label + /// + /// In en, this message translates to: + /// **'Placement'** + String get aedPlaceDesc; + + /// Title of the confirm dialog shown when finishing onboarding without key permissions + /// + /// In en, this message translates to: + /// **'Permissions not granted'** + String get onboardingSkipTitle; + + /// Restroom type: family restroom + /// + /// In en, this message translates to: + /// **'Family'** + String get restroomTypeFamily; + + /// No description provided for @typhoonValueKm. + /// + /// In en, this message translates to: + /// **'{n} km'** + String typhoonValueKm(String n); + + /// No description provided for @typhoonPressure. + /// + /// In en, this message translates to: + /// **'Pressure'** + String get typhoonPressure; + + /// Permission row: battery optimization (Android) + /// + /// In en, this message translates to: + /// **'Battery exemption'** + String get onboardingPermBattery; + + /// No description provided for @typhoonLabelNw. + /// + /// In en, this message translates to: + /// **'NW'** + String get typhoonLabelNw; + + /// Shelter disaster-type filter chip: flood + /// + /// In en, this message translates to: + /// **'Flood'** + String get dpmDisasterFlood; + + /// Phase: waxing crescent + /// + /// In en, this message translates to: + /// **'Waxing crescent'** + String get moonPhaseWaxingCrescent; + + /// Restroom venue category: leisure / entertainment venue + /// + /// In en, this message translates to: + /// **'Leisure'** + String get restroomCategoryLeisure; + + /// Map layer switcher label for the air-temperature layer + /// + /// In en, this message translates to: + /// **'Temperature'** + String get mapLayerTemperature; + + /// AED venue category row label + /// + /// In en, this message translates to: + /// **'Category'** + String get aedCategory; + + /// Section: the radio's channel table + /// + /// In en, this message translates to: + /// **'Channels'** + String get meshtasticChannels; + + /// Shown in the monitor panel before the first RTS snapshot arrives + /// + /// In en, this message translates to: + /// **'Waiting for data…'** + String get monitorWaiting; + + /// Overlay menu: toggle forecast-point Flutter callout cards + /// + /// In en, this message translates to: + /// **'Forecast tooltips'** + String get typhoonOverlayForecastCallouts; + + /// Row label for the epicenter's latitude/longitude + /// + /// In en, this message translates to: + /// **'Epicenter'** + String get reportDetailEpicenter; + + /// Battery voltage + /// + /// In en, this message translates to: + /// **'Voltage'** + String get meshtasticVoltage; + + /// Map layer switcher subtitle + /// + /// In en, this message translates to: + /// **'LoRa mesh nodes heard by your radio'** + String get mapLayerMeshtasticSubtitle; + + /// Map layer switcher label for the wind-direction layer + /// + /// In en, this message translates to: + /// **'Wind direction'** + String get mapLayerWind; + + /// Row label for the report's magnitude + /// + /// In en, this message translates to: + /// **'Magnitude'** + String get reportDetailMagnitude; + + /// Section header over the per-area/town felt-intensity breakdown + /// + /// In en, this message translates to: + /// **'Intensity by area'** + String get reportDetailAreaIntensity; + + /// No description provided for @rainInterval12h. + /// + /// In en, this message translates to: + /// **'12 h'** + String get rainInterval12h; + + /// Emphasized magnitude on a report list row + /// + /// In en, this message translates to: + /// **'M{magnitude}'** + String reportListMagnitude(String magnitude); + + /// Shelter disaster-type filter chip: landslide + /// + /// In en, this message translates to: + /// **'Landslide'** + String get dpmDisasterLandslide; + + /// Notify channel title + /// + /// In en, this message translates to: + /// **'Strong-motion monitor'** + String get notifyMonitor; + + /// Onboarding finish button + /// + /// In en, this message translates to: + /// **'Get started'** + String get onboardingStart; + + /// Monthly price label for a subscription; price is the store-localized amount + /// + /// In en, this message translates to: + /// **'{price} / month'** + String sponsorPerMonth(String price); + + /// Map layer switcher label for the air-pressure layer + /// + /// In en, this message translates to: + /// **'Pressure'** + String get mapLayerPressure; + + /// Himawari near-infrared channel (B04, 0.86 µm) layer name + /// + /// In en, this message translates to: + /// **'Himawari Near-Infrared (B04)'** + String get mapLayerSatelliteB04; + + /// Satellite legend note: on the brightness-temperature-difference layers a near-zero difference is transparent — no absorber is present + /// + /// In en, this message translates to: + /// **'Zero difference = transparent (no signal)'** + String get mapLayerSatelliteTransparentZero; + + /// Shelter detail row: whether indoor shelter is provided + /// + /// In en, this message translates to: + /// **'Indoor shelter'** + String get shelterIndoorLabel; + + /// Notify option label + /// + /// In en, this message translates to: + /// **'Off'** + String get notifyOptOff; + + /// Sort reports by origin time + /// + /// In en, this message translates to: + /// **'Time'** + String get reportFilterSortTime; + + /// Cloud-mask category: probably clear + /// + /// In en, this message translates to: + /// **'Probably clear'** + String get mapLayerSatelliteCloudProbablyClear; + + /// Weather animation forced to a thunderstorm + /// + /// In en, this message translates to: + /// **'Thunderstorm'** + String get weatherModeThunderstorm; + + /// Small home-header link that opens the map tab on the temperature layer at the nearest station + /// + /// In en, this message translates to: + /// **'View on map'** + String get homeViewOnMap; + + /// No description provided for @reportFilterIntensityInfoLegacyTitle. + /// + /// In en, this message translates to: + /// **'Legacy (before 2020)'** + String get reportFilterIntensityInfoLegacyTitle; + + /// Bulletin table row label + /// + /// In en, this message translates to: + /// **'Past movement speed'** + String get typhoonLabelSpeed; + + /// Snackbar when the chosen map app cannot be opened on this device + /// + /// In en, this message translates to: + /// **'Could not open {app}'** + String mapAppOpenFailed(String app); + + /// Satellite legend note for the RGB-recipe products (True Color, Ash, …) that carry no single numerical scale + /// + /// In en, this message translates to: + /// **'RGB composite (JMA recipe)'** + String get mapLayerSatelliteRgbComposite; + + /// Packets received this session + /// + /// In en, this message translates to: + /// **'Received'** + String get meshtasticReceived; + + /// Chip to rank by recorded daily minimum temperature + /// + /// In en, this message translates to: + /// **'Daily low'** + String get weatherRankingExtremeLow; + + /// Himawari lower-level water-vapour channel (B10, 7.3 µm) layer name + /// + /// In en, this message translates to: + /// **'Himawari Lower Water Vapour (B10)'** + String get mapLayerSatelliteB10; + + /// Cloud-mask category: probably cloudy + /// + /// In en, this message translates to: + /// **'Probably cloudy'** + String get mapLayerSatelliteCloudProbablyCloudy; + + /// Satellite legend note: NDWI/MNDWI at zero or below is transparent — no water signal + /// + /// In en, this message translates to: + /// **'≤ 0 = transparent (no water)'** + String get mapLayerSatelliteTransparentNoWater; + + /// Shelter detail applicable-disaster categories row label + /// + /// In en, this message translates to: + /// **'Disaster types'** + String get shelterCategoryLabel; + + /// Connection state label + /// + /// In en, this message translates to: + /// **'Connecting…'** + String get meshtasticStateConnecting; + + /// Moon page title + /// + /// In en, this message translates to: + /// **'Moon'** + String get moonTitle; + + /// Ranking tab/tile for peak gust speed + /// + /// In en, this message translates to: + /// **'Gust'** + String get weatherRankingGust; + + /// Apple App Store link title (brand name) + /// + /// In en, this message translates to: + /// **'App Store'** + String get moreAppStore; + + /// Filter section title in the disaster-map sheet: shelter disaster types + /// + /// In en, this message translates to: + /// **'Shelter disaster types'** + String get dpmFilterSectionShelter; + + /// More-menu link to the ExpTech server status website + /// + /// In en, this message translates to: + /// **'Server status'** + String get moreServerStatus; + + /// Notify page section header + /// + /// In en, this message translates to: + /// **'Weather'** + String get notifySectionWeather; + + /// LoRa modem preset + /// + /// In en, this message translates to: + /// **'Modem preset'** + String get meshtasticPreset; + + /// Section header on the Data hub for earthquake-related entries + /// + /// In en, this message translates to: + /// **'Seismic'** + String get dataSectionSeismic; + + /// Placeholder when a GitHub release has an empty markdown body + /// + /// In en, this message translates to: + /// **'No notes for this release.'** + String get changelogBodyEmpty; + + /// World-country-border overlay toggle in the map's reference-layer overlay menus. + /// + /// In en, this message translates to: + /// **'National borders'** + String get radarGlobalOutline; + + /// Notify channel title + /// + /// In en, this message translates to: + /// **'Emergency earthquake alert'** + String get notifyEew; + + /// Region bar label for the whole-country view + /// + /// In en, this message translates to: + /// **'Nationwide'** + String get regionNationwide; + + /// More-menu link to the DPIP notification send-record website + /// + /// In en, this message translates to: + /// **'DPIP notification log'** + String get moreNotifyLog; + + /// Region bar label for the current GPS township + /// + /// In en, this message translates to: + /// **'Current location'** + String get regionCurrent; + + /// Filter section title in the disaster-map sheet: restroom venue categories + /// + /// In en, this message translates to: + /// **'Venue types'** + String get dpmFilterSectionRestroom; + + /// Empty message log while not connected + /// + /// In en, this message translates to: + /// **'Not connected to a radio'** + String get meshtasticNotConnected; + + /// Label for the weatherModeSnow option in the experimental backdrop settings. + /// + /// In en, this message translates to: + /// **'Snow'** + String get weatherModeSnow; + + /// Map layer name: mesh nodes + /// + /// In en, this message translates to: + /// **'Meshtastic nodes'** + String get mapLayerMeshtastic; + + /// More-menu entry / title for the developer diagnostics page + /// + /// In en, this message translates to: + /// **'Debug info'** + String get moreDeveloper; + + /// Himawari longwave-infrared channel (B14, 11.2 µm) layer name + /// + /// In en, this message translates to: + /// **'Himawari Longwave Infrared (B14)'** + String get mapLayerSatelliteB14; + + /// Share of airtime seen busy + /// + /// In en, this message translates to: + /// **'Channel use'** + String get meshtasticChannelUse; + + /// Short Map-tab bottom-nav / default-layer picker label for lightning + /// + /// In en, this message translates to: + /// **'Lightning'** + String get mapNavLightning; + + /// Empty or failed forecast on the home sheet + /// + /// In en, this message translates to: + /// **'No forecast available'** + String get homeForecastEmpty; + + /// Support page section header for one-time tips + /// + /// In en, this message translates to: + /// **'One-time'** + String get sponsorOneTime; + + /// Himawari split-window brightness-temperature-difference layer name + /// + /// In en, this message translates to: + /// **'Himawari Split Window'** + String get mapLayerSatelliteBtdSplit; + + /// Permission row: background/Always location + /// + /// In en, this message translates to: + /// **'Background location'** + String get onboardingPermBackground; + + /// AED emergency contact phone row label + /// + /// In en, this message translates to: + /// **'Emergency phone'** + String get aedEmergencyPhone; + + /// Action in the disaster-map detail sheet: open the point in an external map app + /// + /// In en, this message translates to: + /// **'Open in maps'** + String get dpmOpenInMaps; + + /// Toggle: local notification when a new node is heard + /// + /// In en, this message translates to: + /// **'Notify on new nodes'** + String get meshtasticNotifyNodes; + + /// Permission row description: critical alerts + /// + /// In en, this message translates to: + /// **'Let life-threatening earthquake warnings sound even in silent mode or Do Not Disturb.'** + String get onboardingPermCriticalDesc; + + /// Satellite legend note: on the IR grayscale/enhancements the warm end is clear sky, drawn transparent so the basemap shows + /// + /// In en, this message translates to: + /// **'Clear sky (warm end) = transparent, the basemap shows'** + String get mapLayerSatelliteTransparentWarm; + + /// Packets sent this session + /// + /// In en, this message translates to: + /// **'Sent'** + String get meshtasticSent; + + /// Section title for the home sheet township hourly forecast + /// + /// In en, this message translates to: + /// **'24-hour forecast'** + String get homeForecastTitle; + + /// Typhoon UI: typhoonLegendWarningAreas + /// + /// In en, this message translates to: + /// **'Warning areas'** + String get typhoonLegendWarningAreas; + + /// How many nodes the filter is hiding + /// + /// In en, this message translates to: + /// **'{count} hidden'** + String meshtasticExcludeMqttHidden(int count); + + /// Notify option label + /// + /// In en, this message translates to: + /// **'Local intensity 1 or above'** + String get notifyOptLocalIntensity1; + + /// Label on the map timeline when the selected frame predates the present + /// + /// In en, this message translates to: + /// **'Past'** + String get mapTimelinePast; + + /// Restroom type: female restroom + /// + /// In en, this message translates to: + /// **'Female'** + String get restroomTypeFemale; + + /// Date section header for reports that originated today (Taipei) + /// + /// In en, this message translates to: + /// **'Today'** + String get reportListToday; + + /// Resting state of the map node sheet + /// + /// In en, this message translates to: + /// **'Tap a node for details'** + String get meshtasticTapNode; + + /// Generic loading label for an async view + /// + /// In en, this message translates to: + /// **'Loading…'** + String get commonLoading; + + /// CWA class: moderate typhoon (past-track colour) + /// + /// In en, this message translates to: + /// **'Moderate typhoon'** + String get typhoonIntensityModerate; + + /// No description provided for @typhoonWind. + /// + /// In en, this message translates to: + /// **'Wind'** + String get typhoonWind; /// Himawari Ash RGB composite layer name /// @@ -1227,17 +2733,35 @@ abstract class AppLocalizations { /// **'Himawari Ash'** String get mapLayerSatelliteAsh; - /// Himawari Dust RGB composite layer name + /// No description provided for @rainInterval3h. /// /// In en, this message translates to: - /// **'Himawari Dust'** - String get mapLayerSatelliteDust; + /// **'3 h'** + String get rainInterval3h; - /// Himawari Airmass RGB composite layer name + /// Fetches the report list with the current draft filters /// /// In en, this message translates to: - /// **'Himawari Airmass'** - String get mapLayerSatelliteAirmass; + /// **'Search'** + String get reportListSearch; + + /// Section title in map overlay lists: satellite-imagery overlays + /// + /// In en, this message translates to: + /// **'Satellite'** + String get mapLayerCategorySatellite; + + /// The DPIP channel exists on the radio + /// + /// In en, this message translates to: + /// **'DPIP channel ready'** + String get meshtasticChannelReady; + + /// Label for the location keyword filter field + /// + /// In en, this message translates to: + /// **'Location'** + String get reportFilterLocation; /// Himawari Night Microphysics RGB composite layer name /// @@ -1245,2225 +2769,2279 @@ abstract class AppLocalizations { /// **'Himawari Night Microphysics'** String get mapLayerSatelliteNightmicrophysics; - /// Himawari water-vapour layer name + /// CWA class: tropical depression (past-track colour) /// /// In en, this message translates to: - /// **'Himawari Water Vapour'** - String get mapLayerSatelliteWatervapor; + /// **'Tropical depression'** + String get typhoonIntensityTd; - /// Himawari split-window brightness-temperature-difference layer name + /// Label for the origin-time date-range filter /// /// In en, this message translates to: - /// **'Himawari Split Window'** - String get mapLayerSatelliteBtdSplit; + /// **'Date'** + String get reportFilterDate; - /// Himawari night fog / low-cloud brightness-temperature-difference layer name + /// Snackbar shown when the store can't be reached to restore /// /// In en, this message translates to: - /// **'Himawari Night Fog'** - String get mapLayerSatelliteBtdFog; + /// **'Can\'t reach the store. Please try again later.'** + String get sponsorRestoreUnavailable; - /// Himawari overshooting-cloud-top brightness-temperature-difference layer name + /// Probability of precipitation percent on a forecast hour chip /// /// In en, this message translates to: - /// **'Himawari Overshooting Top'** - String get mapLayerSatelliteBtdWvirw; + /// **'{pop}%'** + String homeForecastPop(String pop); - /// Himawari SO₂ / cloud-phase brightness-temperature-difference layer name + /// Empty state on the saved-regions manage page /// /// In en, this message translates to: - /// **'Himawari SO₂ / Cloud Phase'** - String get mapLayerSatelliteBtdSo2; + /// **'No saved regions yet'** + String get regionEmpty; - /// Himawari cirrus / cloud-height brightness-temperature-difference layer name + /// Permission row description: battery /// /// In en, this message translates to: - /// **'Himawari Cirrus / Cloud Height'** - String get mapLayerSatelliteBtdCo2; + /// **'Allow DPIP to keep running in the background so alerts aren\'t delayed or missed.'** + String get onboardingPermBatteryDesc; - /// Himawari tropopause brightness-temperature-difference layer name + /// Short Map-tab bottom-nav / default-layer picker label for disaster-prevention map /// /// In en, this message translates to: - /// **'Himawari Tropopause'** - String get mapLayerSatelliteBtdOzone; + /// **'Disaster'** + String get mapNavDisaster; - /// Himawari cloud-top-temperature (Level-2 retrieval) layer name + /// Radar scan-range overlay toggle in the map's radar overlay menu. /// /// In en, this message translates to: - /// **'Himawari Cloud Top Temperature'** - String get mapLayerSatelliteCloudtop; + /// **'Outlines the area the four radars actually observe.'** + String get radarScanRangeSubtitle; - /// Himawari cloud-mask (Level-2 retrieval) layer name + /// AED Sunday opening hours row label /// /// In en, this message translates to: - /// **'Himawari Cloud Mask'** - String get mapLayerSatelliteCloudmask; + /// **'Sunday hours'** + String get aedHoursSunday; - /// Himawari sea-surface-temperature (ACSPO L3C) layer name + /// Row label for the report's origin date/time /// /// In en, this message translates to: - /// **'Himawari Sea Surface Temperature'** - String get mapLayerSatelliteSst; + /// **'Origin time'** + String get reportDetailOriginTime; - /// Himawari normalised-difference vegetation-index layer name + /// Shown in the station trend chart when there is no data to plot /// /// In en, this message translates to: - /// **'Himawari NDVI'** - String get mapLayerSatelliteNdvi; + /// **'No trend data'** + String get trendNoData; - /// Himawari normalised-difference water-index layer name + /// Permission row: location /// /// In en, this message translates to: - /// **'Himawari NDWI'** - String get mapLayerSatelliteNdwi; + /// **'Location'** + String get onboardingPermLocation; - /// Himawari modified normalised-difference water-index layer name + /// More-menu link to the ExpTech Discord community /// /// In en, this message translates to: - /// **'Himawari MNDWI'** - String get mapLayerSatelliteMndwi; + /// **'Discord community'** + String get moreDiscord; - /// Satellite legend row: the country/global border, drawn bright yellow over the imagery + /// Short Map-tab bottom-nav / default-layer picker label for pressure /// /// In en, this message translates to: - /// **'Country border'** - String get mapLayerSatelliteGlobalOutline; + /// **'Pressure'** + String get mapNavPressure; - /// Satellite legend note for the RGB-recipe products (True Color, Ash, …) that carry no single numerical scale + /// Himawari clean-infrared window channel (B13, 10.4 µm) layer name /// /// In en, this message translates to: - /// **'RGB composite (JMA recipe)'** - String get mapLayerSatelliteRgbComposite; + /// **'Himawari Infrared (B13)'** + String get mapLayerSatelliteB13; - /// Cloud-mask category: clear sky, transparent on the map + /// Secondary badge on the typhoon sheet hero: the CWA tropical-depression serial number, e.g. TD 14 /// /// In en, this message translates to: - /// **'Clear'** - String get mapLayerSatelliteCloudClear; + /// **'TD {no}'** + String typhoonTdNo(String no); - /// Cloud-mask category: probably clear + /// Empty state when the releases API returns nothing /// /// In en, this message translates to: - /// **'Probably clear'** - String get mapLayerSatelliteCloudProbablyClear; + /// **'No release notes yet'** + String get changelogEmpty; - /// Cloud-mask category: probably cloudy + /// Explains that startTime covers from midnight on that calendar day /// /// In en, this message translates to: - /// **'Probably cloudy'** - String get mapLayerSatelliteCloudProbablyCloudy; + /// **'Start day: from 00:00 (Taipei)'** + String get reportFilterDateStartNote; - /// Cloud-mask category: cloudy + /// Header of the earthquake monitor when one or more alerts are active /// /// In en, this message translates to: - /// **'Cloudy'** - String get mapLayerSatelliteCloudCloudy; + /// **'Earthquake early warning'** + String get eewTitle; + + /// Map layer switcher label for the ECMWF wind-forecast layer + /// + /// In en, this message translates to: + /// **'ECMWF'** + String get mapLayerWindForecastEcmwf; + + /// Header showing how many saved-region slots are used + /// + /// In en, this message translates to: + /// **'{count}/{max} selected'** + String regionSelectCount(int count, int max); + + /// Himawari SO₂ / cloud-phase brightness-temperature-difference layer name + /// + /// In en, this message translates to: + /// **'Himawari SO₂ / Cloud Phase'** + String get mapLayerSatelliteBtdSo2; + + /// Connection state label + /// + /// In en, this message translates to: + /// **'Error'** + String get meshtasticStateError; + + /// Label for the weatherModeOvercast option in the experimental backdrop settings. + /// + /// In en, this message translates to: + /// **'Overcast'** + String get weatherModeOvercast; + + /// Row label for the report's hypocentral depth + /// + /// In en, this message translates to: + /// **'Depth'** + String get reportDetailDepth; + + /// Tooltip for the warning-areas overlay toggle + /// + /// In en, this message translates to: + /// **'Highlight counties under a typhoon warning'** + String get typhoonOverlayWarningTooltip; + + /// Button to open the date-range picker when none selected + /// + /// In en, this message translates to: + /// **'Pick dates'** + String get reportFilterDatePick; + + /// Dismiss the skip dialog and return to grant permissions + /// + /// In en, this message translates to: + /// **'Go back'** + String get onboardingSkipStay; - /// Satellite legend note: on the IR grayscale/enhancements the warm end is clear sky, drawn transparent so the basemap shows + /// Error headline when a data request (AsyncView) fails, with a retry button /// /// In en, this message translates to: - /// **'Clear sky (warm end) = transparent, the basemap shows'** - String get mapLayerSatelliteTransparentWarm; + /// **'Couldn\'t load data. Please try again.'** + String get commonFetchFailed; - /// Satellite legend note: on the reflectance bands a dark or night pixel is transparent so the basemap shows + /// Shelter detail row: whether outdoor shelter is provided /// /// In en, this message translates to: - /// **'Low reflectance / night = transparent, the basemap shows'** - String get mapLayerSatelliteTransparentReflectance; + /// **'Outdoor shelter'** + String get shelterOutdoorLabel; - /// Satellite legend note: on the brightness-temperature-difference layers a near-zero difference is transparent — no absorber is present + /// Connection state label /// /// In en, this message translates to: - /// **'Zero difference = transparent (no signal)'** - String get mapLayerSatelliteTransparentZero; + /// **'Connected'** + String get meshtasticStateConnected; - /// Satellite legend note: the daytime RGB recipes fade out across the terminator and are transparent at night + /// Short Map-tab bottom-nav / default-layer picker label for radar /// /// In en, this message translates to: - /// **'Night = transparent, the basemap shows'** - String get mapLayerSatelliteTransparentNight; + /// **'Radar'** + String get mapNavRadar; - /// Satellite legend note: the SST retrieval has no value over land, drawn transparent + /// Cloud-mask category: clear sky, transparent on the map /// /// In en, this message translates to: - /// **'No data (land) = transparent'** - String get mapLayerSatelliteTransparentNoData; + /// **'Clear'** + String get mapLayerSatelliteCloudClear; - /// Satellite legend note: NDVI below the bare-soil threshold is transparent + /// One-line summary of an EEW alert's magnitude and depth /// /// In en, this message translates to: - /// **'Below 0.1 = transparent (no vegetation)'** - String get mapLayerSatelliteTransparentNoVegetation; + /// **'M{magnitude} · depth {depth} km'** + String eewSummary(String magnitude, String depth); - /// Satellite legend note: NDWI/MNDWI at zero or below is transparent — no water signal + /// Banner when location permission is denied /// /// In en, this message translates to: - /// **'≤ 0 = transparent (no water)'** - String get mapLayerSatelliteTransparentNoWater; + /// **'Location permission is off — local alerts can\'t target your area.'** + String get locationBannerPermission; - /// Satellite legend note: the cloud-mask clear category is transparent so the basemap shows + /// Tooltip for clearing the weather underlay /// /// In en, this message translates to: - /// **'Clear sky = transparent, the basemap shows'** - String get mapLayerSatelliteTransparentClear; + /// **'No radar or infrared underlay'** + String get typhoonOverlayWeatherNoneTooltip; - /// Section header of the satellite band colour-style menu on the map + /// Hint under the county-border toggle in the radar overlay menu. /// /// In en, this message translates to: - /// **'Colour style'** - String get mapLayerStyleSection; + /// **'Drawn over the echo'** + String get radarCountyOutlineHint; - /// Tooltip of the colour-style chip beside the layer switcher + /// Hint under the county-border toggle in the wind-forecast overlay menu. /// /// In en, this message translates to: - /// **'Colour style'** - String get mapLayerStyleTooltip; + /// **'Drawn over the wind field'** + String get windForecastCountyOutlineHint; - /// Colour-style option: JMA grayscale, the default radar-image convention + /// Section title for the home sheet 1-hour per-minute rainfall bar chart /// /// In en, this message translates to: - /// **'Grayscale (JMA)'** - String get mapLayerStyleGray; + /// **'Next hour precipitation'** + String get homeRainTrendTitle; - /// Explains the JMA grayscale band rendering + /// Phase: first quarter /// /// In en, this message translates to: - /// **'JMA grayscale — colder is whiter'** - String get mapLayerStyleGrayTooltip; + /// **'First quarter'** + String get moonPhaseFirstQuarter; - /// Colour-style option: JMA cloud-top enhancement, tinted below −40 °C + /// Section title in map overlay lists: typhoon overlays /// /// In en, this message translates to: - /// **'Cloud-top enhancement (JMA)'** - String get mapLayerStyleJma; + /// **'Typhoon'** + String get mapLayerCategoryTyphoon; - /// Explains the JMA cloud-top enhancement band rendering + /// Section title for the 24h airtime chart /// /// In en, this message translates to: - /// **'Grayscale base, tinted below −40 °C to highlight cloud-top height'** - String get mapLayerStyleJmaTooltip; + /// **'Airtime (24h)'** + String get meshtasticUtilization; - /// Colour-style option: Dvorak BD curve stepped grayscale + /// Restroom type: mixed/unisex restroom /// /// In en, this message translates to: - /// **'Dvorak BD'** - String get mapLayerStyleBd; + /// **'Mixed'** + String get restroomTypeMixed; - /// Explains the Dvorak BD band rendering + /// Restroom cleanliness grade: good /// /// In en, this message translates to: - /// **'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'** - String get mapLayerStyleBdTooltip; + /// **'Good'** + String get restroomGradeGood; - /// Name of the QPESUMS next-1-hour precipitation forecast layer in the layer picker + /// Notify channel title /// /// In en, this message translates to: - /// **'1h Precipitation Forecast'** - String get mapLayerQpesums; + /// **'Tsunami information'** + String get notifyTsunami; - /// Map layer switcher label for the lightning strike timeline + /// Bottom-nav label and page title for the Data hub tab /// /// In en, this message translates to: - /// **'Lightning'** - String get mapLayerLightning; + /// **'Data'** + String get navData; - /// Lightning legend: cloud-to-ground strike within N minutes + /// Himawari overshooting-cloud-top brightness-temperature-difference layer name /// /// In en, this message translates to: - /// **'Cloud-to-ground · {minutes} min'** - String lightningLegendCg(int minutes); + /// **'Himawari Overshooting Top'** + String get mapLayerSatelliteBtdWvirw; - /// Lightning legend: cloud-to-cloud strike within N minutes + /// How old the battery/airtime numbers are /// /// In en, this message translates to: - /// **'Cloud-to-cloud · {minutes} min'** - String lightningLegendCc(int minutes); + /// **'Reading taken'** + String get meshtasticReadingAge; - /// Label on the map timeline when the newest (latest) frame is selected + /// Snackbar when tapping the emergency phone and the device has no phone handler /// /// In en, this message translates to: - /// **'Now'** - String get mapTimelineNow; + /// **'This device cannot make phone calls'** + String get mapAppCallFailed; - /// Label on the map timeline when the selected frame predates the present + /// Chip / slider label meaning no filter applied /// /// In en, this message translates to: - /// **'Past'** - String get mapTimelinePast; + /// **'Any'** + String get reportFilterAny; - /// Label on the map timeline when the selected frame postdates the present + /// Label before township/county merge chips on ranking /// /// In en, this message translates to: - /// **'Future'** - String get mapTimelineFuture; + /// **'Merge to'** + String get weatherRankingMergeTo; - /// Label above the map timeline's date (the radar observation time), e.g. Observed / 2026/07/14 + /// Notify channel title /// /// In en, this message translates to: - /// **'Observed'** - String get mapTimelineObserved; + /// **'Intensity report'** + String get notifyIntensity; - /// Label above the map timeline's date when the frame times are forecast times, e.g. Forecast / 2026/07/14 + /// Compact typhoon time chip / map label shape (day + hour, no month) /// /// In en, this message translates to: - /// **'Forecast'** - String get mapTimelineForecast; + /// **'{day}日{hour}時'** + String typhoonTimeChip(String day, String hour); - /// Model-run issue time shown on the map timeline under a forecast layer's caption, e.g. Data 8/11 14:00 + /// Tooltip for the rainfall accumulation-interval menu /// /// In en, this message translates to: - /// **'Data {time}'** - String mapTimelineDataTime(String time); + /// **'Accumulation window'** + String get rainIntervalMenu; - /// More-menu entry that opens the notification-settings page + /// Eyebrow label on the detail header for a …000 (unnumbered) report /// /// In en, this message translates to: - /// **'Notification settings'** - String get notifySettingsMenu; + /// **'Local Felt Earthquake'** + String get reportDetailLocalFelt; - /// Title of the notification-settings page + /// Section: device identity /// /// In en, this message translates to: - /// **'Notifications'** - String get notifyTitle; + /// **'Device'** + String get meshtasticDevice; - /// Shown on the notify page when there is no push token yet + /// Permission grant button /// /// In en, this message translates to: - /// **'Push notifications aren\'t ready yet — try again shortly.'** - String get notifyUnavailable; + /// **'Grant'** + String get onboardingGrant; - /// Snackbar shown when saving a notification channel fails + /// Weather animation forced to rain /// /// In en, this message translates to: - /// **'Couldn\'t save the setting. Please try again.'** - String get notifySetFailed; + /// **'Rain'** + String get weatherModeRain; - /// Notify page section header + /// Shelter detail row: whether evacuees needing care can be accommodated /// /// In en, this message translates to: - /// **'Earthquake early warning'** - String get notifySectionEew; + /// **'Vulnerable-people friendly'** + String get shelterVulnerableOkLabel; - /// Notify page section header + /// Empty-state hint in the map station-value sheet, shown before any station is selected /// /// In en, this message translates to: - /// **'Earthquake'** - String get notifySectionEarthquake; + /// **'Tap a station to see its reading'** + String get stationSheetEmpty; - /// Notify page section header + /// Typhoon UI: typhoonLegendProbability /// /// In en, this message translates to: - /// **'Weather'** - String get notifySectionWeather; + /// **'Strike probability'** + String get typhoonLegendProbability; - /// Notify page section header + /// Label for the magnitude range filter /// /// In en, this message translates to: - /// **'Tsunami'** - String get notifySectionTsunami; + /// **'Magnitude'** + String get reportFilterMagnitude; - /// Notify page section header + /// Label for the skyTimeMorning option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Other'** - String get notifySectionOther; + /// **'Morning'** + String get skyTimeMorning; - /// Notify channel title + /// Title of the experimental-features settings page and its More-menu entry /// /// In en, this message translates to: - /// **'Emergency earthquake alert'** - String get notifyEew; + /// **'Experimental features'** + String get experimentalFeatures; - /// Notify channel title + /// Onboarding terms of service body /// /// In en, this message translates to: - /// **'Strong-motion monitor'** - String get notifyMonitor; + /// **'Please read the following notices before using DPIP:\n\n• All information should defer to the content published by the Central Weather Administration (CWA).\n\n• Depending on network, server, app, and upstream data-source conditions, information may not be received; we make every effort to avoid this but cannot guarantee it never happens.\n\n• Strong shaking may reach your location before the notification does.\n\n• Earthquake early warnings are fast-computed results that may carry significant error — understand this and use them with caution.\n\n• Any behavior not sanctioned by the authorities may carry legal risk; please follow all applicable regulations.\n\nIn addition, to provide localized alerts, this service collects and uploads your approximate location and push identifier — in the foreground and background — solely to decide which alerts to send you.\n\nBy tapping \"Agree and continue\" you confirm that you have read, understood, and agree to the above.'** + String get onboardingTermsBody; - /// Notify channel title + /// Title of the earthquake report filter sheet /// /// In en, this message translates to: - /// **'Earthquake report'** - String get notifyReport; + /// **'Filters'** + String get reportFilterTitle; - /// Notify channel title + /// Permission row: critical alerts (iOS) /// /// In en, this message translates to: - /// **'Intensity report'** - String get notifyIntensity; + /// **'Critical alerts'** + String get onboardingPermCritical; - /// Notify channel title + /// Running total label above the cumulative station rain trend chart /// /// In en, this message translates to: - /// **'Thunderstorm alerts'** - String get notifyThunderstorm; + /// **'Cumulative {total} mm'** + String trendCumulativeTotal(String total); - /// Notify channel title + /// This language's own name, shown in the in-app language picker. Each locale's ARB names itself; the picker is built from these, never a hardcoded list. /// /// In en, this message translates to: - /// **'Weather advisories'** - String get notifyAdvisory; + /// **'English'** + String get languageName; - /// Notify channel title + /// Empty state when active filters yield no report rows /// /// In en, this message translates to: - /// **'Disaster information'** - String get notifyEvacuation; + /// **'No earthquake reports match these filters'** + String get reportListEmptyFiltered; - /// Notify channel title + /// Toggle hiding internet-bridged nodes /// /// In en, this message translates to: - /// **'Tsunami information'** - String get notifyTsunami; + /// **'Hide MQTT nodes'** + String get meshtasticExcludeMqtt; - /// Notify channel title + /// Short Map-tab bottom-nav / default-layer picker label for typhoon /// /// In en, this message translates to: - /// **'Announcements'** - String get notifyAnnouncement; + /// **'Typhoon'** + String get mapNavTyphoon; - /// Notify option label + /// Label for the weatherModeSand option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Off'** - String get notifyOptOff; + /// **'Dust'** + String get weatherModeSand; - /// Notify option label + /// Typhoon UI: typhoonSatelliteTitle /// /// In en, this message translates to: - /// **'Receive all'** - String get notifyOptAll; + /// **'Satellite'** + String get typhoonSatelliteTitle; - /// Notify option label + /// Notify channel title /// /// In en, this message translates to: - /// **'Local intensity 4 or above'** - String get notifyOptLocalIntensity4; + /// **'Earthquake report'** + String get notifyReport; - /// Notify option label + /// Snackbar confirming the coordinates were copied /// /// In en, this message translates to: - /// **'Local intensity 1 or above'** - String get notifyOptLocalIntensity1; + /// **'Coordinates copied'** + String get mapAppCoordinatesCopied; - /// Notify option label + /// Label for the skyTimeNight option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Current location only'** - String get notifyOptWeatherLocal; + /// **'Night'** + String get skyTimeNight; - /// Notify option label + /// Badge on the recommended (subscription) support section /// /// In en, this message translates to: - /// **'Tsunami warnings only'** - String get notifyOptTsunamiWarning; + /// **'Recommended'** + String get sponsorRecommended; - /// Notify option label + /// Himawari longwave-infrared channel (B15, 12.4 µm) layer name /// /// In en, this message translates to: - /// **'Tsunami advisories and warnings'** - String get notifyOptTsunamiAll; + /// **'Himawari Longwave Infrared (B15)'** + String get mapLayerSatelliteB15; - /// Onboarding next-step button + /// Ranking tab/tile for sustained wind speed /// /// In en, this message translates to: - /// **'Next'** - String get onboardingNext; + /// **'Wind speed'** + String get weatherRankingWind; - /// Onboarding back button + /// Banner over a realtime feed whose data has aged past the freshness threshold /// /// In en, this message translates to: - /// **'Back'** - String get onboardingBack; + /// **'Data may be out of date'** + String get feedStale; - /// Hint shown until the user scrolls to the end + /// Wind direction string and Beaufort force for the selected hour /// /// In en, this message translates to: - /// **'Scroll down to continue'** - String get onboardingScrollHint; + /// **'{direction} · Force {level}'** + String homeForecastWind(String direction, String level); - /// Onboarding intro page title + /// Bottom-nav label and page title for the Home tab /// /// In en, this message translates to: - /// **'Welcome to DPIP'** - String get onboardingIntroTitle; + /// **'Home'** + String get navHome; - /// Onboarding intro page body + /// LoRa region /// /// In en, this message translates to: - /// **'DPIP is your disaster-prevention companion. It brings together earthquake early warnings, earthquake reports, weather, and hazard information, and alerts you the moment it matters.\n\n• Earthquakes: early warnings, intensity reports, and detailed reports\n• Weather: real-time thunderstorm messages and weather advisories\n• Tsunami and disaster information\n\nNext, we\'ll ask you to review the Terms of Service and grant a few permissions so DPIP can protect you in real time.'** - String get onboardingIntroBody; + /// **'Region'** + String get meshtasticRegionLabel; - /// Onboarding terms page title + /// Himawari cloud-top-temperature (Level-2 retrieval) layer name /// /// In en, this message translates to: - /// **'Terms of Service'** - String get onboardingTermsTitle; + /// **'Himawari Cloud Top Temperature'** + String get mapLayerSatelliteCloudtop; - /// Onboarding terms of service body + /// Moon phase timeline caption /// /// In en, this message translates to: - /// **'Please read the following notices before using DPIP:\n\n• All information should defer to the content published by the Central Weather Administration (CWA).\n\n• Depending on network, server, app, and upstream data-source conditions, information may not be received; we make every effort to avoid this but cannot guarantee it never happens.\n\n• Strong shaking may reach your location before the notification does.\n\n• Earthquake early warnings are fast-computed results that may carry significant error — understand this and use them with caution.\n\n• Any behavior not sanctioned by the authorities may carry legal risk; please follow all applicable regulations.\n\nIn addition, to provide localized alerts, this service collects and uploads your approximate location and push identifier — in the foreground and background — solely to decide which alerts to send you.\n\nBy tapping \"Agree and continue\" you confirm that you have read, understood, and agree to the above.'** - String get onboardingTermsBody; + /// **'Phase'** + String get moonTimelineCaption; - /// Terms agreement checkbox label + /// Magnitude and depth line on a report list row /// /// In en, this message translates to: - /// **'I have read and agree to the Terms of Service'** - String get onboardingTermsAgree; + /// **'M{magnitude} · {depth} km'** + String reportListMeta(String magnitude, String depth); - /// Terms page continue button + /// More-menu entry that opens the bundled open-source license list /// /// In en, this message translates to: - /// **'Agree and continue'** - String get onboardingAgreeContinue; + /// **'Open-source licenses'** + String get openSourceLicenses; - /// Onboarding permissions page title + /// Chip to rank temperature ascending /// /// In en, this message translates to: - /// **'Permissions'** - String get onboardingPermsTitle; + /// **'Lowest'** + String get weatherRankingLowest; - /// Onboarding permissions page intro + /// Sort reports by hypocentral depth /// /// In en, this message translates to: - /// **'So DPIP can alert you the moment disaster strikes, please grant the following. You can change these anytime in system settings.'** - String get onboardingPermsBody; + /// **'Depth'** + String get reportFilterSortDepth; - /// Permission row: notifications + /// Model-run issue time shown on the map timeline under a forecast layer's caption, e.g. Data 8/11 14:00 /// /// In en, this message translates to: - /// **'Notifications'** - String get onboardingPermNotify; + /// **'Data {time}'** + String mapTimelineDataTime(String time); - /// Permission row description: notifications + /// Radar scan-range overlay toggle in the map's radar overlay menu. /// /// In en, this message translates to: - /// **'Deliver earthquake, weather, and disaster alerts the moment they happen.'** - String get onboardingPermNotifyDesc; + /// **'Show scan range'** + String get radarScanRange; - /// Permission row: critical alerts (iOS) + /// How many hops a packet may take /// /// In en, this message translates to: - /// **'Critical alerts'** - String get onboardingPermCritical; + /// **'Hop limit'** + String get meshtasticHopLimit; - /// Permission row description: critical alerts + /// Diurnal range fragment in an extremes analysis line /// /// In en, this message translates to: - /// **'Let life-threatening earthquake warnings sound even in silent mode or Do Not Disturb.'** - String get onboardingPermCriticalDesc; + /// **'Range {value}°C'** + String weatherRankingAnalysisRange(String value); - /// Permission row: location + /// Chip to rank by recorded daily maximum temperature /// /// In en, this message translates to: - /// **'Location'** - String get onboardingPermLocation; + /// **'Daily high'** + String get weatherRankingExtremeHigh; - /// Permission row description: location + /// App bar title on a single release's detail page /// /// In en, this message translates to: - /// **'Target alerts to where you are.'** - String get onboardingPermLocationDesc; + /// **'Release details'** + String get changelogVersionDetails; - /// Permission row: background/Always location + /// Footer link to the Privacy Policy /// /// In en, this message translates to: - /// **'Background location'** - String get onboardingPermBackground; + /// **'Privacy Policy'** + String get sponsorPrivacy; - /// Permission row description: background location + /// Section header over the per-location (GPS + saved townships) felt-intensity readout, shown above the area breakdown /// /// In en, this message translates to: - /// **'Allow \"Always\" so alerts still target you when the app is closed.'** - String get onboardingPermBackgroundDesc; + /// **'Intensity at your locations'** + String get reportDetailLocalIntensity; - /// Permission row: battery optimization (Android) + /// Himawari Natural Color RGB composite layer name /// /// In en, this message translates to: - /// **'Battery exemption'** - String get onboardingPermBattery; + /// **'Himawari Natural Color'** + String get mapLayerSatelliteNaturalcolor; - /// Permission row description: battery + /// Share of airtime this radio transmitted /// /// In en, this message translates to: - /// **'Allow DPIP to keep running in the background so alerts aren\'t delayed or missed.'** - String get onboardingPermBatteryDesc; + /// **'Air time (TX)'** + String get meshtasticAirtime; - /// Permission grant button + /// Shelter detail capacity row value /// /// In en, this message translates to: - /// **'Grant'** - String get onboardingGrant; + /// **'{n} people'** + String shelterCapacityValue(int n); - /// Permission granted label + /// Lightning legend: cloud-to-cloud strike within N minutes /// /// In en, this message translates to: - /// **'Granted'** - String get onboardingGranted; + /// **'Cloud-to-cloud · {minutes} min'** + String lightningLegendCc(int minutes); - /// Onboarding finish button + /// Message input hint /// /// In en, this message translates to: - /// **'Get started'** - String get onboardingStart; + /// **'Message to broadcast'** + String get meshtasticSendHint; - /// Language picker tooltip / label + /// RTS monitor latency: how far behind the latest snapshot is (calibrated now minus the snapshot timestamp), in seconds — pre-formatted to one decimal, e.g. "0.3" /// /// In en, this message translates to: - /// **'Language'** - String get language; + /// **'Delay {value} s'** + String monitorDelay(String value); - /// Label next to the language picker on the welcome screen + /// Negative value in the disaster-map detail sheet /// /// In en, this message translates to: - /// **'Language'** - String get languageSettings; + /// **'No'** + String get dpmNo; - /// Language picker option: follow the system language + /// Himawari upper-level water-vapour channel (B08, 6.2 µm) layer name /// /// In en, this message translates to: - /// **'System default'** - String get languageSystem; + /// **'Himawari Upper Water Vapour (B08)'** + String get mapLayerSatelliteB08; - /// Banner when the OS location toggle is off + /// The link dropped and is being re-established /// /// In en, this message translates to: - /// **'Location services are off — local alerts can\'t target your area.'** - String get locationBannerServiceOff; + /// **'Reconnecting…'** + String get meshtasticReconnecting; - /// Banner when location permission is denied + /// Township-border overlay toggle in the map's radar overlay menu. /// /// In en, this message translates to: - /// **'Location permission is off — local alerts can\'t target your area.'** - String get locationBannerPermission; + /// **'Keeps township borders legible under the radar echo.'** + String get radarTownOutlineSubtitle; - /// Action on the location banner to open system settings + /// Tooltip for Himawari IR underlay (mutex with radar) /// /// In en, this message translates to: - /// **'Open settings'** - String get locationBannerFix; + /// **'Infrared closest to the typhoon bulletin time'** + String get typhoonOverlayWeatherSatelliteTooltip; - /// App-wide banner shown when notification permission is disabled + /// Hint under the radar scan-range toggle in the radar overlay menu. /// /// In en, this message translates to: - /// **'Notifications are off — you won\'t receive disaster alerts.'** - String get notifyBannerDisabled; + /// **'Blank outside means unobserved'** + String get radarScanRangeHint; - /// Title of the confirm dialog shown when finishing onboarding without key permissions + /// Sheet picker: unnamed tropical depression (CWA tdNo) /// /// In en, this message translates to: - /// **'Permissions not granted'** - String get onboardingSkipTitle; + /// **'Tropical depression TD {no}'** + String typhoonPickerTd(String no); - /// Body of the skip-permissions confirmation dialog + /// Himawari water-vapour layer name /// /// In en, this message translates to: - /// **'Without location and notifications, DPIP can\'t alert you to earthquakes and disasters near you in real time. You can still grant them later in Settings.'** - String get onboardingSkipBody; + /// **'Himawari Water Vapour'** + String get mapLayerSatelliteWatervapor; - /// Dismiss the skip dialog and return to grant permissions + /// Button to open the region picker to add a saved region /// /// In en, this message translates to: - /// **'Go back'** - String get onboardingSkipStay; + /// **'Add a region'** + String get regionAddButton; - /// Proceed past onboarding without granting permissions + /// Display-settings menu entry and page title (theme mode) /// /// In en, this message translates to: - /// **'Skip anyway'** - String get onboardingSkipLeave; + /// **'Display'** + String get displaySettings; - /// More-menu link to the ExpTech YouTube channel + /// Restroom cleanliness grade: below standard /// /// In en, this message translates to: - /// **'YouTube'** - String get moreYoutube; + /// **'Below standard'** + String get restroomGradePoor; - /// More-menu link to the ExpTech GitHub organisation + /// Restroom venue category: tourist area / scenic spot /// /// In en, this message translates to: - /// **'ExpTech GitHub'** - String get moreGithub; + /// **'Tourist'** + String get restroomCategoryTourist; - /// More-menu link to DPIP's source repository on GitHub + /// Banner when the OS location toggle is off /// /// In en, this message translates to: - /// **'Source code'** - String get moreSourceCode; + /// **'Location services are off — local alerts can\'t target your area.'** + String get locationBannerServiceOff; - /// More-page section header for the app-store download links + /// Tooltip of the colour-style chip beside the layer switcher /// /// In en, this message translates to: - /// **'Get the app'** - String get moreSectionApp; + /// **'Colour style'** + String get mapLayerStyleTooltip; - /// Google Play store link title (brand name) + /// Lightning legend: cloud-to-ground strike within N minutes /// /// In en, this message translates to: - /// **'Google Play'** - String get moreGooglePlay; + /// **'Cloud-to-ground · {minutes} min'** + String lightningLegendCg(int minutes); - /// Apple App Store link title (brand name) + /// Label for the skyTimeAuto option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'App Store'** - String get moreAppStore; + /// **'Auto'** + String get skyTimeAuto; - /// Display-settings menu entry and page title (theme mode) + /// Title of the in-app log viewer and its entry in the More menu /// /// In en, this message translates to: - /// **'Display'** - String get displaySettings; + /// **'App logs'** + String get appLogs; - /// More-menu entry and page title for choosing the Map tab's default overlay + /// A realtime feed is establishing its first data /// /// In en, this message translates to: - /// **'Default map layer'** - String get defaultMapLayerSettings; + /// **'Connecting…'** + String get feedConnecting; - /// Explanatory subtitle on the default-map-layer settings page + /// App-wide banner shown when notification permission is disabled /// /// In en, this message translates to: - /// **'The Map tab opens on this overlay. The bottom-navigation icon and label follow this choice.'** - String get defaultMapLayerSubtitle; + /// **'Notifications are off — you won\'t receive disaster alerts.'** + String get notifyBannerDisabled; - /// Short Map-tab bottom-nav / default-layer picker label for radar + /// Label for the humidity metric in the home weather header /// /// In en, this message translates to: - /// **'Radar'** - String get mapNavRadar; + /// **'Humidity'** + String get weatherHumidity; - /// Short Map-tab bottom-nav / default-layer picker label for the 1h QPESUMS precipitation forecast + /// No description provided for @typhoonValueMs. /// /// In en, this message translates to: - /// **'Forecast'** - String get mapNavQpesums; + /// **'{n} m/s'** + String typhoonValueMs(String n); - /// Short Map-tab bottom-nav / default-layer picker label for satellite + /// Relative humidity for the selected forecast hour /// /// In en, this message translates to: - /// **'Satellite'** - String get mapNavSatellite; + /// **'Humidity {value}%'** + String homeForecastHumidity(String value); - /// Short Map-tab bottom-nav / default-layer picker label for lightning + /// Why two clients on one radio is a problem /// /// In en, this message translates to: - /// **'Lightning'** - String get mapNavLightning; + /// **'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'** + String get meshtasticBusyBody; - /// Short Map-tab bottom-nav / default-layer picker label for typhoon + /// Every secondary channel slot is taken /// /// In en, this message translates to: - /// **'Typhoon'** - String get mapNavTyphoon; + /// **'No free channel slot — free one on the radio'** + String get meshtasticChannelNoSlot; - /// Short Map-tab bottom-nav / default-layer picker label for RTS seismic monitor + /// Restroom venue category: transport facility /// /// In en, this message translates to: - /// **'Earthquake'** - String get mapNavEarthquake; + /// **'Transport'** + String get restroomCategoryTransport; - /// Short Map-tab bottom-nav / default-layer picker label for temperature + /// Hint for the location keyword filter field /// /// In en, this message translates to: - /// **'Temperature'** - String get mapNavTemperature; + /// **'e.g. Hualien, offshore'** + String get reportFilterLocationHint; - /// Short Map-tab bottom-nav / default-layer picker label for humidity + /// Moon entry card subtitle in the data catalogue /// /// In en, this message translates to: - /// **'Humidity'** - String get mapNavHumidity; + /// **'Lunar phase and illumination — computed locally'** + String get moonSubtitle; - /// Short Map-tab bottom-nav / default-layer picker label for pressure + /// Battery charge /// /// In en, this message translates to: - /// **'Pressure'** - String get mapNavPressure; + /// **'Battery'** + String get meshtasticBattery; - /// Short Map-tab bottom-nav / default-layer picker label for wind + /// No description provided for @meshtasticDistance. /// /// In en, this message translates to: - /// **'Wind'** - String get mapNavWind; + /// **'Distance'** + String get meshtasticDistance; - /// Short Map-tab bottom-nav / default-layer picker label for rain + /// No description provided for @meshtasticSnrTrend. /// /// In en, this message translates to: - /// **'Rain'** - String get mapNavRain; + /// **'Signal trend (SNR)'** + String get meshtasticSnrTrend; - /// Short Map-tab bottom-nav / default-layer picker label for disaster-prevention map + /// No description provided for @meshtasticBatteryTrend. /// /// In en, this message translates to: - /// **'Disaster'** - String get mapNavDisaster; + /// **'Battery trend'** + String get meshtasticBatteryTrend; - /// Section header for the theme-mode chooser on the Display settings page + /// Tooltip for the typhoon overlay-toggle chip beside the layer switcher /// /// In en, this message translates to: - /// **'Theme'** - String get displayTheme; + /// **'Typhoon overlay options'** + String get typhoonOverlayMenuTooltip; - /// Theme option: follow the system light/dark setting + /// Himawari tropopause brightness-temperature-difference layer name /// /// In en, this message translates to: - /// **'System'** - String get themeSystem; + /// **'Himawari Tropopause'** + String get mapLayerSatelliteBtdOzone; - /// Theme option: always light + /// Radio is on another LoRa region than DPIP needs /// /// In en, this message translates to: - /// **'Light'** - String get themeLight; + /// **'Radio region is {region} — DPIP needs TW'** + String meshtasticRegionMismatch(String region); - /// Theme option: always dark + /// Notify page section header /// /// In en, this message translates to: - /// **'Dark'** - String get themeDark; + /// **'Earthquake'** + String get notifySectionEarthquake; - /// More-menu section header for about / legal links + /// Map layer switcher label for the disaster-prevention map (DPM) /// /// In en, this message translates to: - /// **'About'** - String get moreSectionAbout; + /// **'Disaster Map'** + String get mapLayerDisasterMap; - /// More-menu link title for the Terms of Service + /// Weather animation forced to heavy fog /// /// In en, this message translates to: - /// **'Terms of Service'** - String get termsOfService; + /// **'Fog'** + String get weatherModeFog; - /// More-menu link title for the FAQ / help page + /// Sheet picker: named typhoon (CWA name + TY tyNo) /// /// In en, this message translates to: - /// **'FAQ'** - String get faq; + /// **'{name} TY {no}'** + String typhoonPickerNamed(String no, String name); - /// More-menu entry that opens the bundled open-source license list + /// Explains the JMA grayscale band rendering /// /// In en, this message translates to: - /// **'Open-source licenses'** - String get openSourceLicenses; + /// **'JMA grayscale — colder is whiter'** + String get mapLayerStyleGrayTooltip; - /// Support page title and the More-menu entry that opens it + /// More-menu link to the ExpTech announcements website /// /// In en, this message translates to: - /// **'Support DPIP'** - String get sponsorTitle; + /// **'Announcements'** + String get moreAnnouncements; - /// Support page intro paragraph explaining why donations help + /// Satellite legend note: the SST retrieval has no value over land, drawn transparent /// /// In en, this message translates to: - /// **'DPIP is dedicated to real-time disaster-prevention information, with no ads or other revenue model. Your support helps us keep the servers running and keep developing.'** - String get sponsorIntro; + /// **'No data (land) = transparent'** + String get mapLayerSatelliteTransparentNoData; - /// Support page section header for recurring subscription tiers + /// Restroom venue category: public service office /// /// In en, this message translates to: - /// **'Subscriptions'** - String get sponsorSubscriptions; + /// **'Government'** + String get restroomCategoryGovernment; - /// Badge on the recommended (subscription) support section + /// Typhoon map legend: current storm centre /// /// In en, this message translates to: - /// **'Recommended'** - String get sponsorRecommended; + /// **'Current centre'** + String get typhoonLegendCurrent; - /// Support page section header for one-time tips + /// AED detail row label /// /// In en, this message translates to: - /// **'One-time'** - String get sponsorOneTime; + /// **'Address'** + String get aedAddress; - /// Monthly price label for a subscription; price is the store-localized amount + /// Disaster-map overlay menu toggle for AED (defibrillator) points /// /// In en, this message translates to: - /// **'{price} / month'** - String sponsorPerMonth(String price); + /// **'AED'** + String get mapLayerAed; - /// Footer action that restores previously bought purchases + /// Chip label for a pre-release /// /// In en, this message translates to: - /// **'Restore purchases'** - String get sponsorRestore; + /// **'Beta'** + String get changelogTypePrerelease; - /// Footer link to the Terms of Use + /// No description provided for @reportFilterIntensityInfoModernBody. /// /// In en, this message translates to: - /// **'Terms of Use'** - String get sponsorTerms; + /// **'Levels 0–4, 5−, 5+, 6−, 6+, and 7. The filter slider uses this scale; older events still show legacy labels in the list.'** + String get reportFilterIntensityInfoModernBody; - /// Footer link to the Privacy Policy + /// No radar or satellite underlay /// /// In en, this message translates to: - /// **'Privacy Policy'** - String get sponsorPrivacy; + /// **'None'** + String get typhoonOverlayWeatherNone; - /// Snackbar shown when a purchase restore has been requested + /// Colour-style option: JMA grayscale, the default radar-image convention /// /// In en, this message translates to: - /// **'Restoring purchases…'** - String get sponsorRestoring; + /// **'Grayscale (JMA)'** + String get mapLayerStyleGray; - /// Snackbar shown when the store can't be reached to restore + /// Weather animation follows real conditions /// /// In en, this message translates to: - /// **'Can\'t reach the store. Please try again later.'** - String get sponsorRestoreUnavailable; + /// **'Auto'** + String get weatherModeAuto; - /// Generic close button / action label + /// Forecast point: radius of the 70% track probability circle /// /// In en, this message translates to: - /// **'Close'** - String get commonClose; + /// **'70% probability circle'** + String get typhoonLabelProbCircle; - /// Map layer switcher label for the air-temperature layer + /// Notify option label /// /// In en, this message translates to: - /// **'Temperature'** - String get mapLayerTemperature; + /// **'Receive all'** + String get notifyOptAll; - /// Trend chart range toggle: last 24 hours + /// Section header for the theme-mode chooser on the Display settings page /// /// In en, this message translates to: - /// **'24h'** - String get trendRange24h; + /// **'Theme'** + String get displayTheme; - /// Trend chart range toggle: last 7 days + /// Himawari shortwave-infrared channel (B07, 3.9 µm) layer name /// /// In en, this message translates to: - /// **'7d'** - String get trendRange7d; + /// **'Himawari Shortwave Infrared (B07)'** + String get mapLayerSatelliteB07; - /// Shown in the station trend chart when there is no data to plot + /// Bulletin table row label /// /// In en, this message translates to: - /// **'No trend data'** - String get trendNoData; + /// **'Past movement direction'** + String get typhoonLabelDirection; - /// Running total label above the cumulative station rain trend chart + /// More-menu entry that opens the region picker /// /// In en, this message translates to: - /// **'Cumulative {total} mm'** - String trendCumulativeTotal(String total); + /// **'Saved regions'** + String get regionManageTitle; - /// Compact chart X-axis hour tick (e.g. 20h / 20時) + /// Typhoon map legend: uncertainty cone /// /// In en, this message translates to: - /// **'{hour}h'** - String chartHourLabel(int hour); + /// **'Forecast cone'** + String get typhoonLegendCone; - /// Map layer switcher label for the humidity layer + /// More-menu link to the CWA earthquake early warning publication log website /// /// In en, this message translates to: - /// **'Humidity'** - String get mapLayerHumidity; + /// **'CWA earthquake early warning'** + String get moreCwaEew; - /// Map layer switcher label for the air-pressure layer + /// Onboarding permissions page title /// /// In en, this message translates to: - /// **'Pressure'** - String get mapLayerPressure; + /// **'Permissions'** + String get onboardingPermsTitle; - /// Map layer switcher label for the wind-direction layer + /// Colour-style option: JMA cloud-top enhancement, tinted below −40 °C /// /// In en, this message translates to: - /// **'Wind direction'** - String get mapLayerWind; + /// **'Cloud-top enhancement (JMA)'** + String get mapLayerStyleJma; - /// Map layer switcher label for the rainfall station layer + /// No description provided for @rainInterval10m. /// /// In en, this message translates to: - /// **'Rainfall'** - String get mapLayerRain; + /// **'10 min'** + String get rainInterval10m; - /// Tooltip for the rainfall accumulation-interval menu + /// Daily low fragment; value may include clock time /// /// In en, this message translates to: - /// **'Accumulation window'** - String get rainIntervalMenu; + /// **'Low {value}'** + String weatherRankingAnalysisLow(String value); - /// Rainfall accumulation since local midnight (API now) + /// Connect despite the other app /// /// In en, this message translates to: - /// **'Today'** - String get rainIntervalNow; + /// **'Connect anyway'** + String get meshtasticConnectAnyway; - /// No description provided for @rainInterval10m. + /// Number of reports in a day section /// /// In en, this message translates to: - /// **'10 min'** - String get rainInterval10m; + /// **'{count}'** + String reportListDayCount(int count); - /// No description provided for @rainInterval1h. + /// Himawari near-infrared channel (B06, 2.3 µm) layer name /// /// In en, this message translates to: - /// **'1 h'** - String get rainInterval1h; + /// **'Himawari Near-Infrared (B06)'** + String get mapLayerSatelliteB06; - /// No description provided for @rainInterval3h. + /// Satellite legend note: on the reflectance bands a dark or night pixel is transparent so the basemap shows /// /// In en, this message translates to: - /// **'3 h'** - String get rainInterval3h; + /// **'Low reflectance / night = transparent, the basemap shows'** + String get mapLayerSatelliteTransparentReflectance; - /// No description provided for @rainInterval6h. + /// Compact chart X-axis hour tick (e.g. 20h / 20時) /// /// In en, this message translates to: - /// **'6 h'** - String get rainInterval6h; + /// **'{hour}h'** + String chartHourLabel(int hour); - /// No description provided for @rainInterval12h. + /// Disaster-map overlay menu toggle for evacuation shelters /// /// In en, this message translates to: - /// **'12 h'** - String get rainInterval12h; + /// **'Shelters'** + String get mapLayerShelter; - /// No description provided for @rainInterval24h. + /// Tooltip for the strike-probability toggle; notes mutual exclusion with the cone /// /// In en, this message translates to: - /// **'24 h'** - String get rainInterval24h; + /// **'Show strike probability (hides the forecast cone)'** + String get typhoonOverlayProbabilityTooltip; - /// No description provided for @rainInterval2d. + /// Himawari normalised-difference water-index layer name /// /// In en, this message translates to: - /// **'2 d'** - String get rainInterval2d; + /// **'Himawari NDWI'** + String get mapLayerSatelliteNdwi; - /// No description provided for @rainInterval3d. + /// Tooltip for the shelter toggle in the disaster-map overlay menu /// /// In en, this message translates to: - /// **'3 d'** - String get rainInterval3d; + /// **'Show evacuation shelters'** + String get disasterMapOverlayShelterTooltip; - /// Layer-switcher label for the typhoon map layer + /// Short Map-tab bottom-nav / default-layer picker label for humidity /// /// In en, this message translates to: - /// **'Typhoon'** - String get mapLayerTyphoon; + /// **'Humidity'** + String get mapNavHumidity; - /// No description provided for @typhoonNoActive. + /// Tooltip on the area-intensity sort toggle when tapping it switches to grouping by intensity level /// /// In en, this message translates to: - /// **'No active typhoon'** - String get typhoonNoActive; + /// **'Sort by intensity'** + String get reportDetailSortByIntensity; - /// No description provided for @typhoonWind. + /// Label on the home rain trend chart for minutes beyond the forecast window, and the empty-card hint /// /// In en, this message translates to: - /// **'Wind'** - String get typhoonWind; + /// **'No data'** + String get homeRainTrendNoData; - /// No description provided for @typhoonGust. + /// Section title in map overlay lists: radar and precipitation-forecast overlays /// /// In en, this message translates to: - /// **'Gust'** - String get typhoonGust; + /// **'Radar'** + String get mapLayerCategoryRadar; - /// No description provided for @typhoonPressure. + /// The radio's short name /// /// In en, this message translates to: - /// **'Pressure'** - String get typhoonPressure; + /// **'Short name'** + String get meshtasticShortName; - /// No description provided for @typhoonMotion. + /// Himawari Airmass RGB composite layer name /// /// In en, this message translates to: - /// **'Moving'** - String get typhoonMotion; + /// **'Himawari Airmass'** + String get mapLayerSatelliteAirmass; - /// Bulletin table row label + /// Typhoon UI: typhoonTrackDetail /// /// In en, this message translates to: - /// **'Centre location'** - String get typhoonLabelPosition; + /// **'Track detail'** + String get typhoonTrackDetail; - /// Bulletin table row label + /// Section header on the Data hub for weather observation rankings /// /// In en, this message translates to: - /// **'Past movement direction'** - String get typhoonLabelDirection; + /// **'Weather'** + String get dataSectionWeather; - /// Bulletin table row label + /// AED weekday opening hours row label /// /// In en, this message translates to: - /// **'Past movement speed'** - String get typhoonLabelSpeed; + /// **'Weekday hours'** + String get aedHoursWeekday; - /// Bulletin table row label + /// Section title for currently active disaster notices on the collapsed home sheet /// /// In en, this message translates to: - /// **'Central pressure'** - String get typhoonLabelPressure; + /// **'Active events'** + String get homeActiveEventsTitle; - /// Bulletin table row label + /// Daily high fragment; value may include clock time /// /// In en, this message translates to: - /// **'Max. sustained wind near centre'** - String get typhoonLabelWind; + /// **'High {value}'** + String weatherRankingAnalysisHigh(String value); - /// Bulletin table row label + /// More-menu link title for the FAQ / help page /// /// In en, this message translates to: - /// **'Peak gust'** - String get typhoonLabelGust; + /// **'FAQ'** + String get faq; - /// Bulletin table row label + /// Typhoon UI: typhoonHistoryLive /// /// In en, this message translates to: - /// **'Avg. radius of Beaufort 7 winds'** - String get typhoonLabelGaleAvg; + /// **'Live'** + String get typhoonHistoryLive; - /// Bulletin table row label + /// The serial (report number) of an EEW alert /// /// In en, this message translates to: - /// **'Avg. radius of Beaufort 10 winds'** - String get typhoonLabelStormAvg; + /// **'Report {serial}'** + String eewSerial(int serial); - /// Forecast point: radius of the 70% track probability circle + /// Section title for report list sort field + order /// /// In en, this message translates to: - /// **'70% probability circle'** - String get typhoonLabelProbCircle; + /// **'Sort'** + String get reportFilterSort; - /// Forecast lead time for a tapped track point + /// Confirmation before rebooting the radio /// /// In en, this message translates to: - /// **'Forecast +{hours} h'** - String typhoonForecastLead(String hours); + /// **'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'** + String get meshtasticRegionConfirm; - /// No description provided for @typhoonLabelNw. + /// Subtitle under the Earthquake tile on the Data hub /// /// In en, this message translates to: - /// **'NW'** - String get typhoonLabelNw; + /// **'Earthquake reports'** + String get dataEarthquakeSubtitle; - /// No description provided for @typhoonLabelNe. + /// No description provided for @typhoonNoActive. /// /// In en, this message translates to: - /// **'NE'** - String get typhoonLabelNe; + /// **'No active typhoon'** + String get typhoonNoActive; - /// No description provided for @typhoonLabelSw. + /// Himawari SO₂ absorption channel (B11, 8.6 µm) layer name /// /// In en, this message translates to: - /// **'SW'** - String get typhoonLabelSw; + /// **'Himawari SO₂ / Cloud Phase (B11)'** + String get mapLayerSatelliteB11; - /// No description provided for @typhoonLabelSe. + /// Bottom-nav label and page title for the Events tab /// /// In en, this message translates to: - /// **'SE'** - String get typhoonLabelSe; + /// **'Events'** + String get navEvents; - /// No description provided for @typhoonValueLat. + /// Onboarding terms page title /// /// In en, this message translates to: - /// **'{lat}°N'** - String typhoonValueLat(String lat); + /// **'Terms of Service'** + String get onboardingTermsTitle; - /// No description provided for @typhoonValueLon. + /// Map setting: show township-name labels when the map is zoomed in /// /// In en, this message translates to: - /// **'{lon}°E'** - String typhoonValueLon(String lon); + /// **'Township names'** + String get mapTownLabels; - /// No description provided for @typhoonValueKm. + /// Snackbar shown when saving a notification channel fails /// /// In en, this message translates to: - /// **'{n} km'** - String typhoonValueKm(String n); + /// **'Couldn\'t save the setting. Please try again.'** + String get notifySetFailed; - /// No description provided for @typhoonValueHpa. + /// Disconnect from the radio /// /// In en, this message translates to: - /// **'{n} hPa'** - String typhoonValueHpa(String n); + /// **'Disconnect'** + String get meshtasticDisconnect; - /// No description provided for @typhoonValueMs. + /// Packets the radio could not decrypt /// /// In en, this message translates to: - /// **'{n} m/s'** - String typhoonValueMs(String n); + /// **'Not decrypted'** + String get meshtasticUndecoded; - /// Bulletin data time under the intensity chip (Taipei wall clock) + /// Notify channel title /// /// In en, this message translates to: - /// **'Data time\n{time}'** - String typhoonDataTime(String time); + /// **'Announcements'** + String get notifyAnnouncement; - /// Map layer switcher label for the ECMWF wind-forecast layer + /// Onboarding intro page title /// /// In en, this message translates to: - /// **'ECMWF'** - String get mapLayerWindForecastEcmwf; + /// **'Welcome to DPIP'** + String get onboardingIntroTitle; - /// Map layer switcher label for the GFS wind-forecast layer + /// Shown when the current-location area is selected but GPS is off/unavailable /// /// In en, this message translates to: - /// **'GFS'** - String get mapLayerWindForecastGfs; + /// **'Can\'t get current location'** + String get regionCurrentUnavailable; - /// Map layer switcher label for the real-time seismic monitor (RTS) + /// Language picker option: follow the system language /// /// In en, this message translates to: - /// **'Seismic Monitor'** - String get mapLayerMonitor; + /// **'System default'** + String get languageSystem; - /// Map layer switcher label for the disaster-prevention map (DPM) + /// Label for the skyTimeSunset option in the experimental backdrop settings. /// /// In en, this message translates to: - /// **'Disaster Map'** - String get mapLayerDisasterMap; + /// **'Sunset'** + String get skyTimeSunset; - /// Disaster-map overlay menu toggle for AED (defibrillator) points + /// Himawari Dust RGB composite layer name /// /// In en, this message translates to: - /// **'AED'** - String get mapLayerAed; + /// **'Himawari Dust'** + String get mapLayerSatelliteDust; - /// Tooltip on the disaster-map overlay tune button + /// External map app choice: Apple Maps /// /// In en, this message translates to: - /// **'Disaster map layers'** - String get disasterMapOverlayMenuTooltip; + /// **'Apple Maps'** + String get mapAppAppleMaps; - /// Section header for DPM sub-layer toggles in the overlay menu + /// Edit action on a saved-region bottom sheet /// /// In en, this message translates to: - /// **'Layers'** - String get disasterMapOverlaySectionLayers; + /// **'Edit'** + String get regionEdit; - /// Tooltip for the AED toggle in the disaster-map overlay menu + /// Setting that forces the home weather backdrop to a fixed state /// /// In en, this message translates to: - /// **'Show AED locations'** - String get disasterMapOverlayAedTooltip; + /// **'Weather animation'** + String get weatherDynamicState; - /// AED detail row label + /// Placeholder shown in place of the map while MapLibre is disabled /// /// In en, this message translates to: - /// **'Address'** - String get aedAddress; + /// **'Map (temporarily disabled)'** + String get mapPlaceholderDisabled; - /// AED city / district row label + /// Returns the moon page to the present moment /// /// In en, this message translates to: - /// **'Region'** - String get aedRegion; + /// **'Now'** + String get moonNow; - /// AED venue category row label + /// Section header: how the Moon looks at the chosen moment /// /// In en, this message translates to: - /// **'Category'** - String get aedCategory; + /// **'Appearance'** + String get moonSectionAppearance; - /// AED venue type row label + /// Section header: moonrise and moonset for the user's township /// /// In en, this message translates to: - /// **'Type'** - String get aedType; + /// **'Rise and set'** + String get moonSectionRiseSet; - /// AED placement description row label + /// Section header: the next full and new moons /// /// In en, this message translates to: - /// **'Placement'** - String get aedPlaceDesc; + /// **'Upcoming'** + String get moonSectionUpcoming; - /// AED free-text description row label + /// Section header: the month-at-a-glance phase calendar /// /// In en, this message translates to: - /// **'Notes'** - String get aedDescription; + /// **'Calendar'** + String get moonSectionCalendar; - /// AED weekday opening hours row label + /// Earth-Moon centre-to-centre distance /// /// In en, this message translates to: - /// **'Weekday hours'** - String get aedHoursWeekday; + /// **'Distance'** + String get moonDistance; - /// AED Saturday opening hours row label + /// Unit suffix for the lunar distance /// /// In en, this message translates to: - /// **'Saturday hours'** - String get aedHoursSaturday; + /// **'km'** + String get moonKilometres; - /// AED Sunday opening hours row label + /// The Moon's apparent angular diameter /// /// In en, this message translates to: - /// **'Sunday hours'** - String get aedHoursSunday; + /// **'Apparent size'** + String get moonApparentSize; - /// AED opening-hours remark row label + /// Time the Moon rises /// /// In en, this message translates to: - /// **'Hours note'** - String get aedOpenRemark; + /// **'Moonrise'** + String get moonRise; - /// AED emergency contact phone row label + /// Time the Moon sets /// /// In en, this message translates to: - /// **'Emergency phone'** - String get aedEmergencyPhone; + /// **'Moonset'** + String get moonSet; - /// Disaster-map overlay menu toggle for public restrooms + /// Date and time of the next new moon /// /// In en, this message translates to: - /// **'Restrooms'** - String get mapLayerRestroom; + /// **'Next new moon'** + String get moonNextNewMoon; - /// Disaster-map overlay menu toggle for evacuation shelters + /// Shown when the Moon neither rises nor sets and stays above the horizon /// /// In en, this message translates to: - /// **'Shelters'** - String get mapLayerShelter; + /// **'Up all day'** + String get moonAlwaysUp; - /// Tooltip for the restroom toggle in the disaster-map overlay menu + /// Shown when a calendar day has no moonrise or no moonset /// /// In en, this message translates to: - /// **'Show public restrooms'** - String get disasterMapOverlayRestroomTooltip; + /// **'None today'** + String get moonNoEvent; - /// Tooltip for the shelter toggle in the disaster-map overlay menu + /// Sun page title /// /// In en, this message translates to: - /// **'Show evacuation shelters'** - String get disasterMapOverlayShelterTooltip; + /// **'Sun'** + String get sunTitle; - /// Action in the disaster-map detail sheet: open the point in an external map app + /// Sun page one-line summary on the data hub /// /// In en, this message translates to: - /// **'Open in maps'** - String get dpmOpenInMaps; + /// **'Sunrise, twilight and the solar terms'** + String get sunSubtitle; - /// External map app choice: Google Maps + /// Section header: sunrise, noon, sunset, day length /// /// In en, this message translates to: - /// **'Google Maps'** - String get mapAppGoogleMaps; + /// **'Daylight'** + String get sunSectionDaylight; - /// External map app choice: Apple Maps + /// Section header: the three twilight bands /// /// In en, this message translates to: - /// **'Apple Maps'** - String get mapAppAppleMaps; + /// **'Twilight'** + String get sunSectionTwilight; - /// Choice-sheet label suffix marking the platform home map app, with the app name + /// Section header: golden and blue hour /// /// In en, this message translates to: - /// **'{app} (default)'** - String mapAppDefault(String app); + /// **'Light'** + String get sunSectionLight; - /// Choice-sheet action: copy the point's coordinates + /// Section header: equation of time and the next solar term /// /// In en, this message translates to: - /// **'Copy coordinates'** - String get mapAppCopyCoordinates; + /// **'Sundial'** + String get sunSectionSundial; - /// Snackbar confirming the coordinates were copied + /// Section header: the year's twenty-four solar terms /// /// In en, this message translates to: - /// **'Coordinates copied'** - String get mapAppCoordinatesCopied; + /// **'Solar terms'** + String get sunSectionTerms; - /// Snackbar when the chosen map app cannot be opened on this device + /// Time the Sun rises /// /// In en, this message translates to: - /// **'Could not open {app}'** - String mapAppOpenFailed(String app); + /// **'Sunrise'** + String get sunRise; - /// Snackbar when tapping the emergency phone and the device has no phone handler + /// Time the Sun sets /// /// In en, this message translates to: - /// **'This device cannot make phone calls'** - String get mapAppCallFailed; + /// **'Sunset'** + String get sunSet; - /// Section title in map overlay settings menus: the reference overlays + /// Solar noon, the Sun's upper transit /// /// In en, this message translates to: - /// **'Reference layers'** - String get mapOverlaySectionReference; + /// **'Solar noon'** + String get sunNoon; - /// Section title in map overlay lists: the seismic-monitor overlays + /// How long the Sun is above the horizon, as hours:minutes /// /// In en, this message translates to: - /// **'Earthquake'** - String get mapLayerCategoryEarthquake; + /// **'Day length'** + String get sunDayLength; - /// Section title in map overlay lists: typhoon overlays + /// Civil twilight, the Sun 6 degrees below the horizon /// /// In en, this message translates to: - /// **'Typhoon'** - String get mapLayerCategoryTyphoon; + /// **'Civil'** + String get sunTwilightCivil; - /// Section title in map overlay lists: the weather-observation overlays + /// Nautical twilight, 12 degrees below /// /// In en, this message translates to: - /// **'Weather observations'** - String get mapLayerCategoryWeather; + /// **'Nautical'** + String get sunTwilightNautical; - /// Section title in map overlay lists: satellite-imagery overlays + /// Astronomical twilight, 18 degrees below /// /// In en, this message translates to: - /// **'Satellite'** - String get mapLayerCategorySatellite; + /// **'Astronomical'** + String get sunTwilightAstronomical; - /// Section title in map overlay lists: radar and precipitation-forecast overlays + /// Morning golden hour span /// /// In en, this message translates to: - /// **'Radar'** - String get mapLayerCategoryRadar; + /// **'Morning golden hour'** + String get sunGoldenHourMorning; - /// Section title in map overlay lists: everyday-life facility overlays + /// Evening golden hour span /// /// In en, this message translates to: - /// **'Daily life'** - String get mapLayerCategoryLife; + /// **'Evening golden hour'** + String get sunGoldenHourEvening; - /// Section title in map overlay lists: numerical weather prediction (ECMWF/GFS) wind-field overlays + /// Blue hour span after sunset /// /// In en, this message translates to: - /// **'Numerical forecast'** - String get mapLayerCategoryForecast; + /// **'Blue hour'** + String get sunBlueHour; - /// Section title in map overlay settings menus: base-map settings + /// Apparent solar time minus mean solar time /// /// In en, this message translates to: - /// **'Map'** - String get mapOverlaySectionMap; + /// **'Equation of time'** + String get sunEquationOfTime; - /// Section title in the rainfall menu: the accumulation-interval choices + /// Unit suffix for the equation of time /// /// In en, this message translates to: - /// **'Time window'** - String get rainIntervalSection; + /// **'min'** + String get sunMinutes; - /// Map setting: show township-name labels when the map is zoomed in + /// The next of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Township names'** - String get mapTownLabels; + /// **'Next term'** + String get solarTermNext; - /// Hint under the township-names setting + /// Planets page title /// /// In en, this message translates to: - /// **'Show township names when zoomed in'** - String get mapTownLabelsHint; + /// **'Planets'** + String get planetsTitle; - /// Map setting: show the base map's hillshade relief + /// Planets page one-line summary on the data hub /// /// In en, this message translates to: - /// **'Terrain relief'** - String get mapTerrainRelief; + /// **'Where they are tonight, and how bright'** + String get planetsSubtitle; - /// Hint under the terrain-relief setting + /// Section header: the planets right now /// /// In en, this message translates to: - /// **'Show shaded terrain relief on the base map'** - String get mapTerrainReliefHint; + /// **'Right now'** + String get planetsSectionTonight; - /// Hint in the disaster-map detail sheet when nothing is selected + /// Badge: the planet is above the horizon /// /// In en, this message translates to: - /// **'Tap a marker on the map for details'** - String get dpmSheetEmpty; + /// **'Up'** + String get planetUp; - /// Address row label in the disaster-map restroom / shelter detail sheet + /// Badge: the planet is below the horizon /// /// In en, this message translates to: - /// **'Address'** - String get dpmAddress; + /// **'Below'** + String get planetDown; - /// Restroom detail row label for the toilet type + /// Badge: too close to the Sun to be seen /// /// In en, this message translates to: - /// **'Type'** - String get restroomTypeLabel; + /// **'In glare'** + String get planetInGlare; - /// Restroom detail row label for the venue category + /// Apparent visual magnitude /// /// In en, this message translates to: - /// **'Category'** - String get restroomCategoryLabel; + /// **'Magnitude'** + String get planetMagnitude; - /// Restroom detail row label for the cleanliness grade + /// Angular distance from the Sun /// /// In en, this message translates to: - /// **'Grade'** - String get restroomGradeLabel; + /// **'Elongation'** + String get planetElongation; - /// Restroom type: female restroom + /// Label for whether the planet is an evening or morning object /// /// In en, this message translates to: - /// **'Female'** - String get restroomTypeFemale; + /// **'Sky'** + String get planetSky; - /// Restroom type: male restroom + /// Sets after the Sun, so visible in the evening /// /// In en, this message translates to: - /// **'Male'** - String get restroomTypeMale; + /// **'Evening'** + String get planetEvening; - /// Restroom type: mixed/unisex restroom + /// Rises before the Sun, so visible before dawn /// /// In en, this message translates to: - /// **'Mixed'** - String get restroomTypeMixed; + /// **'Morning'** + String get planetMorning; - /// Restroom type: accessible restroom + /// Distance from the Earth /// /// In en, this message translates to: - /// **'Accessible'** - String get restroomTypeAccessible; + /// **'Distance'** + String get planetDistance; - /// Restroom type: gender-neutral restroom + /// Unit suffix: astronomical units /// /// In en, this message translates to: - /// **'Gender-neutral'** - String get restroomTypeGenderNeutral; + /// **'au'** + String get planetAu; - /// Restroom type: family restroom + /// Height above the horizon right now /// /// In en, this message translates to: - /// **'Family'** - String get restroomTypeFamily; + /// **'Altitude'** + String get planetAltitude; - /// Restroom type: not specified + /// Planet name /// /// In en, this message translates to: - /// **'Unspecified'** - String get restroomTypeUnspecified; + /// **'Mercury'** + String get planetMercury; - /// Restroom venue category: transport facility + /// Planet name /// /// In en, this message translates to: - /// **'Transport'** - String get restroomCategoryTransport; + /// **'Venus'** + String get planetVenus; - /// Restroom venue category: park + /// Planet name /// /// In en, this message translates to: - /// **'Park'** - String get restroomCategoryPark; + /// **'Mars'** + String get planetMars; - /// Restroom venue category: commercial establishment + /// Planet name /// /// In en, this message translates to: - /// **'Commercial'** - String get restroomCategoryCommercial; + /// **'Jupiter'** + String get planetJupiter; - /// Restroom venue category: religious / ceremonial venue + /// Planet name /// /// In en, this message translates to: - /// **'Religious'** - String get restroomCategoryReligious; + /// **'Saturn'** + String get planetSaturn; - /// Restroom venue category: cultural / leisure activity venue + /// Planet name /// /// In en, this message translates to: - /// **'Cultural'** - String get restroomCategoryCultural; + /// **'Uranus'** + String get planetUranus; - /// Restroom venue category: public service office + /// Planet name /// /// In en, this message translates to: - /// **'Government'** - String get restroomCategoryGovernment; + /// **'Neptune'** + String get planetNeptune; - /// Restroom venue category: social welfare institution / gathering place + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Welfare'** - String get restroomCategoryWelfare; + /// **'Vernal Equinox'** + String get solarTermVernalEquinox; - /// Restroom venue category: tourist area / scenic spot + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Tourist'** - String get restroomCategoryTourist; + /// **'Pure Brightness'** + String get solarTermPureBrightness; - /// Restroom venue category: leisure / entertainment venue + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Leisure'** - String get restroomCategoryLeisure; + /// **'Grain Rain'** + String get solarTermGrainRain; - /// Restroom venue category: other + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Other'** - String get restroomCategoryOther; + /// **'Start of Summer'** + String get solarTermStartOfSummer; - /// Restroom cleanliness grade: excellent + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Excellent'** - String get restroomGradeExcellent; + /// **'Grain Full'** + String get solarTermGrainFull; - /// Restroom cleanliness grade: good + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Good'** - String get restroomGradeGood; + /// **'Grain in Ear'** + String get solarTermGrainInEar; - /// Restroom cleanliness grade: average + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Average'** - String get restroomGradeAverage; + /// **'Summer Solstice'** + String get solarTermSummerSolstice; - /// Restroom cleanliness grade: below standard + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Below standard'** - String get restroomGradePoor; + /// **'Minor Heat'** + String get solarTermMinorHeat; - /// Shelter detail address row label + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Address'** - String get shelterAddressLabel; + /// **'Major Heat'** + String get solarTermMajorHeat; - /// Shelter detail capacity row label + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Capacity'** - String get shelterCapacityLabel; + /// **'Start of Autumn'** + String get solarTermStartOfAutumn; - /// Shelter detail capacity row value + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'{n} people'** - String shelterCapacityValue(int n); + /// **'End of Heat'** + String get solarTermEndOfHeat; - /// Shelter detail applicable-disaster categories row label + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Disaster types'** - String get shelterCategoryLabel; + /// **'White Dew'** + String get solarTermWhiteDew; - /// Shelter detail row: whether indoor shelter is provided + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Indoor shelter'** - String get shelterIndoorLabel; + /// **'Autumnal Equinox'** + String get solarTermAutumnalEquinox; - /// Shelter detail row: whether outdoor shelter is provided + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Outdoor shelter'** - String get shelterOutdoorLabel; + /// **'Cold Dew'** + String get solarTermColdDew; - /// Shelter detail row: whether evacuees needing care can be accommodated + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Vulnerable-people friendly'** - String get shelterVulnerableOkLabel; + /// **'Frost Descent'** + String get solarTermFrostDescent; - /// Affirmative value in the disaster-map detail sheet + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Yes'** - String get dpmYes; + /// **'Start of Winter'** + String get solarTermStartOfWinter; - /// Negative value in the disaster-map detail sheet + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'No'** - String get dpmNo; + /// **'Minor Snow'** + String get solarTermMinorSnow; - /// Empty-state hint in the map station-value sheet, shown before any station is selected + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Tap a station to see its reading'** - String get stationSheetEmpty; + /// **'Major Snow'** + String get solarTermMajorSnow; - /// RTS monitor latency: how far behind the latest snapshot is (calibrated now minus the snapshot timestamp), in seconds — pre-formatted to one decimal, e.g. "0.3" + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Delay {value} s'** - String monitorDelay(String value); + /// **'Winter Solstice'** + String get solarTermWinterSolstice; - /// Shown in the monitor panel before the first RTS snapshot arrives + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Waiting for data…'** - String get monitorWaiting; + /// **'Minor Cold'** + String get solarTermMinorCold; - /// Unit footer under a map colour legend (e.g. Unit: dBZ) + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Unit: {unit}'** - String mapLegendUnit(String unit); + /// **'Major Cold'** + String get solarTermMajorCold; - /// Typhoon map legend: past/observed path + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Observed track'** - String get typhoonLegendPast; + /// **'Start of Spring'** + String get solarTermStartOfSpring; - /// CWA class: tropical depression (past-track colour) + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'Tropical depression'** - String get typhoonIntensityTd; + /// **'Rain Water'** + String get solarTermRainWater; - /// Sheet picker: named typhoon (CWA name + TY tyNo) + /// One of the twenty-four solar terms /// /// In en, this message translates to: - /// **'{name} TY {no}'** - String typhoonPickerNamed(String no, String name); + /// **'Awakening of Insects'** + String get solarTermAwakeningOfInsects; - /// Sheet picker: unnamed tropical depression (CWA tdNo) + /// Tonight page title /// /// In en, this message translates to: - /// **'Tropical depression TD {no}'** - String typhoonPickerTd(String no); + /// **'Tonight'** + String get tonightTitle; - /// Secondary badge on the typhoon sheet hero: the CWA typhoon serial number, e.g. TY 4 + /// Tonight page summary on the data hub /// /// In en, this message translates to: - /// **'TY {no}'** - String typhoonTyNo(String no); + /// **'What is observable, and when'** + String get tonightSubtitle; - /// Secondary badge on the typhoon sheet hero: the CWA tropical-depression serial number, e.g. TD 14 + /// Section header: the observing window /// /// In en, this message translates to: - /// **'TD {no}'** - String typhoonTdNo(String no); + /// **'Observing window'** + String get tonightSectionDark; - /// CWA class: mild typhoon (past-track colour) + /// Dusk to dawn with the Sun 18 degrees down /// /// In en, this message translates to: - /// **'Mild typhoon'** - String get typhoonIntensityMild; + /// **'Astronomical night'** + String get tonightAstronomicalNight; - /// CWA class: moderate typhoon (past-track colour) + /// Shown when the Sun never gets 18 degrees below the horizon /// /// In en, this message translates to: - /// **'Moderate typhoon'** - String get typhoonIntensityModerate; + /// **'Never fully dark'** + String get tonightNeverDark; - /// CWA class: intense typhoon (past-track colour) + /// The longest stretch with no Sun and no Moon /// /// In en, this message translates to: - /// **'Intense typhoon'** - String get typhoonIntensityIntense; + /// **'Dark window'** + String get tonightDarkWindow; - /// Typhoon map legend: forecast path + /// Shown when the Moon is up for the whole night /// /// In en, this message translates to: - /// **'Forecast track'** - String get typhoonLegendForecast; + /// **'Moon up all night'** + String get tonightMoonAllNight; - /// Typhoon map legend: forecast waypoint + /// Total dark time, hours:minutes /// /// In en, this message translates to: - /// **'Forecast point'** - String get typhoonLegendForecastPoint; + /// **'Total dark'** + String get tonightDarkTotal; - /// Typhoon map legend: current storm centre + /// The Moon's illuminated fraction tonight /// /// In en, this message translates to: - /// **'Current centre'** - String get typhoonLegendCurrent; + /// **'Moonlight'** + String get tonightMoonlight; - /// Typhoon map legend: uncertainty cone + /// Section header: meteor showers running now /// /// In en, this message translates to: - /// **'Forecast cone'** - String get typhoonLegendCone; + /// **'Meteor showers'** + String get tonightSectionShowers; - /// Collapsed map-legend chip label / tooltip — tap to expand + /// The shower's radiant never rises here /// /// In en, this message translates to: - /// **'Legend'** - String get mapLegendExpand; + /// **'Radiant never rises'** + String get tonightRadiantDown; - /// Tooltip on the control that collapses the map legend + /// Unit: meteors per hour /// /// In en, this message translates to: - /// **'Hide legend'** - String get mapLegendCollapse; + /// **'/h'** + String get tonightPerHour; - /// Map control that centers the camera on the device GPS fix + /// Section header: visible satellite passes /// /// In en, this message translates to: - /// **'My location'** - String get mapMyLocation; + /// **'Satellite passes'** + String get tonightSectionSatellites; - /// Map compass tooltip: re-points the camera to north-up + /// Section header: deep-sky objects high enough to observe /// /// In en, this message translates to: - /// **'Reset north'** - String get mapResetNorth; + /// **'Targets up now'** + String get tonightSectionTargets; - /// Typhoon UI: typhoonLegendCircle15 + /// Meteor shower name /// /// In en, this message translates to: - /// **'Gale circle (L7)'** - String get typhoonLegendCircle15; + /// **'Quadrantids'** + String get showerQuadrantids; - /// Legend for the purple dashed mean-radius storm circle + /// Meteor shower name /// /// In en, this message translates to: - /// **'Average circle'** - String get typhoonLegendCircleAvg; + /// **'Lyrids'** + String get showerLyrids; - /// Typhoon UI: typhoonLegendCircle25 + /// Meteor shower name /// /// In en, this message translates to: - /// **'Storm circle (L10)'** - String get typhoonLegendCircle25; + /// **'Eta Aquariids'** + String get showerEtaAquariids; - /// Per-quadrant storm-wind radii (km) for a typhoon circle + /// Meteor shower name /// /// In en, this message translates to: - /// **'NE {ne} · SE {se} · SW {sw} · NW {nw} km'** - String typhoonStormRadii(String ne, String se, String sw, String nw); + /// **'Delta Aquariids'** + String get showerDeltaAquariids; - /// Compact typhoon time chip / map label shape (day + hour, no month) + /// Meteor shower name /// /// In en, this message translates to: - /// **'{day}日{hour}時'** - String typhoonTimeChip(String day, String hour); + /// **'Perseids'** + String get showerPerseids; - /// Typhoon UI: typhoonLegendProbability + /// Meteor shower name /// /// In en, this message translates to: - /// **'Strike probability'** - String get typhoonLegendProbability; + /// **'Orionids'** + String get showerOrionids; - /// Typhoon UI: typhoonLegendWarningAreas + /// Meteor shower name /// /// In en, this message translates to: - /// **'Warning areas'** - String get typhoonLegendWarningAreas; + /// **'Southern Taurids'** + String get showerSouthernTaurids; - /// Tooltip for the typhoon overlay-toggle chip beside the layer switcher + /// Meteor shower name /// /// In en, this message translates to: - /// **'Typhoon overlay options'** - String get typhoonOverlayMenuTooltip; + /// **'Leonids'** + String get showerLeonids; - /// Section header for L7/L10 storm-band choices in the overlay menu + /// Meteor shower name /// /// In en, this message translates to: - /// **'Storm wind'** - String get typhoonOverlaySectionStorm; + /// **'Geminids'** + String get showerGeminids; - /// Section header for optional typhoon overlays (probability, warning) + /// Meteor shower name /// /// In en, this message translates to: - /// **'Overlays'** - String get typhoonOverlaySectionExtra; + /// **'Ursids'** + String get showerUrsids; - /// Subtitle under each storm-band option (fill + dashed avg) + /// Deep-sky object type /// /// In en, this message translates to: - /// **'With average circle'** - String get typhoonOverlayStormBandSubtitle; + /// **'Open cluster'** + String get deepSkyOpenCluster; - /// Short hint under the strike-probability toggle + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Hides the forecast cone'** - String get typhoonOverlayProbabilityHint; + /// **'Globular cluster'** + String get deepSkyGlobularCluster; - /// Tooltip for the strike-probability toggle; notes mutual exclusion with the cone + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Show strike probability (hides the forecast cone)'** - String get typhoonOverlayProbabilityTooltip; + /// **'Spiral galaxy'** + String get deepSkySpiralGalaxy; - /// Tooltip for the warning-areas overlay toggle + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Highlight counties under a typhoon warning'** - String get typhoonOverlayWarningTooltip; + /// **'Elliptical galaxy'** + String get deepSkyEllipticalGalaxy; - /// Tooltip for the L7 storm-band radio option + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Level-7 wind field + average circle (purple)'** - String get typhoonOverlayStormL7Tooltip; + /// **'Irregular galaxy'** + String get deepSkyIrregularGalaxy; - /// Tooltip for the L10 storm-band radio row + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Level-10 wind field + average circle (yellow)'** - String get typhoonOverlayStormL10Tooltip; + /// **'Planetary nebula'** + String get deepSkyPlanetaryNebula; - /// Overlay-menu section for radar / IR under the typhoon vectors + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Weather underlay'** - String get typhoonOverlaySectionWeather; + /// **'Supernova remnant'** + String get deepSkySupernovaRemnant; - /// No radar or satellite underlay + /// Deep-sky object type /// /// In en, this message translates to: - /// **'None'** - String get typhoonOverlayWeatherNone; + /// **'Emission nebula'** + String get deepSkyEmissionNebula; - /// Subtitle: weather tile matches typhoon report time + /// Deep-sky object type /// /// In en, this message translates to: - /// **'Aligned to bulletin time'** - String get typhoonOverlayWeatherHint; + /// **'Reflection nebula'** + String get deepSkyReflectionNebula; - /// Tooltip for clearing the weather underlay + /// Deep-sky object type: a star pattern, not a single object /// /// In en, this message translates to: - /// **'No radar or infrared underlay'** - String get typhoonOverlayWeatherNoneTooltip; + /// **'Asterism'** + String get deepSkyAsterism; - /// Tooltip for radar underlay (mutex with IR) + /// Almanac page title /// /// In en, this message translates to: - /// **'Radar echo closest to the typhoon bulletin time'** - String get typhoonOverlayWeatherRadarTooltip; + /// **'Almanac'** + String get almanacTitle; - /// Tooltip for Himawari IR underlay (mutex with radar) + /// Almanac page summary on the data hub /// /// In en, this message translates to: - /// **'Infrared closest to the typhoon bulletin time'** - String get typhoonOverlayWeatherSatelliteTooltip; + /// **'The lunisolar date and the eclipses ahead'** + String get almanacSubtitle; - /// Typhoon UI: typhoonWarningTitle + /// Section header: today's date in both calendars /// /// In en, this message translates to: - /// **'Typhoon warning'** - String get typhoonWarningTitle; + /// **'Today'** + String get almanacSectionToday; - /// List of counties under a typhoon warning + /// The Gregorian date /// /// In en, this message translates to: - /// **'Areas: {areas}'** - String typhoonWarningAreas(String areas); + /// **'Gregorian'** + String get almanacGregorian; - /// Typhoon UI: typhoonTrackDetail + /// The lunisolar date /// /// In en, this message translates to: - /// **'Track detail'** - String get typhoonTrackDetail; + /// **'Lunisolar'** + String get almanacLunar; - /// Typhoon UI: typhoonHistoryTitle + /// The sexagenary year and its zodiac animal /// /// In en, this message translates to: - /// **'Dataset time'** - String get typhoonHistoryTitle; + /// **'Year'** + String get almanacYear; - /// Typhoon UI: typhoonHistoryLive + /// Whether this lunar month has 29 or 30 days /// /// In en, this message translates to: - /// **'Live'** - String get typhoonHistoryLive; + /// **'Month length'** + String get almanacMonthLength; - /// Typhoon UI: typhoonSatelliteTitle + /// A 30-day lunar month /// /// In en, this message translates to: - /// **'Satellite'** - String get typhoonSatelliteTitle; + /// **'30 days'** + String get almanacLongMonth; - /// Overlay menu: toggle forecast-point Flutter callout cards + /// A 29-day lunar month /// /// In en, this message translates to: - /// **'Forecast tooltips'** - String get typhoonOverlayForecastCallouts; + /// **'29 days'** + String get almanacShortMonth; - /// Tooltip for the forecast callouts overlay toggle + /// Prefix marking an intercalary lunar month /// /// In en, this message translates to: - /// **'Show forecast-point detail cards when zoomed in'** - String get typhoonOverlayForecastCalloutsTooltip; + /// **'Leap '** + String get almanacLeapPrefix; - /// Filter section title in the disaster-map sheet: restroom venue categories + /// Section header: upcoming lunar eclipses /// /// In en, this message translates to: - /// **'Venue types'** - String get dpmFilterSectionRestroom; + /// **'Lunar eclipses'** + String get almanacSectionLunarEclipses; - /// Filter section title in the disaster-map sheet: restroom toilet-kind categories + /// Section header: solar eclipses visible from here /// /// In en, this message translates to: - /// **'Toilet types'** - String get dpmFilterSectionRestroomType; + /// **'Solar eclipses'** + String get almanacSectionSolarEclipses; - /// Filter section title in the disaster-map sheet: shelter disaster types + /// No solar eclipse is visible from here in the search window /// /// In en, this message translates to: - /// **'Shelter disaster types'** - String get dpmFilterSectionShelter; + /// **'None in range'** + String get almanacNoSolarEclipse; - /// Shelter disaster-type filter chip: flood + /// Eclipse type /// /// In en, this message translates to: - /// **'Flood'** - String get dpmDisasterFlood; + /// **'Total'** + String get eclipseTotal; - /// Shelter disaster-type filter chip: earthquake + /// Eclipse type /// /// In en, this message translates to: - /// **'Earthquake'** - String get dpmDisasterEarthquake; + /// **'Partial'** + String get eclipsePartial; - /// Shelter disaster-type filter chip: landslide + /// Eclipse type: a ring of Sun remains /// /// In en, this message translates to: - /// **'Landslide'** - String get dpmDisasterLandslide; + /// **'Annular'** + String get eclipseAnnular; - /// Shelter disaster-type filter chip: tsunami + /// Eclipse type: the Moon only enters the outer shadow /// /// In en, this message translates to: - /// **'Tsunami'** - String get dpmDisasterTsunami; + /// **'Penumbral'** + String get eclipsePenumbral; - /// Shelter disaster-type filter chip: slope hazard + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Slope hazard'** - String get dpmDisasterSlope; + /// **'Rat'** + String get zodiacRat; - /// Shelter disaster-type filter chip: nuclear accident + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Nuclear accident'** - String get dpmDisasterNuclear; + /// **'Ox'** + String get zodiacOx; - /// Label for the experimental sky time-of-day override. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Sky time'** - String get skyTime; + /// **'Tiger'** + String get zodiacTiger; - /// Label for the skyTimeAuto option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Auto'** - String get skyTimeAuto; + /// **'Rabbit'** + String get zodiacRabbit; - /// Label for the skyTimeDawn option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Dawn'** - String get skyTimeDawn; + /// **'Dragon'** + String get zodiacDragon; - /// Label for the skyTimeSunrise option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Sunrise'** - String get skyTimeSunrise; + /// **'Snake'** + String get zodiacSnake; - /// Label for the skyTimeMorning option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Morning'** - String get skyTimeMorning; + /// **'Horse'** + String get zodiacHorse; - /// Label for the skyTimeNoon option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Noon'** - String get skyTimeNoon; + /// **'Goat'** + String get zodiacGoat; - /// Label for the skyTimeAfternoon option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Afternoon'** - String get skyTimeAfternoon; + /// **'Monkey'** + String get zodiacMonkey; - /// Label for the skyTimeGolden option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Golden hour'** - String get skyTimeGolden; + /// **'Rooster'** + String get zodiacRooster; - /// Label for the skyTimeSunset option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Sunset'** - String get skyTimeSunset; + /// **'Dog'** + String get zodiacDog; - /// Label for the skyTimeDusk option in the experimental backdrop settings. + /// Chinese zodiac animal /// /// In en, this message translates to: - /// **'Dusk'** - String get skyTimeDusk; + /// **'Pig'** + String get zodiacPig; - /// Label for the skyTimeNight option in the experimental backdrop settings. + /// Tide page title /// /// In en, this message translates to: - /// **'Night'** - String get skyTimeNight; + /// **'Tide'** + String get tideTitle; - /// Label for the weatherModeCloudy option in the experimental backdrop settings. + /// Tide page summary on the data hub /// /// In en, this message translates to: - /// **'Cloudy'** - String get weatherModeCloudy; + /// **'Spring, neap and the pull of the Moon'** + String get tideSubtitle; - /// Label for the weatherModeOvercast option in the experimental backdrop settings. + /// Says plainly that this is the astronomical forcing, not a harbour tide table /// /// In en, this message translates to: - /// **'Overcast'** - String get weatherModeOvercast; + /// **'Astronomical forcing only — not a harbour tide table. For water levels use the CWA\'s published tables.'** + String get tideDisclaimer; - /// Label for the weatherModeSnow option in the experimental backdrop settings. + /// Section header: the tide-raising force right now /// /// In en, this message translates to: - /// **'Snow'** - String get weatherModeSnow; + /// **'Right now'** + String get tideSectionNow; - /// Label for the weatherModeSand option in the experimental backdrop settings. + /// Where in the spring-neap cycle the tide sits /// /// In en, this message translates to: - /// **'Dust'** - String get weatherModeSand; + /// **'Cycle'** + String get tidePhase; - /// Radar scan-range overlay toggle in the map's radar overlay menu. + /// Spring tide: Sun and Moon aligned /// /// In en, this message translates to: - /// **'Show scan range'** - String get radarScanRange; + /// **'Spring'** + String get tideSpring; - /// Radar scan-range overlay toggle in the map's radar overlay menu. + /// Neap tide: Sun and Moon at right angles /// /// In en, this message translates to: - /// **'Outlines the area the four radars actually observe.'** - String get radarScanRangeSubtitle; + /// **'Neap'** + String get tideNeap; - /// Hint under the radar scan-range toggle in the radar overlay menu. + /// Between spring and neap /// /// In en, this message translates to: - /// **'Blank outside means unobserved'** - String get radarScanRangeHint; + /// **'Middling'** + String get tideMiddling; - /// Tooltip for the radar overlay-options chip beside the layer switcher + /// How much stronger the Moon's pull is than at mean distance /// /// In en, this message translates to: - /// **'Radar overlay options'** - String get radarOverlayMenuTooltip; + /// **'Lunar pull'** + String get tideLunarDistanceFactor; - /// County-border overlay toggle in the map's radar overlay menu. + /// The equilibrium tide height /// /// In en, this message translates to: - /// **'County borders'** - String get radarCountyOutline; + /// **'Equilibrium tide'** + String get tideEquilibrium; - /// World-country-border overlay toggle in the map's reference-layer overlay menus. + /// Unit: metres /// /// In en, this message translates to: - /// **'National borders'** - String get radarGlobalOutline; + /// **'m'** + String get tideMetres; - /// Hint under the national-border toggle in the radar overlay menu. + /// The next spring tide at lunar perigee - the highest water /// /// In en, this message translates to: - /// **'Every country\'s outer frame'** - String get radarGlobalOutlineHint; + /// **'Next perigean spring'** + String get tidePerigeanSpring; - /// Hint under the county-border toggle in the radar overlay menu. + /// Section header: when the forcing peaks and troughs /// /// In en, this message translates to: - /// **'Drawn over the echo'** - String get radarCountyOutlineHint; + /// **'Turning points'** + String get tideSectionTurningPoints; - /// County-border overlay toggle in the map's radar overlay menu. + /// A high point of the tidal forcing /// /// In en, this message translates to: - /// **'Keeps county borders legible under the radar echo.'** - String get radarCountyOutlineSubtitle; + /// **'High'** + String get tideHigh; - /// Township-border overlay toggle in the map's radar overlay menu. + /// A low point of the tidal forcing /// /// In en, this message translates to: - /// **'Township borders'** - String get radarTownOutline; + /// **'Low'** + String get tideLow; - /// Hint under the township-border toggle in the radar overlay menu. + /// Sky chart page title /// /// In en, this message translates to: - /// **'The finer mesh'** - String get radarTownOutlineHint; + /// **'Sky chart'** + String get skyChartTitle; - /// Township-border overlay toggle in the map's radar overlay menu. + /// Sky chart page summary on the data hub /// /// In en, this message translates to: - /// **'Keeps township borders legible under the radar echo.'** - String get radarTownOutlineSubtitle; + /// **'The naked-eye sky above you'** + String get skyChartSubtitle; - /// Tooltip for the QPESUMS forecast overlay-options chip beside the layer switcher. + /// Compass point on the sky chart /// /// In en, this message translates to: - /// **'QPESUMS overlay options'** - String get qpesumsOverlayMenuTooltip; + /// **'N'** + String get skyChartNorth; - /// Tooltip for the wind-forecast overlay-options chip beside the layer switcher. + /// Compass point on the sky chart /// /// In en, this message translates to: - /// **'Wind forecast overlay options'** - String get windForecastOverlayMenuTooltip; + /// **'E'** + String get skyChartEast; - /// Hint under the county-border toggle in the wind-forecast overlay menu. + /// Compass point on the sky chart /// /// In en, this message translates to: - /// **'Drawn over the wind field'** - String get windForecastCountyOutlineHint; + /// **'S'** + String get skyChartSouth; - /// Hint under the national-border toggle in the wind-forecast overlay menu. + /// Compass point on the sky chart /// /// In en, this message translates to: - /// **'Every country\'s outer frame'** - String get windForecastGlobalOutlineHint; + /// **'W'** + String get skyChartWest; - /// Hint under the township-border toggle in the wind-forecast overlay menu. + /// How old the bundled satellite element set is, in days /// /// In en, this message translates to: - /// **'The finer mesh'** - String get windForecastTownOutlineHint; + /// **'elements {days} d old'** + String tonightElementAge(int days); - /// The serial (report number) of an EEW alert + /// A lunisolar date: an optional leap marker, the month and the day /// /// In en, this message translates to: - /// **'Report {serial}'** - String eewSerial(int serial); + /// **'{leap}month {month}, day {day}'** + String almanacLunarDate(String leap, int month, int day); - /// Label for an EEW alert's maximum felt intensity badge + /// Shown when no meteor shower is running today /// /// In en, this message translates to: - /// **'Max intensity'** - String get eewMaxIntensity; + /// **'No shower running'** + String get tonightNoShowers; - /// Label for the estimated felt intensity at the user's location + /// Shown when no satellite pass is visible in the next two days /// /// In en, this message translates to: - /// **'Estimated at my location'** - String get eewLocalIntensity; + /// **'No visible pass in 48 h'** + String get tonightNoPasses; - /// Label for the S-wave arrival countdown tile + /// Shown when the bundled element set could not be read /// /// In en, this message translates to: - /// **'S-wave'** - String get eewSWave; + /// **'Orbit data unavailable'** + String get tonightSatellitesUnavailable; - /// S-wave arrival countdown state once the wave has arrived + /// Shown when nothing in the catalogue is high enough tonight /// /// In en, this message translates to: - /// **'Arrived'** - String get eewArrived; + /// **'Nothing high enough'** + String get tonightNoTargets; - /// S-wave arrival countdown in seconds + /// Shown when the bundled star catalogue could not be read /// /// In en, this message translates to: - /// **'{seconds} s'** - String eewCountdown(int seconds); + /// **'Star catalogue unavailable'** + String get skyChartUnavailable; } class _AppLocalizationsDelegate diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index 12e9ff2bd..23fad974e 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,510 +10,505 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String get languageName => 'English'; + String typhoonValueLat(String lat) { + return '$lat°N'; + } @override - String get navHome => 'Home'; + String get onboardingSkipBody => + 'Without location and notifications, DPIP can\'t alert you to earthquakes and disasters near you in real time. You can still grant them later in Settings.'; @override - String get navEvents => 'Events'; + String get rainInterval24h => '24 h'; @override - String get navMap => 'Map'; + String homeRainTrendHeavyStopping(int minutes) { + return 'Heavy rain likely to stop in $minutes minutes'; + } @override - String get navData => 'Data'; + String get mapTimelineObserved => 'Observed'; @override - String get navEarthquake => 'Earthquake'; + String get regionSelectTitle => 'Select a region'; @override - String get dataSectionSeismic => 'Seismic'; + String get skyTimeNoon => 'Noon'; @override - String get dataEarthquakeSubtitle => 'Earthquake reports'; + String get radarCountyOutlineSubtitle => + 'Keeps county borders legible under the radar echo.'; @override - String get dataSectionWeather => 'Weather'; + String get dpmFilterSectionRestroomType => 'Toilet types'; @override - String get dataWeatherRankingSubtitle => 'Live station rankings'; + String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; @override - String get weatherRankingTitle => 'Observation rankings'; + String get reportFilterIntensity => 'Intensity'; @override - String weatherRankingMeta(String time, int count) { - return 'Data time: $time\n$count stations'; - } + String get mapLayerLightning => 'Lightning'; @override - String get weatherRankingEmpty => 'No observations to rank'; + String get restroomTypeMale => 'Male'; @override - String get weatherRankingBy => 'Sort by'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => 'Highest'; + String get reportDetailSortByCounty => 'Sort by county'; @override - String get weatherRankingLowest => 'Lowest'; + String get homeRainTrendScattered => 'Light showers possible'; @override - String get weatherRankingMergeTo => 'Merge to'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => 'Township'; + String get weatherRankingTempExtremes => 'Daily extremes'; @override - String get weatherRankingMergeCounty => 'County'; + String get themeLight => 'Light'; @override - String get weatherRankingWind => 'Wind speed'; + String get mapTerrainReliefHint => + 'Show shaded terrain relief on the base map'; @override - String get weatherRankingGust => 'Gust'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => 'Daily extremes'; + String get moreSectionRegion => 'Region'; @override - String get weatherRankingExtremeHigh => 'Daily high'; + String get dpmDisasterEarthquake => 'Earthquake'; @override - String get weatherRankingExtremeLow => 'Daily low'; + String get mapLayerSatellite => 'Himawari Infrared (B13)'; @override - String get weatherRankingExtremeRange => 'Diurnal range'; + String get aedHoursSaturday => 'Saturday hours'; @override - String weatherRankingRecordedAt(String time) { - return 'Recorded at $time'; - } + String get dpmDisasterSlope => 'Slope hazard'; @override - String weatherRankingAnalysisCurrent(String value) { - return 'Now $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return 'High $value'; - } + String get notifySectionEew => 'Earthquake early warning'; @override - String weatherRankingAnalysisLow(String value) { - return 'Low $value'; - } + String get mapResetNorth => 'Reset north'; @override - String weatherRankingAnalysisRange(String value) { - return 'Range $value°C'; - } + String get rainInterval2d => '2 d'; @override - String get reportListEmpty => 'No earthquake reports'; + String get mapTownLabelsHint => 'Show township names when zoomed in'; @override - String get reportListEmptyFiltered => - 'No earthquake reports match these filters'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => 'Tsunami warnings only'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => 'Advanced'; @override - String get reportListLocalFelt => 'Local felt'; + String get weatherRankingExtremeRange => 'Diurnal range'; @override - String get reportListToday => 'Today'; + String get notifySettingsMenu => 'Notification settings'; @override - String get reportListYesterday => 'Yesterday'; + String get typhoonHistoryTitle => 'Dataset time'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app (default)'; } @override - String get reportListEnd => 'End of list'; - - @override - String get reportFilterTitle => 'Filters'; + String get trendRange24h => '24h'; @override - String get reportFilterSort => 'Sort'; + String get mapLayerStyleJmaTooltip => + 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; @override - String get reportFilterSortTime => 'Time'; + String weatherRankingRecordedAt(String time) { + return 'Recorded at $time'; + } @override - String get reportFilterSortIntensity => 'Intensity'; + String get mapLayerRain => 'Rainfall'; @override - String get reportFilterSortMagnitude => 'Magnitude'; + String get mapLayerQpesums => '1h Precipitation Forecast'; @override - String get reportFilterSortDepth => 'Depth'; + String get mapOverlaySectionMap => 'Map'; @override - String get reportFilterOrderDesc => 'Descending'; + String get mapTerrainRelief => 'Terrain relief'; @override - String get reportFilterOrderAsc => 'Ascending'; + String get eewMaxIntensity => 'Max intensity'; @override - String get reportFilterIntensity => 'Intensity'; + String get mapLegendCollapse => 'Hide legend'; @override - String get reportFilterIntensityInfoTitle => 'Intensity scales'; + String get changelogTitle => 'Changelog'; @override - String get reportFilterIntensityInfoIntro => - 'CWA changed the felt-intensity scale on 1 Jan 2020 (Taipei time).'; + String get reportFilterOrderDesc => 'Descending'; @override - String get reportFilterIntensityInfoLegacyTitle => 'Legacy (before 2020)'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyBody => - 'Only levels 0–7. No 5− / 5+ / 6− / 6+ split.'; + String get reportFilterIntensityInfoTitle => 'Intensity scales'; @override - String get reportFilterIntensityInfoModernTitle => 'Current (from 2020)'; + String get mapLayerTyphoon => 'Typhoon'; @override - String get reportFilterIntensityInfoModernBody => - 'Levels 0–4, 5−, 5+, 6−, 6+, and 7. The filter slider uses this scale; older events still show legacy labels in the list.'; + String get radarOverlayMenuTooltip => 'Radar overlay options'; @override - String get reportFilterMagnitude => 'Magnitude'; + String get mapMyLocation => 'My location'; @override - String get reportFilterDepth => 'Depth'; + String get meshtasticNodes => 'Nodes'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get meshtasticSend => 'Send'; @override - String get reportFilterDate => 'Date'; + String get typhoonOverlayStormL7Tooltip => + 'Level-7 wind field + average circle (purple)'; @override - String get reportFilterDatePick => 'Pick dates'; + String get aedType => 'Type'; @override - String get reportFilterDateStartNote => 'Start day: from 00:00 (Taipei)'; + String get termsOfService => 'Terms of Service'; @override - String get reportFilterDateEndNote => 'End day: through 24:00 (Taipei)'; + String get typhoonLegendCircle25 => 'Storm circle (L10)'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get sponsorTitle => 'Support DPIP'; @override - String get reportFilterLocation => 'Location'; + String get mapNavSatellite => 'Satellite'; @override - String get reportFilterLocationHint => 'e.g. Hualien, offshore'; + String homeRainTrendUpdated(String time) { + return 'Updated $time'; + } @override - String get reportFilterAny => 'Any'; + String get onboardingNext => 'Next'; @override - String get reportFilterApply => 'Apply'; + String get weatherRankingMergeTown => 'Township'; @override - String get reportFilterReset => 'Reset'; + String get mapLayerMonitor => 'Seismic Monitor'; @override - String get reportListSearch => 'Search'; + String get moreYoutube => 'YouTube'; @override - String get reportDetailTitle => 'Earthquake Report'; + String get sponsorSubscriptions => 'Subscriptions'; @override - String reportDetailNumbered(String number) { - return 'No. $number Significant Earthquake'; + String typhoonValueLon(String lon) { + return '$lon°E'; } @override - String get reportDetailLocalFelt => 'Local Felt Earthquake'; + String get skyTime => 'Sky time'; @override - String get reportDetailInfo => 'Details'; + String get weatherModeCloudy => 'Cloudy'; @override - String get reportDetailOriginTime => 'Origin time'; + String get skyTimeDusk => 'Dusk'; @override - String get reportDetailEpicenter => 'Epicenter'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailMagnitude => 'Magnitude'; + String get reportFilterDateEndNote => 'End day: through 24:00 (Taipei)'; @override - String get reportDetailDepth => 'Depth'; + String get reportFilterSortMagnitude => 'Magnitude'; @override - String get reportDetailAreaIntensity => 'Intensity by area'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailLocalIntensity => 'Intensity at your locations'; + String get mapLayerCategoryEarthquake => 'Earthquake'; @override - String get reportDetailLocalIntensityUnavailable => 'No intensity data'; + String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; @override - String get reportDetailSortByIntensity => 'Sort by intensity'; + String get typhoonLegendPast => 'Observed track'; @override - String get reportDetailSortByCounty => 'Sort by county'; + String get restroomCategoryOther => 'Other'; @override - String get reportDetailImage => 'Report image'; + String homeForecastHighLow(String high, String low) { + return 'H $high° · L $low°'; + } @override - String get reportDetailImageUnavailable => 'Report image not available'; + String get locationBannerFix => 'Open settings'; @override - String get reportDetailOpenReport => 'Report page'; + String get mapLegendExpand => 'Legend'; @override - String get reportDetailReplay => 'Replay'; + String get eewNone => 'No active earthquake early warning'; @override - String get navMore => 'More'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get appLogs => 'App logs'; + String get notifyOptTsunamiAll => 'Tsunami advisories and warnings'; @override - String get changelogTitle => 'Changelog'; + String get meshtasticLayerOptions => 'Node options'; @override - String get changelogEmpty => 'No release notes yet'; + String get onboardingAgreeContinue => 'Agree and continue'; @override - String get changelogTypePrerelease => 'Beta'; + String get commonRetry => 'Retry'; @override - String get changelogTypeStable => 'Stable'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogCurrentVersion => 'Current'; + String reportDetailNumbered(String number) { + return 'No. $number Significant Earthquake'; + } @override - String get changelogVersionDetails => 'Release details'; + String get typhoonOverlayStormBandSubtitle => 'With average circle'; @override - String get changelogBodyEmpty => 'No notes for this release.'; + String get disasterMapOverlayRestroomTooltip => 'Show public restrooms'; @override - String get mapPlaceholderDisabled => 'Map (temporarily disabled)'; + String get weatherRankingTitle => 'Observation rankings'; @override - String get moreSectionRegion => 'Region'; + String get homeRainTrendHeavySustained => + 'Heavy rain continuing for the next hour'; @override - String get moreSectionNotify => 'Notifications'; + String get notifySectionTsunami => 'Tsunami'; @override - String get moreSectionDisplay => 'Display'; + String get restroomCategoryPark => 'Park'; @override - String get regionManageTitle => 'Saved regions'; + String get moreLinkOpenFailed => 'Couldn\'t open the link'; @override - String get regionAddButton => 'Add a region'; + String get themeDark => 'Dark'; @override - String get regionEmpty => 'No saved regions yet'; + String get sponsorRestore => 'Restore purchases'; @override - String get regionSelectTitle => 'Select a region'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String regionSelectCount(int count, int max) { - return '$count/$max selected'; - } + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String regionSelectFull(int max) { - return 'You can save up to $max regions'; - } + String get meshtasticTraffic => 'Traffic'; @override - String get regionEdit => 'Edit'; + String get mapLayerStyleBdTooltip => + 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; @override - String get moreSectionAdvanced => 'Advanced'; + String get disasterMapOverlayAedTooltip => 'Show AED locations'; @override - String get moreDeveloper => 'Debug info'; + String get mapLayerHumidity => 'Humidity'; @override - String get experimentalFeatures => 'Experimental features'; + String get mapLayerSatelliteTransparentNight => + 'Night = transparent, the basemap shows'; @override - String get moreSectionLinks => 'Links'; + String get meshtasticScanning => 'Scanning…'; @override - String get moreCwaEew => 'CWA earthquake early warning'; + String regionSelectFull(int max) { + return 'You can save up to $max regions'; + } @override - String get moreTremReport => 'TREM detection report'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreServerStatus => 'Server status'; + String get navMore => 'More'; @override - String get moreAnnouncements => 'Announcements'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreDiscord => 'Discord community'; + String get disasterMapOverlaySectionLayers => 'Layers'; @override - String get moreNotifyLog => 'DPIP notification log'; + String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; @override - String get moreLinkOpenFailed => 'Couldn\'t open the link'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get weatherDynamicState => 'Weather animation'; + String get typhoonLabelNe => 'NE'; @override - String get weatherDynamicStateSubtitle => - 'Override the home backdrop weather'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherModeAuto => 'Auto'; + String get reportListEmpty => 'No earthquake reports'; @override - String get weatherModeClear => 'Clear'; + String get reportListEnd => 'End of list'; @override - String get weatherModeRain => 'Rain'; + String get mapLayerSatelliteTruecolor => 'Himawari True Color'; @override - String get weatherModeFog => 'Fog'; + String get typhoonOverlaySectionExtra => 'Overlays'; @override - String get weatherModeThunderstorm => 'Thunderstorm'; + String get eewSWave => 'S-wave'; @override - String get commonLoading => 'Loading…'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get commonRetry => 'Retry'; + String get restroomCategoryCultural => 'Cultural'; @override - String get commonError => 'Something went wrong'; + String get typhoonLabelWind => 'Max. sustained wind near centre'; @override - String get commonFetchFailed => 'Couldn\'t load data. Please try again.'; + String get radarGlobalOutlineHint => 'Every country\'s outer frame'; @override - String get commonEmpty => 'Nothing to show'; + String get notifyEvacuation => 'Disaster information'; @override - String get feedConnecting => 'Connecting…'; + String get typhoonLegendCircle15 => 'Gale circle (L7)'; @override - String get feedStale => 'Data may be out of date'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedOffline => 'Connection lost'; + String get homeRainTrendLightSustained => + 'Light rain continuing for the next hour'; @override - String get eewTitle => 'Earthquake early warning'; + String get commonError => 'Something went wrong'; @override - String get eewNone => 'No active earthquake early warning'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String eewSummary(String magnitude, String depth) { - return 'M$magnitude · depth $depth km'; + String get meshtasticPower => 'Power'; + + @override + String get mapTimelineNow => 'Now'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => 'Nationwide'; + String get reportDetailOpenReport => 'Report page'; @override - String get regionCurrent => 'Current location'; + String get trendRange7d => '7d'; @override - String get regionCurrentUnavailable => 'Can\'t get current location'; + String typhoonWarningAreas(String areas) { + return 'Areas: $areas'; + } @override - String get weatherPrecipitation => 'Precipitation'; + String get rainIntervalSection => 'Time window'; @override - String get weatherHumidity => 'Humidity'; + String get notifyTitle => 'Notifications'; @override - String weatherDataTime(String station, String time) { - return '$station · Data $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => 'View on map'; + String get restroomCategoryLabel => 'Category'; @override - String get homeForecastTitle => '24-hour forecast'; + String get sponsorRestoring => 'Restoring purchases…'; @override - String homeForecastHighLow(String high, String low) { - return 'H $high° · L $low°'; - } + String get sponsorIntro => + 'DPIP is dedicated to real-time disaster-prevention information, with no ads or other revenue model. Your support helps us keep the servers running and keep developing.'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => 'Address'; @override - String homeForecastFeelsLike(String temp) { - return 'Feels like $temp°'; - } + String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; @override - String homeForecastHumidity(String value) { - return 'Humidity $value%'; - } + String get restroomCategoryCommercial => 'Commercial'; @override - String homeForecastWind(String direction, String level) { - return '$direction · Force $level'; - } + String get aedRegion => 'Region'; @override - String get homeForecastUnavailable => 'Select a township to see the forecast'; + String homeRainTrendLightStopping(int minutes) { + return 'Light rain likely to stop in $minutes minutes'; + } @override - String get homeForecastEmpty => 'No forecast available'; + String get reportDetailInfo => 'Details'; @override - String get homeActiveEventsTitle => 'Active events'; + String get mapNavWind => 'Wind'; @override - String get homeActiveEventsEmpty => 'No active events'; + String get windForecastOverlayMenuTooltip => 'Wind forecast overlay options'; @override - String get homeRainTrendTitle => 'Next hour precipitation'; + String get dataWeatherRankingSubtitle => 'Live station rankings'; @override String homeRainTrendMinute(int minute) { @@ -520,1315 +516,2123 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return 'Updated $time'; - } + String get rainInterval6h => '6 h'; @override - String get homeRainTrendNoData => 'No data'; + String get restroomTypeUnspecified => 'Unspecified'; @override - String get homeRainTrendScattered => 'Light showers possible'; + String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; @override - String get homeRainTrendLightSustained => - 'Light rain continuing for the next hour'; + String get mapLayerSatelliteGlobalOutline => 'Country border'; @override - String homeRainTrendLightStopping(int minutes) { - return 'Light rain likely to stop in $minutes minutes'; - } + String get mapNavTemperature => 'Temperature'; @override - String get homeRainTrendHeavySustained => - 'Heavy rain continuing for the next hour'; + String get typhoonLegendForecastPoint => 'Forecast point'; @override - String homeRainTrendHeavyStopping(int minutes) { - return 'Heavy rain likely to stop in $minutes minutes'; - } + String get reportListYesterday => 'Yesterday'; @override - String get mapLayers => 'Layers'; + String get moreSectionLinks => 'Links'; @override - String get mapLayerOrderTitle => 'Reorder layers'; + String get feedOffline => 'Connection lost'; @override - String get mapLayerOrderReset => 'Reset order'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => 'Composite Radar Reflectivity'; + String get moreSectionDisplay => 'Display'; @override - String get mapLayerSatellite => 'Himawari Infrared (B13)'; + String get rainInterval3d => '3 d'; @override - String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; + String get defaultMapLayerSubtitle => + 'The Map tab opens on this overlay. The bottom-navigation icon and label follow this choice.'; @override - String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; + String get aedDescription => 'Notes'; @override - String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; + String get typhoonOverlayWeatherRadarTooltip => + 'Radar echo closest to the typhoon bulletin time'; @override - String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + String get onboardingPermLocationDesc => 'Target alerts to where you are.'; @override - String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; + String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; @override - String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + String get homeActiveEventsEmpty => 'No active events'; @override - String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + String get typhoonLabelPosition => 'Centre location'; @override - String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; + String get weatherRankingBy => 'Sort by'; @override - String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; + String get typhoonIntensityMild => 'Mild typhoon'; @override - String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + String get windForecastGlobalOutlineHint => 'Every country\'s outer frame'; @override - String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; + String get rainInterval1h => '1 h'; @override - String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; + String get eewLocalIntensity => 'Estimated at my location'; @override - String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + String get mapLayerRadar => 'Composite Radar Reflectivity'; @override - String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + String get restroomCategoryReligious => 'Religious'; @override - String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; + String get mapLayerSatelliteCloudCloudy => 'Cloudy'; @override - String get mapLayerSatelliteTruecolor => 'Himawari True Color'; + String get skyTimeSunrise => 'Sunrise'; @override - String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => 'Himawari Ash'; + String get onboardingPermNotifyDesc => + 'Deliver earthquake, weather, and disaster alerts the moment they happen.'; @override - String get mapLayerSatelliteDust => 'Himawari Dust'; + String get radarTownOutline => 'Township borders'; @override - String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + String get mapLayerStyleSection => 'Colour style'; @override - String get mapLayerSatelliteNightmicrophysics => - 'Himawari Night Microphysics'; + String get disasterMapOverlayMenuTooltip => 'Disaster map layers'; @override - String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; + String get typhoonLabelSw => 'SW'; @override - String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + String typhoonForecastLead(String hours) { + return 'Forecast +$hours h'; + } @override - String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + String get dpmDisasterTsunami => 'Tsunami'; @override - String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; + String get changelogTypeStable => 'Stable'; @override - String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; + String get mapLayerSatelliteTransparentClear => + 'Clear sky = transparent, the basemap shows'; @override - String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + String get mapOverlaySectionReference => 'Reference layers'; @override - String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; + String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; @override - String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; + String get reportListLocalFelt => 'Local felt'; @override - String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + String get weatherRankingEmpty => 'No observations to rank'; @override - String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + String get notifySectionOther => 'Other'; @override - String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; + String weatherRankingMeta(String time, int count) { + return 'Data time: $time\n$count stations'; + } @override - String get mapLayerSatelliteGlobalOutline => 'Country border'; + String get onboardingTermsAgree => + 'I have read and agree to the Terms of Service'; @override - String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + String get mapLayerSatelliteTransparentNoVegetation => + 'Below 0.1 = transparent (no vegetation)'; @override - String get mapLayerSatelliteCloudClear => 'Clear'; + String get notifyOptLocalIntensity4 => 'Local intensity 4 or above'; @override - String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + String get eewArrived => 'Arrived'; @override - String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => 'Cloudy'; + String get mapLayerCategoryLife => 'Daily life'; @override - String get mapLayerSatelliteTransparentWarm => - 'Clear sky (warm end) = transparent, the basemap shows'; + String get reportFilterSortIntensity => 'Intensity'; @override - String get mapLayerSatelliteTransparentReflectance => - 'Low reflectance / night = transparent, the basemap shows'; + String get typhoonMotion => 'Moving'; @override - String get mapLayerSatelliteTransparentZero => - 'Zero difference = transparent (no signal)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => - 'Night = transparent, the basemap shows'; + String get typhoonIntensityIntense => 'Intense typhoon'; @override - String get mapLayerSatelliteTransparentNoData => - 'No data (land) = transparent'; + String get mapLayerOrderTitle => 'Reorder layers'; @override - String get mapLayerSatelliteTransparentNoVegetation => - 'Below 0.1 = transparent (no vegetation)'; + String get dpmYes => 'Yes'; @override - String get mapLayerSatelliteTransparentNoWater => - '≤ 0 = transparent (no water)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => - 'Clear sky = transparent, the basemap shows'; + String get reportDetailLocalIntensityUnavailable => 'No intensity data'; @override - String get mapLayerStyleSection => 'Colour style'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => 'Colour style'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => 'Grayscale (JMA)'; + String get reportFilterDepth => 'Depth'; @override - String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; + String get onboardingScrollHint => 'Scroll down to continue'; @override - String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + String get mapNavQpesums => 'Forecast'; @override - String get mapLayerStyleJmaTooltip => - 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; + String get navMap => 'Map'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => 'Weather advisories'; @override - String get mapLayerStyleBdTooltip => - 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; + String get reportFilterReset => 'Reset'; @override - String get mapLayerQpesums => '1h Precipitation Forecast'; + String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; @override - String get mapLayerLightning => 'Lightning'; + String get typhoonOverlaySectionStorm => 'Storm wind'; @override - String lightningLegendCg(int minutes) { - return 'Cloud-to-ground · $minutes min'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return 'Cloud-to-cloud · $minutes min'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => 'Now'; + String get weatherDynamicStateSubtitle => + 'Override the home backdrop weather'; @override - String get mapTimelinePast => 'Past'; + String get reportFilterIntensityInfoModernTitle => 'Current (from 2020)'; @override - String get mapTimelineFuture => 'Future'; + String typhoonDataTime(String time) { + return 'Data time\n$time'; + } @override - String get mapTimelineObserved => 'Observed'; + String get restroomTypeAccessible => 'Accessible'; @override - String get mapTimelineForecast => 'Forecast'; + String get moreSectionAbout => 'About'; @override - String mapTimelineDataTime(String time) { - return 'Data $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => 'Notification settings'; + String get onboardingIntroBody => + 'DPIP is your disaster-prevention companion. It brings together earthquake early warnings, earthquake reports, weather, and hazard information, and alerts you the moment it matters.\n\n• Earthquakes: early warnings, intensity reports, and detailed reports\n• Weather: real-time thunderstorm messages and weather advisories\n• Tsunami and disaster information\n\nNext, we\'ll ask you to review the Terms of Service and grant a few permissions so DPIP can protect you in real time.'; @override - String get notifyTitle => 'Notifications'; + String get shelterCapacityLabel => 'Capacity'; @override - String get notifyUnavailable => - 'Push notifications aren\'t ready yet — try again shortly.'; + String get reportDetailImage => 'Report image'; @override - String get notifySetFailed => 'Couldn\'t save the setting. Please try again.'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => 'Earthquake early warning'; + String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; @override - String get notifySectionEarthquake => 'Earthquake'; + String get onboardingPermNotify => 'Notifications'; @override - String get notifySectionWeather => 'Weather'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => 'Tsunami'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => 'Other'; + String get defaultMapLayerSettings => 'Default map layer'; @override - String get notifyEew => 'Emergency earthquake alert'; + String get moreSectionNotify => 'Notifications'; @override - String get notifyMonitor => 'Strong-motion monitor'; + String get notifyUnavailable => + 'Push notifications aren\'t ready yet — try again shortly.'; @override - String get notifyReport => 'Earthquake report'; + String get mapLayerOrderReset => 'Reset order'; @override - String get notifyIntensity => 'Intensity report'; + String get dpmAddress => 'Address'; @override - String get notifyThunderstorm => 'Thunderstorm alerts'; + String get weatherRankingMergeCounty => 'County'; @override - String get notifyAdvisory => 'Weather advisories'; + String get moreSectionApp => 'Get the app'; @override - String get notifyEvacuation => 'Disaster information'; + String get reportFilterIntensityInfoLegacyBody => + 'Only levels 0–7. No 5− / 5+ / 6− / 6+ split.'; @override - String get notifyTsunami => 'Tsunami information'; + String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; @override - String get notifyAnnouncement => 'Announcements'; + String get qpesumsOverlayMenuTooltip => 'QPESUMS overlay options'; @override - String get notifyOptOff => 'Off'; + String get mapTimelineFuture => 'Future'; @override - String get notifyOptAll => 'Receive all'; + String get typhoonLegendCircleAvg => 'Average circle'; @override - String get notifyOptLocalIntensity4 => 'Local intensity 4 or above'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => 'Local intensity 1 or above'; + String get typhoonLabelSe => 'SE'; @override - String get notifyOptWeatherLocal => 'Current location only'; + String get radarTownOutlineHint => 'The finer mesh'; @override - String get notifyOptTsunamiWarning => 'Tsunami warnings only'; + String eewCountdown(int seconds) { + return '$seconds s'; + } @override - String get notifyOptTsunamiAll => 'Tsunami advisories and warnings'; + String get typhoonLabelGust => 'Peak gust'; @override - String get onboardingNext => 'Next'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => 'Back'; + String get sponsorTerms => 'Terms of Use'; @override - String get onboardingScrollHint => 'Scroll down to continue'; + String get restroomTypeGenderNeutral => 'Gender-neutral'; @override - String get onboardingIntroTitle => 'Welcome to DPIP'; + String get notifyThunderstorm => 'Thunderstorm alerts'; @override - String get onboardingIntroBody => - 'DPIP is your disaster-prevention companion. It brings together earthquake early warnings, earthquake reports, weather, and hazard information, and alerts you the moment it matters.\n\n• Earthquakes: early warnings, intensity reports, and detailed reports\n• Weather: real-time thunderstorm messages and weather advisories\n• Tsunami and disaster information\n\nNext, we\'ll ask you to review the Terms of Service and grant a few permissions so DPIP can protect you in real time.'; + String get skyTimeGolden => 'Golden hour'; @override - String get onboardingTermsTitle => 'Terms of Service'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'Please read the following notices before using DPIP:\n\n• All information should defer to the content published by the Central Weather Administration (CWA).\n\n• Depending on network, server, app, and upstream data-source conditions, information may not be received; we make every effort to avoid this but cannot guarantee it never happens.\n\n• Strong shaking may reach your location before the notification does.\n\n• Earthquake early warnings are fast-computed results that may carry significant error — understand this and use them with caution.\n\n• Any behavior not sanctioned by the authorities may carry legal risk; please follow all applicable regulations.\n\nIn addition, to provide localized alerts, this service collects and uploads your approximate location and push identifier — in the foreground and background — solely to decide which alerts to send you.\n\nBy tapping \"Agree and continue\" you confirm that you have read, understood, and agree to the above.'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => - 'I have read and agree to the Terms of Service'; + String weatherRankingAnalysisCurrent(String value) { + return 'Now $value°C'; + } @override - String get onboardingAgreeContinue => 'Agree and continue'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => 'Permissions'; + String get homeForecastUnavailable => 'Select a township to see the forecast'; @override - String get onboardingPermsBody => - 'So DPIP can alert you the moment disaster strikes, please grant the following. You can change these anytime in system settings.'; + String get mapLayers => 'Layers'; @override - String get onboardingPermNotify => 'Notifications'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => - 'Deliver earthquake, weather, and disaster alerts the moment they happen.'; + String get languageSettings => 'Language'; @override - String get onboardingPermCritical => 'Critical alerts'; + String get dpmDisasterNuclear => 'Nuclear accident'; @override - String get onboardingPermCriticalDesc => - 'Let life-threatening earthquake warnings sound even in silent mode or Do Not Disturb.'; + String get language => 'Language'; @override - String get onboardingPermLocation => 'Location'; + String homeForecastFeelsLike(String temp) { + return 'Feels like $temp°'; + } @override - String get onboardingPermLocationDesc => 'Target alerts to where you are.'; + String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; @override - String get onboardingPermBackground => 'Background location'; + String get skyTimeDawn => 'Dawn'; @override - String get onboardingPermBackgroundDesc => - 'Allow \"Always\" so alerts still target you when the app is closed.'; + String get skyTimeAfternoon => 'Afternoon'; @override - String get onboardingPermBattery => 'Battery exemption'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'Allow DPIP to keep running in the background so alerts aren\'t delayed or missed.'; + String get typhoonWarningTitle => 'Typhoon warning'; @override - String get onboardingGrant => 'Grant'; + String get moreSourceCode => 'Source code'; @override - String get onboardingGranted => 'Granted'; + String get mapLayerCategoryWeather => 'Weather observations'; @override - String get onboardingStart => 'Get started'; + String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; @override - String get language => 'Language'; + String get windForecastTownOutlineHint => 'The finer mesh'; @override - String get languageSettings => 'Language'; + String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; @override - String get languageSystem => 'System default'; + String get mapAppCopyCoordinates => 'Copy coordinates'; @override - String get locationBannerServiceOff => - 'Location services are off — local alerts can\'t target your area.'; + String get reportFilterIntensityInfoIntro => + 'CWA changed the felt-intensity scale on 1 Jan 2020 (Taipei time).'; @override - String get locationBannerPermission => - 'Location permission is off — local alerts can\'t target your area.'; + String get mapNavEarthquake => 'Earthquake'; @override - String get locationBannerFix => 'Open settings'; + String get typhoonGust => 'Gust'; @override - String get notifyBannerDisabled => - 'Notifications are off — you won\'t receive disaster alerts.'; + String get restroomGradeAverage => 'Average'; @override - String get onboardingSkipTitle => 'Permissions not granted'; + String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; @override - String get onboardingSkipBody => - 'Without location and notifications, DPIP can\'t alert you to earthquakes and disasters near you in real time. You can still grant them later in Settings.'; + String get onboardingPermBackgroundDesc => + 'Allow \"Always\" so alerts still target you when the app is closed.'; @override - String get onboardingSkipStay => 'Go back'; + String get mapTimelineForecast => 'Forecast'; @override - String get onboardingSkipLeave => 'Skip anyway'; + String get restroomTypeLabel => 'Type'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => 'Earthquake'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => + 'Level-10 wind field + average circle (yellow)'; @override - String get moreSourceCode => 'Source code'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => 'Get the app'; + String get reportDetailTitle => 'Earthquake Report'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'TREM detection report'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · Data $time'; + } @override - String get displaySettings => 'Display'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => 'Default map layer'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - 'The Map tab opens on this overlay. The bottom-navigation icon and label follow this choice.'; + String get radarCountyOutline => 'County borders'; @override - String get mapNavRadar => 'Radar'; + String get onboardingGranted => 'Granted'; @override - String get mapNavQpesums => 'Forecast'; + String get commonClose => 'Close'; @override - String get mapNavSatellite => 'Satellite'; + String get restroomGradeLabel => 'Grade'; @override - String get mapNavLightning => 'Lightning'; + String get rainIntervalNow => 'Today'; @override - String get mapNavTyphoon => 'Typhoon'; + String get changelogCurrentVersion => 'Current'; @override - String get mapNavEarthquake => 'Earthquake'; + String get typhoonLabelPressure => 'Central pressure'; @override - String get mapNavTemperature => 'Temperature'; + String get typhoonOverlayForecastCalloutsTooltip => + 'Show forecast-point detail cards when zoomed in'; @override - String get mapNavHumidity => 'Humidity'; + String get aedOpenRemark => 'Hours note'; @override - String get mapNavPressure => 'Pressure'; + String get onboardingPermsBody => + 'So DPIP can alert you the moment disaster strikes, please grant the following. You can change these anytime in system settings.'; @override - String get mapNavWind => 'Wind'; + String get typhoonOverlaySectionWeather => 'Weather underlay'; + + @override + String get notifyOptWeatherLocal => 'Current location only'; @override String get mapNavRain => 'Rain'; @override - String get mapNavDisaster => 'Disaster'; + String get moonDays => 'days'; @override - String get displayTheme => 'Theme'; + String mapLegendUnit(String unit) { + return 'Unit: $unit'; + } @override - String get themeSystem => 'System'; + String get weatherModeClear => 'Clear'; @override - String get themeLight => 'Light'; + String get meshtasticRadio => 'Radio'; @override - String get themeDark => 'Dark'; + String get commonEmpty => 'Nothing to show'; @override - String get moreSectionAbout => 'About'; + String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; @override - String get termsOfService => 'Terms of Service'; + String get meshtasticExternalPower => 'External power'; @override - String get faq => 'FAQ'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get openSourceLicenses => 'Open-source licenses'; + String get reportFilterOrderAsc => 'Ascending'; @override - String get sponsorTitle => 'Support DPIP'; + String get reportFilterApply => 'Apply'; @override - String get sponsorIntro => - 'DPIP is dedicated to real-time disaster-prevention information, with no ads or other revenue model. Your support helps us keep the servers running and keep developing.'; + String get reportDetailImageUnavailable => 'Report image not available'; @override - String get sponsorSubscriptions => 'Subscriptions'; + String get weatherRankingHighest => 'Highest'; @override - String get sponsorRecommended => 'Recommended'; + String get reportDetailReplay => 'Replay'; @override - String get sponsorOneTime => 'One-time'; + String get mapLayerRestroom => 'Restrooms'; @override - String sponsorPerMonth(String price) { - return '$price / month'; + String get restroomCategoryWelfare => 'Welfare'; + + @override + String get restroomGradeExcellent => 'Excellent'; + + @override + String get meshtasticLastSent => 'Last sent'; + + @override + String get meshtasticName => 'Name'; + + @override + String get meshtasticScan => 'Scan'; + + @override + String get mapLayerCategoryForecast => 'Numerical forecast'; + + @override + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; + + @override + String get themeSystem => 'System'; + + @override + String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + + @override + String get typhoonLegendForecast => 'Forecast track'; + + @override + String typhoonValueHpa(String n) { + return '$n hPa'; + } + + @override + String get weatherPrecipitation => 'Precipitation'; + + @override + String get moonNextFullMoon => 'Next full moon'; + + @override + String get dpmSheetEmpty => 'Tap a marker on the map for details'; + + @override + String get onboardingSkipLeave => 'Skip anyway'; + + @override + String get onboardingBack => 'Back'; + + @override + String get aedPlaceDesc => 'Placement'; + + @override + String get onboardingSkipTitle => 'Permissions not granted'; + + @override + String get restroomTypeFamily => 'Family'; + + @override + String typhoonValueKm(String n) { + return '$n km'; + } + + @override + String get typhoonPressure => 'Pressure'; + + @override + String get onboardingPermBattery => 'Battery exemption'; + + @override + String get typhoonLabelNw => 'NW'; + + @override + String get dpmDisasterFlood => 'Flood'; + + @override + String get moonPhaseWaxingCrescent => 'Waxing crescent'; + + @override + String get restroomCategoryLeisure => 'Leisure'; + + @override + String get mapLayerTemperature => 'Temperature'; + + @override + String get aedCategory => 'Category'; + + @override + String get meshtasticChannels => 'Channels'; + + @override + String get monitorWaiting => 'Waiting for data…'; + + @override + String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + + @override + String get reportDetailEpicenter => 'Epicenter'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => 'Wind direction'; + + @override + String get reportDetailMagnitude => 'Magnitude'; + + @override + String get reportDetailAreaIntensity => 'Intensity by area'; + + @override + String get rainInterval12h => '12 h'; + + @override + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } + + @override + String get dpmDisasterLandslide => 'Landslide'; + + @override + String get notifyMonitor => 'Strong-motion monitor'; + + @override + String get onboardingStart => 'Get started'; + + @override + String sponsorPerMonth(String price) { + return '$price / month'; + } + + @override + String get mapLayerPressure => 'Pressure'; + + @override + String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + + @override + String get mapLayerSatelliteTransparentZero => + 'Zero difference = transparent (no signal)'; + + @override + String get shelterIndoorLabel => 'Indoor shelter'; + + @override + String get notifyOptOff => 'Off'; + + @override + String get reportFilterSortTime => 'Time'; + + @override + String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + + @override + String get weatherModeThunderstorm => 'Thunderstorm'; + + @override + String get homeViewOnMap => 'View on map'; + + @override + String get reportFilterIntensityInfoLegacyTitle => 'Legacy (before 2020)'; + + @override + String get typhoonLabelSpeed => 'Past movement speed'; + + @override + String mapAppOpenFailed(String app) { + return 'Could not open $app'; + } + + @override + String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + + @override + String get meshtasticReceived => 'Received'; + + @override + String get weatherRankingExtremeLow => 'Daily low'; + + @override + String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + + @override + String get mapLayerSatelliteTransparentNoWater => + '≤ 0 = transparent (no water)'; + + @override + String get shelterCategoryLabel => 'Disaster types'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => 'Gust'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => 'Shelter disaster types'; + + @override + String get moreServerStatus => 'Server status'; + + @override + String get notifySectionWeather => 'Weather'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => 'Seismic'; + + @override + String get changelogBodyEmpty => 'No notes for this release.'; + + @override + String get radarGlobalOutline => 'National borders'; + + @override + String get notifyEew => 'Emergency earthquake alert'; + + @override + String get regionNationwide => 'Nationwide'; + + @override + String get moreNotifyLog => 'DPIP notification log'; + + @override + String get regionCurrent => 'Current location'; + + @override + String get dpmFilterSectionRestroom => 'Venue types'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => 'Snow'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => 'Debug info'; + + @override + String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => 'Lightning'; + + @override + String get homeForecastEmpty => 'No forecast available'; + + @override + String get sponsorOneTime => 'One-time'; + + @override + String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + + @override + String get onboardingPermBackground => 'Background location'; + + @override + String get aedEmergencyPhone => 'Emergency phone'; + + @override + String get dpmOpenInMaps => 'Open in maps'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + 'Let life-threatening earthquake warnings sound even in silent mode or Do Not Disturb.'; + + @override + String get mapLayerSatelliteTransparentWarm => + 'Clear sky (warm end) = transparent, the basemap shows'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => '24-hour forecast'; + + @override + String get typhoonLegendWarningAreas => 'Warning areas'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; + } + + @override + String get notifyOptLocalIntensity1 => 'Local intensity 1 or above'; + + @override + String get mapTimelinePast => 'Past'; + + @override + String get restroomTypeFemale => 'Female'; + + @override + String get reportListToday => 'Today'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => 'Loading…'; + + @override + String get typhoonIntensityModerate => 'Moderate typhoon'; + + @override + String get typhoonWind => 'Wind'; + + @override + String get mapLayerSatelliteAsh => 'Himawari Ash'; + + @override + String get rainInterval3h => '3 h'; + + @override + String get reportListSearch => 'Search'; + + @override + String get mapLayerCategorySatellite => 'Satellite'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => 'Location'; + + @override + String get mapLayerSatelliteNightmicrophysics => + 'Himawari Night Microphysics'; + + @override + String get typhoonIntensityTd => 'Tropical depression'; + + @override + String get reportFilterDate => 'Date'; + + @override + String get sponsorRestoreUnavailable => + 'Can\'t reach the store. Please try again later.'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => 'No saved regions yet'; + + @override + String get onboardingPermBatteryDesc => + 'Allow DPIP to keep running in the background so alerts aren\'t delayed or missed.'; + + @override + String get mapNavDisaster => 'Disaster'; + + @override + String get radarScanRangeSubtitle => + 'Outlines the area the four radars actually observe.'; + + @override + String get aedHoursSunday => 'Sunday hours'; + + @override + String get reportDetailOriginTime => 'Origin time'; + + @override + String get trendNoData => 'No trend data'; + + @override + String get onboardingPermLocation => 'Location'; + + @override + String get moreDiscord => 'Discord community'; + + @override + String get mapNavPressure => 'Pressure'; + + @override + String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => 'No release notes yet'; + + @override + String get reportFilterDateStartNote => 'Start day: from 00:00 (Taipei)'; + + @override + String get eewTitle => 'Earthquake early warning'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '$count/$max selected'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => 'Overcast'; + + @override + String get reportDetailDepth => 'Depth'; + + @override + String get typhoonOverlayWarningTooltip => + 'Highlight counties under a typhoon warning'; + + @override + String get reportFilterDatePick => 'Pick dates'; + + @override + String get onboardingSkipStay => 'Go back'; + + @override + String get commonFetchFailed => 'Couldn\'t load data. Please try again.'; + + @override + String get shelterOutdoorLabel => 'Outdoor shelter'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => 'Radar'; + + @override + String get mapLayerSatelliteCloudClear => 'Clear'; + + @override + String eewSummary(String magnitude, String depth) { + return 'M$magnitude · depth $depth km'; + } + + @override + String get locationBannerPermission => + 'Location permission is off — local alerts can\'t target your area.'; + + @override + String get typhoonOverlayWeatherNoneTooltip => + 'No radar or infrared underlay'; + + @override + String get radarCountyOutlineHint => 'Drawn over the echo'; + + @override + String get windForecastCountyOutlineHint => 'Drawn over the wind field'; + + @override + String get homeRainTrendTitle => 'Next hour precipitation'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => 'Typhoon'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => 'Mixed'; + + @override + String get restroomGradeGood => 'Good'; + + @override + String get notifyTsunami => 'Tsunami information'; + + @override + String get navData => 'Data'; + + @override + String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => 'This device cannot make phone calls'; + + @override + String get reportFilterAny => 'Any'; + + @override + String get weatherRankingMergeTo => 'Merge to'; + + @override + String get notifyIntensity => 'Intensity report'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => 'Accumulation window'; + + @override + String get reportDetailLocalFelt => 'Local Felt Earthquake'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => 'Grant'; + + @override + String get weatherModeRain => 'Rain'; + + @override + String get shelterVulnerableOkLabel => 'Vulnerable-people friendly'; + + @override + String get stationSheetEmpty => 'Tap a station to see its reading'; + + @override + String get typhoonLegendProbability => 'Strike probability'; + + @override + String get reportFilterMagnitude => 'Magnitude'; + + @override + String get skyTimeMorning => 'Morning'; + + @override + String get experimentalFeatures => 'Experimental features'; + + @override + String get onboardingTermsBody => + 'Please read the following notices before using DPIP:\n\n• All information should defer to the content published by the Central Weather Administration (CWA).\n\n• Depending on network, server, app, and upstream data-source conditions, information may not be received; we make every effort to avoid this but cannot guarantee it never happens.\n\n• Strong shaking may reach your location before the notification does.\n\n• Earthquake early warnings are fast-computed results that may carry significant error — understand this and use them with caution.\n\n• Any behavior not sanctioned by the authorities may carry legal risk; please follow all applicable regulations.\n\nIn addition, to provide localized alerts, this service collects and uploads your approximate location and push identifier — in the foreground and background — solely to decide which alerts to send you.\n\nBy tapping \"Agree and continue\" you confirm that you have read, understood, and agree to the above.'; + + @override + String get reportFilterTitle => 'Filters'; + + @override + String get onboardingPermCritical => 'Critical alerts'; + + @override + String trendCumulativeTotal(String total) { + return 'Cumulative $total mm'; + } + + @override + String get languageName => 'English'; + + @override + String get reportListEmptyFiltered => + 'No earthquake reports match these filters'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => 'Typhoon'; + + @override + String get weatherModeSand => 'Dust'; + + @override + String get typhoonSatelliteTitle => 'Satellite'; + + @override + String get notifyReport => 'Earthquake report'; + + @override + String get mapAppCoordinatesCopied => 'Coordinates copied'; + + @override + String get skyTimeNight => 'Night'; + + @override + String get sponsorRecommended => 'Recommended'; + + @override + String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + + @override + String get weatherRankingWind => 'Wind speed'; + + @override + String get feedStale => 'Data may be out of date'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · Force $level'; + } + + @override + String get navHome => 'Home'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => 'Open-source licenses'; + + @override + String get weatherRankingLowest => 'Lowest'; + + @override + String get reportFilterSortDepth => 'Depth'; + + @override + String mapTimelineDataTime(String time) { + return 'Data $time'; + } + + @override + String get radarScanRange => 'Show scan range'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return 'Range $value°C'; + } + + @override + String get weatherRankingExtremeHigh => 'Daily high'; + + @override + String get changelogVersionDetails => 'Release details'; + + @override + String get sponsorPrivacy => 'Privacy Policy'; + + @override + String get reportDetailLocalIntensity => 'Intensity at your locations'; + + @override + String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; + + @override + String shelterCapacityValue(int n) { + return '$n people'; } @override - String get sponsorRestore => 'Restore purchases'; + String lightningLegendCc(int minutes) { + return 'Cloud-to-cloud · $minutes min'; + } @override - String get sponsorTerms => 'Terms of Use'; + String get meshtasticSendHint => 'Message to broadcast'; @override - String get sponsorPrivacy => 'Privacy Policy'; + String monitorDelay(String value) { + return 'Delay $value s'; + } @override - String get sponsorRestoring => 'Restoring purchases…'; + String get dpmNo => 'No'; @override - String get sponsorRestoreUnavailable => - 'Can\'t reach the store. Please try again later.'; + String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; @override - String get commonClose => 'Close'; + String get meshtasticReconnecting => 'Reconnecting…'; @override - String get mapLayerTemperature => 'Temperature'; + String get radarTownOutlineSubtitle => + 'Keeps township borders legible under the radar echo.'; @override - String get trendRange24h => '24h'; + String get typhoonOverlayWeatherSatelliteTooltip => + 'Infrared closest to the typhoon bulletin time'; @override - String get trendRange7d => '7d'; + String get radarScanRangeHint => 'Blank outside means unobserved'; @override - String get trendNoData => 'No trend data'; + String typhoonPickerTd(String no) { + return 'Tropical depression TD $no'; + } @override - String trendCumulativeTotal(String total) { - return 'Cumulative $total mm'; + String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + + @override + String get regionAddButton => 'Add a region'; + + @override + String get displaySettings => 'Display'; + + @override + String get restroomGradePoor => 'Below standard'; + + @override + String get restroomCategoryTourist => 'Tourist'; + + @override + String get locationBannerServiceOff => + 'Location services are off — local alerts can\'t target your area.'; + + @override + String get mapLayerStyleTooltip => 'Colour style'; + + @override + String lightningLegendCg(int minutes) { + return 'Cloud-to-ground · $minutes min'; } @override - String chartHourLabel(int hour) { - return '${hour}h'; + String get skyTimeAuto => 'Auto'; + + @override + String get appLogs => 'App logs'; + + @override + String get feedConnecting => 'Connecting…'; + + @override + String get notifyBannerDisabled => + 'Notifications are off — you won\'t receive disaster alerts.'; + + @override + String get weatherHumidity => 'Humidity'; + + @override + String typhoonValueMs(String n) { + return '$n m/s'; } @override - String get mapLayerHumidity => 'Humidity'; + String homeForecastHumidity(String value) { + return 'Humidity $value%'; + } @override - String get mapLayerPressure => 'Pressure'; + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; @override - String get mapLayerWind => 'Wind direction'; + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; @override - String get mapLayerRain => 'Rainfall'; + String get restroomCategoryTransport => 'Transport'; @override - String get rainIntervalMenu => 'Accumulation window'; + String get reportFilterLocationHint => 'e.g. Hualien, offshore'; @override - String get rainIntervalNow => 'Today'; + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; @override - String get rainInterval10m => '10 min'; + String get meshtasticBattery => 'Battery'; @override - String get rainInterval1h => '1 h'; + String get meshtasticDistance => 'Distance'; @override - String get rainInterval3h => '3 h'; + String get meshtasticSnrTrend => 'Signal trend (SNR)'; @override - String get rainInterval6h => '6 h'; + String get meshtasticBatteryTrend => 'Battery trend'; @override - String get rainInterval12h => '12 h'; + String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; @override - String get rainInterval24h => '24 h'; + String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; @override - String get rainInterval2d => '2 d'; + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } @override - String get rainInterval3d => '3 d'; + String get notifySectionEarthquake => 'Earthquake'; @override - String get mapLayerTyphoon => 'Typhoon'; + String get mapLayerDisasterMap => 'Disaster Map'; @override - String get typhoonNoActive => 'No active typhoon'; + String get weatherModeFog => 'Fog'; @override - String get typhoonWind => 'Wind'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } @override - String get typhoonGust => 'Gust'; + String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; @override - String get typhoonPressure => 'Pressure'; + String get moreAnnouncements => 'Announcements'; @override - String get typhoonMotion => 'Moving'; + String get mapLayerSatelliteTransparentNoData => + 'No data (land) = transparent'; @override - String get typhoonLabelPosition => 'Centre location'; + String get restroomCategoryGovernment => 'Government'; @override - String get typhoonLabelDirection => 'Past movement direction'; + String get typhoonLegendCurrent => 'Current centre'; @override - String get typhoonLabelSpeed => 'Past movement speed'; + String get aedAddress => 'Address'; @override - String get typhoonLabelPressure => 'Central pressure'; + String get mapLayerAed => 'AED'; @override - String get typhoonLabelWind => 'Max. sustained wind near centre'; + String get changelogTypePrerelease => 'Beta'; @override - String get typhoonLabelGust => 'Peak gust'; + String get reportFilterIntensityInfoModernBody => + 'Levels 0–4, 5−, 5+, 6−, 6+, and 7. The filter slider uses this scale; older events still show legacy labels in the list.'; @override - String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; + String get typhoonOverlayWeatherNone => 'None'; @override - String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; + String get mapLayerStyleGray => 'Grayscale (JMA)'; + + @override + String get weatherModeAuto => 'Auto'; @override String get typhoonLabelProbCircle => '70% probability circle'; @override - String typhoonForecastLead(String hours) { - return 'Forecast +$hours h'; + String get notifyOptAll => 'Receive all'; + + @override + String get displayTheme => 'Theme'; + + @override + String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + + @override + String get typhoonLabelDirection => 'Past movement direction'; + + @override + String get regionManageTitle => 'Saved regions'; + + @override + String get typhoonLegendCone => 'Forecast cone'; + + @override + String get moreCwaEew => 'CWA earthquake early warning'; + + @override + String get onboardingPermsTitle => 'Permissions'; + + @override + String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + + @override + String get rainInterval10m => '10 min'; + + @override + String weatherRankingAnalysisLow(String value) { + return 'Low $value'; } @override - String get typhoonLabelNw => 'NW'; + String get meshtasticConnectAnyway => 'Connect anyway'; + + @override + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => + 'Low reflectance / night = transparent, the basemap shows'; + + @override + String chartHourLabel(int hour) { + return '${hour}h'; + } + + @override + String get mapLayerShelter => 'Shelters'; + + @override + String get typhoonOverlayProbabilityTooltip => + 'Show strike probability (hides the forecast cone)'; + + @override + String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => 'Show evacuation shelters'; + + @override + String get mapNavHumidity => 'Humidity'; + + @override + String get reportDetailSortByIntensity => 'Sort by intensity'; + + @override + String get homeRainTrendNoData => 'No data'; + + @override + String get mapLayerCategoryRadar => 'Radar'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + + @override + String get typhoonTrackDetail => 'Track detail'; + + @override + String get dataSectionWeather => 'Weather'; + + @override + String get aedHoursWeekday => 'Weekday hours'; + + @override + String get homeActiveEventsTitle => 'Active events'; + + @override + String weatherRankingAnalysisHigh(String value) { + return 'High $value'; + } + + @override + String get faq => 'FAQ'; + + @override + String get typhoonHistoryLive => 'Live'; + + @override + String eewSerial(int serial) { + return 'Report $serial'; + } + + @override + String get reportFilterSort => 'Sort'; + + @override + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; + + @override + String get dataEarthquakeSubtitle => 'Earthquake reports'; @override - String get typhoonLabelNe => 'NE'; + String get typhoonNoActive => 'No active typhoon'; @override - String get typhoonLabelSw => 'SW'; + String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; @override - String get typhoonLabelSe => 'SE'; + String get navEvents => 'Events'; @override - String typhoonValueLat(String lat) { - return '$lat°N'; - } + String get onboardingTermsTitle => 'Terms of Service'; @override - String typhoonValueLon(String lon) { - return '$lon°E'; - } + String get mapTownLabels => 'Township names'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => 'Couldn\'t save the setting. Please try again.'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '$n m/s'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return 'Data time\n$time'; - } + String get notifyAnnouncement => 'Announcements'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'Welcome to DPIP'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => 'Can\'t get current location'; @override - String get mapLayerMonitor => 'Seismic Monitor'; + String get languageSystem => 'System default'; @override - String get mapLayerDisasterMap => 'Disaster Map'; + String get skyTimeSunset => 'Sunset'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'Himawari Dust'; @override - String get disasterMapOverlayMenuTooltip => 'Disaster map layers'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => 'Layers'; + String get regionEdit => 'Edit'; @override - String get disasterMapOverlayAedTooltip => 'Show AED locations'; + String get weatherDynamicState => 'Weather animation'; @override - String get aedAddress => 'Address'; + String get mapPlaceholderDisabled => 'Map (temporarily disabled)'; @override - String get aedRegion => 'Region'; + String get moonNow => 'Now'; @override - String get aedCategory => 'Category'; + String get moonSectionAppearance => 'Appearance'; @override - String get aedType => 'Type'; + String get moonSectionRiseSet => 'Rise and set'; @override - String get aedPlaceDesc => 'Placement'; + String get moonSectionUpcoming => 'Upcoming'; @override - String get aedDescription => 'Notes'; + String get moonSectionCalendar => 'Calendar'; @override - String get aedHoursWeekday => 'Weekday hours'; + String get moonDistance => 'Distance'; @override - String get aedHoursSaturday => 'Saturday hours'; + String get moonKilometres => 'km'; @override - String get aedHoursSunday => 'Sunday hours'; + String get moonApparentSize => 'Apparent size'; @override - String get aedOpenRemark => 'Hours note'; + String get moonRise => 'Moonrise'; @override - String get aedEmergencyPhone => 'Emergency phone'; + String get moonSet => 'Moonset'; @override - String get mapLayerRestroom => 'Restrooms'; + String get moonNextNewMoon => 'Next new moon'; @override - String get mapLayerShelter => 'Shelters'; + String get moonAlwaysUp => 'Up all day'; @override - String get disasterMapOverlayRestroomTooltip => 'Show public restrooms'; + String get moonNoEvent => 'None today'; @override - String get disasterMapOverlayShelterTooltip => 'Show evacuation shelters'; + String get sunTitle => 'Sun'; @override - String get dpmOpenInMaps => 'Open in maps'; + String get sunSubtitle => 'Sunrise, twilight and the solar terms'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => 'Daylight'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => 'Twilight'; @override - String mapAppDefault(String app) { - return '$app (default)'; - } + String get sunSectionLight => 'Light'; @override - String get mapAppCopyCoordinates => 'Copy coordinates'; + String get sunSectionSundial => 'Sundial'; @override - String get mapAppCoordinatesCopied => 'Coordinates copied'; + String get sunSectionTerms => 'Solar terms'; @override - String mapAppOpenFailed(String app) { - return 'Could not open $app'; - } + String get sunRise => 'Sunrise'; @override - String get mapAppCallFailed => 'This device cannot make phone calls'; + String get sunSet => 'Sunset'; @override - String get mapOverlaySectionReference => 'Reference layers'; + String get sunNoon => 'Solar noon'; @override - String get mapLayerCategoryEarthquake => 'Earthquake'; + String get sunDayLength => 'Day length'; @override - String get mapLayerCategoryTyphoon => 'Typhoon'; + String get sunTwilightCivil => 'Civil'; @override - String get mapLayerCategoryWeather => 'Weather observations'; + String get sunTwilightNautical => 'Nautical'; @override - String get mapLayerCategorySatellite => 'Satellite'; + String get sunTwilightAstronomical => 'Astronomical'; @override - String get mapLayerCategoryRadar => 'Radar'; + String get sunGoldenHourMorning => 'Morning golden hour'; @override - String get mapLayerCategoryLife => 'Daily life'; + String get sunGoldenHourEvening => 'Evening golden hour'; @override - String get mapLayerCategoryForecast => 'Numerical forecast'; + String get sunBlueHour => 'Blue hour'; @override - String get mapOverlaySectionMap => 'Map'; + String get sunEquationOfTime => 'Equation of time'; @override - String get rainIntervalSection => 'Time window'; + String get sunMinutes => 'min'; @override - String get mapTownLabels => 'Township names'; + String get solarTermNext => 'Next term'; @override - String get mapTownLabelsHint => 'Show township names when zoomed in'; + String get planetsTitle => 'Planets'; @override - String get mapTerrainRelief => 'Terrain relief'; + String get planetsSubtitle => 'Where they are tonight, and how bright'; @override - String get mapTerrainReliefHint => - 'Show shaded terrain relief on the base map'; + String get planetsSectionTonight => 'Right now'; @override - String get dpmSheetEmpty => 'Tap a marker on the map for details'; + String get planetUp => 'Up'; @override - String get dpmAddress => 'Address'; + String get planetDown => 'Below'; @override - String get restroomTypeLabel => 'Type'; + String get planetInGlare => 'In glare'; @override - String get restroomCategoryLabel => 'Category'; + String get planetMagnitude => 'Magnitude'; @override - String get restroomGradeLabel => 'Grade'; + String get planetElongation => 'Elongation'; @override - String get restroomTypeFemale => 'Female'; + String get planetSky => 'Sky'; @override - String get restroomTypeMale => 'Male'; + String get planetEvening => 'Evening'; @override - String get restroomTypeMixed => 'Mixed'; + String get planetMorning => 'Morning'; @override - String get restroomTypeAccessible => 'Accessible'; + String get planetDistance => 'Distance'; @override - String get restroomTypeGenderNeutral => 'Gender-neutral'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => 'Family'; + String get planetAltitude => 'Altitude'; @override - String get restroomTypeUnspecified => 'Unspecified'; + String get planetMercury => 'Mercury'; @override - String get restroomCategoryTransport => 'Transport'; + String get planetVenus => 'Venus'; @override - String get restroomCategoryPark => 'Park'; + String get planetMars => 'Mars'; @override - String get restroomCategoryCommercial => 'Commercial'; + String get planetJupiter => 'Jupiter'; @override - String get restroomCategoryReligious => 'Religious'; + String get planetSaturn => 'Saturn'; @override - String get restroomCategoryCultural => 'Cultural'; + String get planetUranus => 'Uranus'; @override - String get restroomCategoryGovernment => 'Government'; + String get planetNeptune => 'Neptune'; @override - String get restroomCategoryWelfare => 'Welfare'; + String get solarTermVernalEquinox => 'Vernal Equinox'; @override - String get restroomCategoryTourist => 'Tourist'; + String get solarTermPureBrightness => 'Pure Brightness'; @override - String get restroomCategoryLeisure => 'Leisure'; + String get solarTermGrainRain => 'Grain Rain'; @override - String get restroomCategoryOther => 'Other'; + String get solarTermStartOfSummer => 'Start of Summer'; @override - String get restroomGradeExcellent => 'Excellent'; + String get solarTermGrainFull => 'Grain Full'; @override - String get restroomGradeGood => 'Good'; + String get solarTermGrainInEar => 'Grain in Ear'; @override - String get restroomGradeAverage => 'Average'; + String get solarTermSummerSolstice => 'Summer Solstice'; @override - String get restroomGradePoor => 'Below standard'; + String get solarTermMinorHeat => 'Minor Heat'; @override - String get shelterAddressLabel => 'Address'; + String get solarTermMajorHeat => 'Major Heat'; @override - String get shelterCapacityLabel => 'Capacity'; + String get solarTermStartOfAutumn => 'Start of Autumn'; @override - String shelterCapacityValue(int n) { - return '$n people'; - } + String get solarTermEndOfHeat => 'End of Heat'; @override - String get shelterCategoryLabel => 'Disaster types'; + String get solarTermWhiteDew => 'White Dew'; @override - String get shelterIndoorLabel => 'Indoor shelter'; + String get solarTermAutumnalEquinox => 'Autumnal Equinox'; @override - String get shelterOutdoorLabel => 'Outdoor shelter'; + String get solarTermColdDew => 'Cold Dew'; @override - String get shelterVulnerableOkLabel => 'Vulnerable-people friendly'; + String get solarTermFrostDescent => 'Frost Descent'; @override - String get dpmYes => 'Yes'; + String get solarTermStartOfWinter => 'Start of Winter'; @override - String get dpmNo => 'No'; + String get solarTermMinorSnow => 'Minor Snow'; @override - String get stationSheetEmpty => 'Tap a station to see its reading'; + String get solarTermMajorSnow => 'Major Snow'; @override - String monitorDelay(String value) { - return 'Delay $value s'; - } + String get solarTermWinterSolstice => 'Winter Solstice'; @override - String get monitorWaiting => 'Waiting for data…'; + String get solarTermMinorCold => 'Minor Cold'; @override - String mapLegendUnit(String unit) { - return 'Unit: $unit'; - } + String get solarTermMajorCold => 'Major Cold'; @override - String get typhoonLegendPast => 'Observed track'; + String get solarTermStartOfSpring => 'Start of Spring'; @override - String get typhoonIntensityTd => 'Tropical depression'; + String get solarTermRainWater => 'Rain Water'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => 'Awakening of Insects'; @override - String typhoonPickerTd(String no) { - return 'Tropical depression TD $no'; - } + String get tonightTitle => 'Tonight'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => 'What is observable, and when'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => 'Observing window'; @override - String get typhoonIntensityMild => 'Mild typhoon'; + String get tonightAstronomicalNight => 'Astronomical night'; @override - String get typhoonIntensityModerate => 'Moderate typhoon'; + String get tonightNeverDark => 'Never fully dark'; @override - String get typhoonIntensityIntense => 'Intense typhoon'; + String get tonightDarkWindow => 'Dark window'; @override - String get typhoonLegendForecast => 'Forecast track'; + String get tonightMoonAllNight => 'Moon up all night'; @override - String get typhoonLegendForecastPoint => 'Forecast point'; + String get tonightDarkTotal => 'Total dark'; @override - String get typhoonLegendCurrent => 'Current centre'; + String get tonightMoonlight => 'Moonlight'; @override - String get typhoonLegendCone => 'Forecast cone'; + String get tonightSectionShowers => 'Meteor showers'; @override - String get mapLegendExpand => 'Legend'; + String get tonightRadiantDown => 'Radiant never rises'; @override - String get mapLegendCollapse => 'Hide legend'; + String get tonightPerHour => '/h'; @override - String get mapMyLocation => 'My location'; + String get tonightSectionSatellites => 'Satellite passes'; @override - String get mapResetNorth => 'Reset north'; + String get tonightSectionTargets => 'Targets up now'; @override - String get typhoonLegendCircle15 => 'Gale circle (L7)'; + String get showerQuadrantids => 'Quadrantids'; @override - String get typhoonLegendCircleAvg => 'Average circle'; + String get showerLyrids => 'Lyrids'; @override - String get typhoonLegendCircle25 => 'Storm circle (L10)'; + String get showerEtaAquariids => 'Eta Aquariids'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => 'Delta Aquariids'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => 'Perseids'; @override - String get typhoonLegendProbability => 'Strike probability'; + String get showerOrionids => 'Orionids'; @override - String get typhoonLegendWarningAreas => 'Warning areas'; + String get showerSouthernTaurids => 'Southern Taurids'; @override - String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + String get showerLeonids => 'Leonids'; @override - String get typhoonOverlaySectionStorm => 'Storm wind'; + String get showerGeminids => 'Geminids'; @override - String get typhoonOverlaySectionExtra => 'Overlays'; + String get showerUrsids => 'Ursids'; @override - String get typhoonOverlayStormBandSubtitle => 'With average circle'; + String get deepSkyOpenCluster => 'Open cluster'; @override - String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; + String get deepSkyGlobularCluster => 'Globular cluster'; @override - String get typhoonOverlayProbabilityTooltip => - 'Show strike probability (hides the forecast cone)'; + String get deepSkySpiralGalaxy => 'Spiral galaxy'; @override - String get typhoonOverlayWarningTooltip => - 'Highlight counties under a typhoon warning'; + String get deepSkyEllipticalGalaxy => 'Elliptical galaxy'; @override - String get typhoonOverlayStormL7Tooltip => - 'Level-7 wind field + average circle (purple)'; + String get deepSkyIrregularGalaxy => 'Irregular galaxy'; @override - String get typhoonOverlayStormL10Tooltip => - 'Level-10 wind field + average circle (yellow)'; + String get deepSkyPlanetaryNebula => 'Planetary nebula'; @override - String get typhoonOverlaySectionWeather => 'Weather underlay'; + String get deepSkySupernovaRemnant => 'Supernova remnant'; @override - String get typhoonOverlayWeatherNone => 'None'; + String get deepSkyEmissionNebula => 'Emission nebula'; @override - String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; + String get deepSkyReflectionNebula => 'Reflection nebula'; @override - String get typhoonOverlayWeatherNoneTooltip => - 'No radar or infrared underlay'; + String get deepSkyAsterism => 'Asterism'; @override - String get typhoonOverlayWeatherRadarTooltip => - 'Radar echo closest to the typhoon bulletin time'; + String get almanacTitle => 'Almanac'; @override - String get typhoonOverlayWeatherSatelliteTooltip => - 'Infrared closest to the typhoon bulletin time'; + String get almanacSubtitle => 'The lunisolar date and the eclipses ahead'; @override - String get typhoonWarningTitle => 'Typhoon warning'; + String get almanacSectionToday => 'Today'; @override - String typhoonWarningAreas(String areas) { - return 'Areas: $areas'; - } + String get almanacGregorian => 'Gregorian'; @override - String get typhoonTrackDetail => 'Track detail'; + String get almanacLunar => 'Lunisolar'; @override - String get typhoonHistoryTitle => 'Dataset time'; + String get almanacYear => 'Year'; @override - String get typhoonHistoryLive => 'Live'; + String get almanacMonthLength => 'Month length'; @override - String get typhoonSatelliteTitle => 'Satellite'; + String get almanacLongMonth => '30 days'; @override - String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + String get almanacShortMonth => '29 days'; @override - String get typhoonOverlayForecastCalloutsTooltip => - 'Show forecast-point detail cards when zoomed in'; + String get almanacLeapPrefix => 'Leap '; @override - String get dpmFilterSectionRestroom => 'Venue types'; + String get almanacSectionLunarEclipses => 'Lunar eclipses'; @override - String get dpmFilterSectionRestroomType => 'Toilet types'; + String get almanacSectionSolarEclipses => 'Solar eclipses'; @override - String get dpmFilterSectionShelter => 'Shelter disaster types'; + String get almanacNoSolarEclipse => 'None in range'; @override - String get dpmDisasterFlood => 'Flood'; + String get eclipseTotal => 'Total'; @override - String get dpmDisasterEarthquake => 'Earthquake'; + String get eclipsePartial => 'Partial'; @override - String get dpmDisasterLandslide => 'Landslide'; + String get eclipseAnnular => 'Annular'; @override - String get dpmDisasterTsunami => 'Tsunami'; + String get eclipsePenumbral => 'Penumbral'; @override - String get dpmDisasterSlope => 'Slope hazard'; + String get zodiacRat => 'Rat'; @override - String get dpmDisasterNuclear => 'Nuclear accident'; + String get zodiacOx => 'Ox'; @override - String get skyTime => 'Sky time'; + String get zodiacTiger => 'Tiger'; @override - String get skyTimeAuto => 'Auto'; + String get zodiacRabbit => 'Rabbit'; @override - String get skyTimeDawn => 'Dawn'; + String get zodiacDragon => 'Dragon'; @override - String get skyTimeSunrise => 'Sunrise'; + String get zodiacSnake => 'Snake'; @override - String get skyTimeMorning => 'Morning'; + String get zodiacHorse => 'Horse'; @override - String get skyTimeNoon => 'Noon'; + String get zodiacGoat => 'Goat'; @override - String get skyTimeAfternoon => 'Afternoon'; + String get zodiacMonkey => 'Monkey'; @override - String get skyTimeGolden => 'Golden hour'; + String get zodiacRooster => 'Rooster'; @override - String get skyTimeSunset => 'Sunset'; + String get zodiacDog => 'Dog'; @override - String get skyTimeDusk => 'Dusk'; + String get zodiacPig => 'Pig'; @override - String get skyTimeNight => 'Night'; + String get tideTitle => 'Tide'; @override - String get weatherModeCloudy => 'Cloudy'; + String get tideSubtitle => 'Spring, neap and the pull of the Moon'; @override - String get weatherModeOvercast => 'Overcast'; + String get tideDisclaimer => + 'Astronomical forcing only — not a harbour tide table. For water levels use the CWA\'s published tables.'; @override - String get weatherModeSnow => 'Snow'; + String get tideSectionNow => 'Right now'; @override - String get weatherModeSand => 'Dust'; + String get tidePhase => 'Cycle'; @override - String get radarScanRange => 'Show scan range'; + String get tideSpring => 'Spring'; @override - String get radarScanRangeSubtitle => - 'Outlines the area the four radars actually observe.'; + String get tideNeap => 'Neap'; @override - String get radarScanRangeHint => 'Blank outside means unobserved'; + String get tideMiddling => 'Middling'; @override - String get radarOverlayMenuTooltip => 'Radar overlay options'; + String get tideLunarDistanceFactor => 'Lunar pull'; @override - String get radarCountyOutline => 'County borders'; + String get tideEquilibrium => 'Equilibrium tide'; @override - String get radarGlobalOutline => 'National borders'; + String get tideMetres => 'm'; @override - String get radarGlobalOutlineHint => 'Every country\'s outer frame'; + String get tidePerigeanSpring => 'Next perigean spring'; @override - String get radarCountyOutlineHint => 'Drawn over the echo'; + String get tideSectionTurningPoints => 'Turning points'; @override - String get radarCountyOutlineSubtitle => - 'Keeps county borders legible under the radar echo.'; + String get tideHigh => 'High'; @override - String get radarTownOutline => 'Township borders'; + String get tideLow => 'Low'; @override - String get radarTownOutlineHint => 'The finer mesh'; + String get skyChartTitle => 'Sky chart'; @override - String get radarTownOutlineSubtitle => - 'Keeps township borders legible under the radar echo.'; + String get skyChartSubtitle => 'The naked-eye sky above you'; @override - String get qpesumsOverlayMenuTooltip => 'QPESUMS overlay options'; + String get skyChartNorth => 'N'; @override - String get windForecastOverlayMenuTooltip => 'Wind forecast overlay options'; + String get skyChartEast => 'E'; @override - String get windForecastCountyOutlineHint => 'Drawn over the wind field'; + String get skyChartSouth => 'S'; @override - String get windForecastGlobalOutlineHint => 'Every country\'s outer frame'; + String get skyChartWest => 'W'; @override - String get windForecastTownOutlineHint => 'The finer mesh'; + String tonightElementAge(int days) { + return 'elements $days d old'; + } @override - String eewSerial(int serial) { - return 'Report $serial'; + String almanacLunarDate(String leap, int month, int day) { + return '${leap}month $month, day $day'; } @override - String get eewMaxIntensity => 'Max intensity'; + String get tonightNoShowers => 'No shower running'; @override - String get eewLocalIntensity => 'Estimated at my location'; + String get tonightNoPasses => 'No visible pass in 48 h'; @override - String get eewSWave => 'S-wave'; + String get tonightSatellitesUnavailable => 'Orbit data unavailable'; @override - String get eewArrived => 'Arrived'; + String get tonightNoTargets => 'Nothing high enough'; @override - String eewCountdown(int seconds) { - return '$seconds s'; - } + String get skyChartUnavailable => 'Star catalogue unavailable'; } diff --git a/lib/l10n/gen/app_localizations_fil.dart b/lib/l10n/gen/app_localizations_fil.dart index ed2d2273a..0653ad51d 100644 --- a/lib/l10n/gen/app_localizations_fil.dart +++ b/lib/l10n/gen/app_localizations_fil.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,515 +10,508 @@ class AppLocalizationsFil extends AppLocalizations { AppLocalizationsFil([String locale = 'fil']) : super(locale); @override - String get languageName => 'Filipino'; + String typhoonValueLat(String lat) { + return '$lat°N'; + } @override - String get navHome => 'Tahanan'; + String get onboardingSkipBody => + 'Kung walang lokasyon at mga notification, hindi ka maaalertuhan ng DPIP nang real time sa mga lindol at sakuna malapit sa iyo. Maaari mo pa ring ibigay ang mga ito sa ibang pagkakataon sa Settings.'; @override - String get navEvents => 'Mga Kaganapan'; + String get rainInterval24h => '24 oras'; @override - String get navMap => 'Mapa'; + String homeRainTrendHeavyStopping(int minutes) { + return 'Baka huminto ang malakas na ulan sa loob ng $minutes minuto'; + } @override - String get navData => 'Datos'; + String get mapTimelineObserved => 'Naobserbahan'; @override - String get navEarthquake => 'Lindol'; + String get regionSelectTitle => 'Pumili ng rehiyon'; @override - String get dataSectionSeismic => 'Seismic'; + String get skyTimeNoon => 'Tanghali'; @override - String get dataEarthquakeSubtitle => 'Mga ulat ng lindol'; + String get radarCountyOutlineSubtitle => + 'Nananatiling mababasa ang mga hangganan sa ilalim ng radar echo.'; @override - String get dataSectionWeather => 'Panahon'; + String get dpmFilterSectionRestroomType => 'Mga uri ng banyo'; @override - String get dataWeatherRankingSubtitle => 'Live na ranggo ng istasyon'; + String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; @override - String get weatherRankingTitle => 'Mga ranggo ng obserbasyon'; + String get reportFilterIntensity => 'Intensity'; @override - String weatherRankingMeta(String time, int count) { - return 'Oras ng datos: $time\n$count istasyon'; - } + String get mapLayerLightning => 'Kidlat'; @override - String get weatherRankingEmpty => 'Walang obserbasyon na iraranggo'; + String get restroomTypeMale => 'Palikuran ng lalaki'; @override - String get weatherRankingBy => 'Ayon sa'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => 'Pinakamataas'; + String get reportDetailSortByCounty => 'Ayusin ayon sa lalawigan'; @override - String get weatherRankingLowest => 'Pinakamababa'; + String get homeRainTrendScattered => 'Posibleng mahinang ulan'; @override - String get weatherRankingMergeTo => 'Pagsamahin'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => 'Bayan'; + String get weatherRankingTempExtremes => 'Mga sukdulan ng temperatura'; @override - String get weatherRankingMergeCounty => 'Lalawigan'; + String get themeLight => 'Maliwanag'; @override - String get weatherRankingWind => 'Bilis ng hangin'; + String get mapTerrainReliefHint => 'Ipakita ang anino ng terrain sa base map'; @override - String get weatherRankingGust => 'Bugso'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => 'Mga sukdulan ng temperatura'; + String get moreSectionRegion => 'Rehiyon'; @override - String get weatherRankingExtremeHigh => 'Pinakamataas ngayong araw'; + String get dpmDisasterEarthquake => 'Lindol'; @override - String get weatherRankingExtremeLow => 'Pinakamababa ngayong araw'; + String get mapLayerSatellite => 'Himawari Infrared (B13)'; @override - String get weatherRankingExtremeRange => 'Saklaw sa araw'; + String get aedHoursSaturday => 'Oras sa Sabado'; @override - String weatherRankingRecordedAt(String time) { - return 'Naitala noong $time'; - } + String get dpmDisasterSlope => 'Panganib sa dalisdis'; @override - String weatherRankingAnalysisCurrent(String value) { - return 'Ngayon $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return 'Mataas $value'; - } + String get notifySectionEew => 'Maagang babala sa lindol'; @override - String weatherRankingAnalysisLow(String value) { - return 'Mababa $value'; - } + String get mapResetNorth => 'Bumalik sa hilaga'; @override - String weatherRankingAnalysisRange(String value) { - return 'Saklaw $value°C'; - } + String get rainInterval2d => '2 araw'; @override - String get reportListEmpty => 'Walang ulat ng lindol'; + String get mapTownLabelsHint => + 'Ipakita ang mga pangalan ng bayan kapag naka-zoom'; @override - String get reportListEmptyFiltered => - 'Walang ulat na tumutugma sa mga filter'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => 'Mga babala sa tsunami lamang'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => 'Advanced'; @override - String get reportListLocalFelt => 'Lokal na naramdaman'; + String get weatherRankingExtremeRange => 'Saklaw sa araw'; @override - String get reportListToday => 'Ngayon'; + String get notifySettingsMenu => 'Mga setting ng notipikasyon'; @override - String get reportListYesterday => 'Kahapon'; + String get typhoonHistoryTitle => 'Dataset time'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app (default)'; } @override - String get reportListEnd => 'Dulo ng listahan'; + String get trendRange24h => '24 oras'; @override - String get reportFilterTitle => 'Mga filter'; + String get mapLayerStyleJmaTooltip => + 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; @override - String get reportFilterSort => 'Pagkakasunud-sunod'; + String weatherRankingRecordedAt(String time) { + return 'Naitala noong $time'; + } @override - String get reportFilterSortTime => 'Oras'; + String get mapLayerRain => 'Ulan'; @override - String get reportFilterSortIntensity => 'Intensity'; + String get mapLayerQpesums => 'Pagtaya ng ulan sa susunod na 1 oras'; @override - String get reportFilterSortMagnitude => 'Magnitude'; + String get mapOverlaySectionMap => 'Mapa'; @override - String get reportFilterSortDepth => 'Lalim'; + String get mapTerrainRelief => 'Rehiyebo ng terrain'; @override - String get reportFilterOrderDesc => 'Pababa'; + String get eewMaxIntensity => 'Pinakamataas na intensidad'; @override - String get reportFilterOrderAsc => 'Pataas'; + String get mapLegendCollapse => 'Itago ang alamat'; @override - String get reportFilterIntensity => 'Intensity'; + String get changelogTitle => 'Changelog'; @override - String get reportFilterIntensityInfoTitle => - 'Bagong at lumang intensity scale'; + String get reportFilterOrderDesc => 'Pababa'; @override - String get reportFilterIntensityInfoIntro => - 'Pinalitan ng CWA ang intensity scale noong 1 Ene 2020 (oras ng Taipei).'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyTitle => 'Luma (bago ang 2020)'; + String get reportFilterIntensityInfoTitle => + 'Bagong at lumang intensity scale'; @override - String get reportFilterIntensityInfoLegacyBody => - 'Antas 0–7 lang; walang 5−/5+/6−/6+.'; + String get mapLayerTyphoon => 'Bagyo'; @override - String get reportFilterIntensityInfoModernTitle => 'Bago (mula 2020)'; + String get radarOverlayMenuTooltip => 'Mga opsyon sa layer ng radar'; @override - String get reportFilterIntensityInfoModernBody => - 'Antas 0–4, 5−, 5+, 6−, 6+, 7. Gamit ng filter ang bagong scale; ang mga lumang event ay may legacy label sa listahan.'; + String get mapMyLocation => 'Aking lokasyon'; @override - String get reportFilterMagnitude => 'Magnitude'; + String get meshtasticNodes => 'Nodes'; @override - String get reportFilterDepth => 'Depth'; + String get meshtasticSend => 'Send'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get typhoonOverlayStormL7Tooltip => + 'Level-7 wind field + average circle (purple)'; @override - String get reportFilterDate => 'Petsa'; + String get aedType => 'Uri'; @override - String get reportFilterDatePick => 'Pumili ng petsa'; + String get termsOfService => 'Mga Tuntunin ng Serbisyo'; @override - String get reportFilterDateStartNote => 'Start day: from 00:00(Taipei)'; + String get typhoonLegendCircle25 => 'Storm circle (L10)'; @override - String get reportFilterDateEndNote => 'End day: through 24:00(Taipei)'; + String get sponsorTitle => 'Suportahan ang DPIP'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get mapNavSatellite => 'Satellite'; @override - String get reportFilterLocation => 'Lokasyon'; + String homeRainTrendUpdated(String time) { + return 'Na-update $time'; + } @override - String get reportFilterLocationHint => 'hal. Hualien, offshore'; + String get onboardingNext => 'Susunod'; @override - String get reportFilterAny => 'Lahat'; + String get weatherRankingMergeTown => 'Bayan'; @override - String get reportFilterApply => 'I-apply'; + String get mapLayerMonitor => 'Seismic Monitor'; @override - String get reportFilterReset => 'I-reset'; + String get moreYoutube => 'YouTube'; @override - String get reportListSearch => 'Maghanap'; + String get sponsorSubscriptions => 'Mga subscription'; @override - String get reportDetailTitle => 'Ulat ng Lindol'; + String typhoonValueLon(String lon) { + return '$lon°E'; + } @override - String reportDetailNumbered(String number) { - return 'Blg. $number Makabuluhang Naramdamang Lindol'; - } + String get skyTime => 'Oras ng langit'; @override - String get reportDetailLocalFelt => 'Lokal na Naramdamang Lindol'; + String get weatherModeCloudy => 'Maulap'; @override - String get reportDetailInfo => 'Mga Detalye'; + String get skyTimeDusk => 'Takipsilim'; @override - String get reportDetailOriginTime => 'Oras ng pangyayari'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailEpicenter => 'Coordinates ng Epicenter'; + String get reportFilterDateEndNote => 'End day: through 24:00(Taipei)'; @override - String get reportDetailMagnitude => 'Magnitude'; + String get reportFilterSortMagnitude => 'Magnitude'; @override - String get reportDetailDepth => 'Lalim ng Hypocenter'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailAreaIntensity => 'Intensity ayon sa lugar'; + String get mapLayerCategoryEarthquake => 'Lindol'; @override - String get reportDetailLocalIntensity => 'Intensity sa iyong lokasyon'; + String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; @override - String get reportDetailLocalIntensityUnavailable => - 'Walang datos ng intensity'; + String get typhoonLegendPast => 'Aktwal na landas'; @override - String get reportDetailSortByIntensity => 'Ayusin ayon sa intensity'; + String get restroomCategoryOther => 'Iba pa'; @override - String get reportDetailSortByCounty => 'Ayusin ayon sa lalawigan'; + String homeForecastHighLow(String high, String low) { + return 'T $high° · B $low°'; + } @override - String get reportDetailImage => 'Larawan ng Ulat'; + String get locationBannerFix => 'Buksan ang mga setting'; @override - String get reportDetailImageUnavailable => - 'Wala pang available na larawan ng ulat'; + String get mapLegendExpand => 'Alamat'; @override - String get reportDetailOpenReport => 'Pahina ng Ulat'; + String get eewNone => 'Walang aktibong maagang babala sa lindol'; @override - String get reportDetailReplay => 'I-replay'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get navMore => 'Higit Pa'; + String get notifyOptTsunamiAll => 'Mga abiso at babala sa tsunami'; @override - String get appLogs => 'Mga log ng app'; + String get meshtasticLayerOptions => 'Node options'; @override - String get changelogTitle => 'Changelog'; + String get onboardingAgreeContinue => 'Sumang-ayon at magpatuloy'; @override - String get changelogEmpty => 'Wala pang release notes'; + String get commonRetry => 'Subukan Muli'; @override - String get changelogTypePrerelease => 'Beta'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogTypeStable => 'Stable'; + String reportDetailNumbered(String number) { + return 'Blg. $number Makabuluhang Naramdamang Lindol'; + } @override - String get changelogCurrentVersion => 'Kasalukuyan'; + String get typhoonOverlayStormBandSubtitle => 'With average circle'; @override - String get changelogVersionDetails => 'Detalye ng release'; + String get disasterMapOverlayRestroomTooltip => + 'Ipakita ang mga pampublikong palikuran'; @override - String get changelogBodyEmpty => 'Walang tala para sa release na ito.'; + String get weatherRankingTitle => 'Mga ranggo ng obserbasyon'; @override - String get mapPlaceholderDisabled => 'Mapa (pansamantalang naka-disable)'; + String get homeRainTrendHeavySustained => + 'Tuloy-tuloy na malakas na ulan sa susunod na oras'; @override - String get moreSectionRegion => 'Rehiyon'; + String get notifySectionTsunami => 'Tsunami'; @override - String get moreSectionNotify => 'Mga Abiso'; + String get restroomCategoryPark => 'Parke'; @override - String get moreSectionDisplay => 'Display'; + String get moreLinkOpenFailed => 'Hindi mabuksan ang link'; @override - String get regionManageTitle => 'Mga naka-save na rehiyon'; + String get themeDark => 'Madilim'; @override - String get regionAddButton => 'Magdagdag ng rehiyon'; + String get sponsorRestore => 'Ibalik ang mga pagbili'; @override - String get regionEmpty => 'Wala pang naka-save na rehiyon'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String get regionSelectTitle => 'Pumili ng rehiyon'; + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String regionSelectCount(int count, int max) { - return '$count/$max ang napili'; - } + String get meshtasticTraffic => 'Traffic'; @override - String regionSelectFull(int max) { - return 'Maaari kang mag-save ng hanggang $max na rehiyon'; - } + String get mapLayerStyleBdTooltip => + 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; @override - String get regionEdit => 'I-edit'; + String get disasterMapOverlayAedTooltip => 'Show AED locations'; @override - String get moreSectionAdvanced => 'Advanced'; + String get mapLayerHumidity => 'Halumigmig'; @override - String get moreDeveloper => 'Impormasyon sa debug'; + String get mapLayerSatelliteTransparentNight => + 'Night = transparent, the basemap shows'; @override - String get experimentalFeatures => 'Mga experimental na feature'; + String get meshtasticScanning => 'Scanning…'; @override - String get moreSectionLinks => 'Mga Link'; + String regionSelectFull(int max) { + return 'Maaari kang mag-save ng hanggang $max na rehiyon'; + } @override - String get moreCwaEew => 'Maagang babala sa lindol ng CWA'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreTremReport => 'Ulat ng pagtukoy ng TREM'; + String get navMore => 'Higit Pa'; @override - String get moreServerStatus => 'Katayuan ng server'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreAnnouncements => 'Mga Anunsyo'; + String get disasterMapOverlaySectionLayers => 'Layers'; @override - String get moreDiscord => 'Komunidad sa Discord'; + String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; @override - String get moreNotifyLog => 'Log ng notipikasyon ng DPIP'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get moreLinkOpenFailed => 'Hindi mabuksan ang link'; + String get typhoonLabelNe => 'NE'; @override - String get weatherDynamicState => 'Animation ng panahon'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherDynamicStateSubtitle => - 'I-override ang panahon sa background ng home'; + String get reportListEmpty => 'Walang ulat ng lindol'; @override - String get weatherModeAuto => 'Awtomatiko'; + String get reportListEnd => 'Dulo ng listahan'; @override - String get weatherModeClear => 'Maaliwalas'; + String get mapLayerSatelliteTruecolor => 'Himawari True Color'; @override - String get weatherModeRain => 'Ulan'; + String get typhoonOverlaySectionExtra => 'Overlays'; @override - String get weatherModeFog => 'Makapal na Hamog'; + String get eewSWave => 'S wave'; @override - String get weatherModeThunderstorm => 'Kulog at Kidlat'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get commonLoading => 'Naglo-load…'; + String get restroomCategoryCultural => 'Pook na pangkultura'; @override - String get commonRetry => 'Subukan Muli'; + String get typhoonLabelWind => 'Max. sustained wind near centre'; @override - String get commonError => 'May Nangyaring Mali'; + String get radarGlobalOutlineHint => 'Panlabas na balangkas ng bawat bansa'; @override - String get commonFetchFailed => 'Hindi ma-load ang data. Pakisubukan muli.'; + String get notifyEvacuation => 'Impormasyon sa sakuna'; @override - String get commonEmpty => 'Walang Maipakita'; + String get typhoonLegendCircle15 => 'Gale circle (L7)'; @override - String get feedConnecting => 'Kumokonekta…'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedStale => 'Maaaring luma na ang datos'; + String get homeRainTrendLightSustained => + 'Tuloy-tuloy na mahinang ulan sa susunod na oras'; @override - String get feedOffline => 'Nawala ang koneksyon'; + String get commonError => 'May Nangyaring Mali'; @override - String get eewTitle => 'Maagang babala sa lindol'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String get eewNone => 'Walang aktibong maagang babala sa lindol'; + String get meshtasticPower => 'Power'; @override - String eewSummary(String magnitude, String depth) { - return 'M$magnitude · lalim $depth km'; + String get mapTimelineNow => 'Ngayon'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => 'Buong bansa'; + String get reportDetailOpenReport => 'Pahina ng Ulat'; @override - String get regionCurrent => 'Kasalukuyang lokasyon'; + String get trendRange7d => '7 araw'; @override - String get regionCurrentUnavailable => - 'Hindi makuha ang kasalukuyang lokasyon'; + String typhoonWarningAreas(String areas) { + return 'Areas: $areas'; + } @override - String get weatherPrecipitation => 'Pag-ulan'; + String get rainIntervalSection => 'Window ng oras'; @override - String get weatherHumidity => 'Halumigmig'; + String get notifyTitle => 'Mga Notipikasyon'; @override - String weatherDataTime(String station, String time) { - return '$station · Oras ng datos $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => 'Tingnan sa mapa'; + String get restroomCategoryLabel => 'Kategorya'; @override - String get homeForecastTitle => '24-oras na forecast'; + String get sponsorRestoring => 'Ibinabalik ang mga pagbili…'; @override - String homeForecastHighLow(String high, String low) { - return 'T $high° · B $low°'; - } + String get sponsorIntro => + 'Nakatuon ang DPIP sa pagbibigay ng real-time na impormasyon sa pag-iwas sa sakuna, nang walang ad o iba pang modelo ng kita. Tumutulong ang inyong suporta na mapanatili ang mga server at magpatuloy sa pagbuo.'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => 'Address'; @override - String homeForecastFeelsLike(String temp) { - return 'Pakiramdam $temp°'; - } + String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; @override - String homeForecastHumidity(String value) { - return 'Halumigmig $value%'; - } + String get restroomCategoryCommercial => 'Komersyal na establisyimento'; @override - String homeForecastWind(String direction, String level) { - return '$direction · Force $level'; - } + String get aedRegion => 'Rehiyon'; @override - String get homeForecastUnavailable => - 'Pumili ng bayan para makita ang forecast'; + String homeRainTrendLightStopping(int minutes) { + return 'Baka huminto ang mahinang ulan sa loob ng $minutes minuto'; + } @override - String get homeForecastEmpty => 'Walang forecast'; + String get reportDetailInfo => 'Mga Detalye'; @override - String get homeActiveEventsTitle => 'Mga aktibong event'; + String get mapNavWind => 'Hangin'; @override - String get homeActiveEventsEmpty => 'Walang aktibong event'; + String get windForecastOverlayMenuTooltip => + 'Mga opsyon sa layer ng pagtataya ng hangin'; @override - String get homeRainTrendTitle => 'Ulan sa susunod na oras'; + String get dataWeatherRankingSubtitle => 'Live na ranggo ng istasyon'; @override String homeRainTrendMinute(int minute) { @@ -525,1321 +519,2131 @@ class AppLocalizationsFil extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return 'Na-update $time'; - } + String get rainInterval6h => '6 oras'; @override - String get homeRainTrendNoData => 'Walang data'; + String get restroomTypeUnspecified => 'Hindi natukoy'; @override - String get homeRainTrendScattered => 'Posibleng mahinang ulan'; + String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; @override - String get homeRainTrendLightSustained => - 'Tuloy-tuloy na mahinang ulan sa susunod na oras'; + String get mapLayerSatelliteGlobalOutline => 'Country border'; @override - String homeRainTrendLightStopping(int minutes) { - return 'Baka huminto ang mahinang ulan sa loob ng $minutes minuto'; - } + String get mapNavTemperature => 'Temperatura'; @override - String get homeRainTrendHeavySustained => - 'Tuloy-tuloy na malakas na ulan sa susunod na oras'; + String get typhoonLegendForecastPoint => 'Punto ng forecast'; @override - String homeRainTrendHeavyStopping(int minutes) { - return 'Baka huminto ang malakas na ulan sa loob ng $minutes minuto'; - } + String get reportListYesterday => 'Kahapon'; @override - String get mapLayers => 'Mga Layer'; + String get moreSectionLinks => 'Mga Link'; @override - String get mapLayerOrderTitle => 'Ayusin ang ayos ng layer'; + String get feedOffline => 'Nawala ang koneksyon'; @override - String get mapLayerOrderReset => 'I-reset ang ayos'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => 'Composite Radar Reflectivity'; + String get moreSectionDisplay => 'Display'; @override - String get mapLayerSatellite => 'Himawari Infrared (B13)'; + String get rainInterval3d => '3 araw'; @override - String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; + String get defaultMapLayerSubtitle => + 'Bubukas ang tab ng Mapa sa layer na ito. Susunod ang icon at label ng bottom navigation.'; @override - String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; + String get aedDescription => 'Tala'; @override - String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; + String get typhoonOverlayWeatherRadarTooltip => + 'Radar echo closest to the typhoon bulletin time'; @override - String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + String get onboardingPermLocationDesc => + 'Itutok ang mga alerto sa kinaroroonan mo.'; @override - String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; + String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; @override - String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + String get homeActiveEventsEmpty => 'Walang aktibong event'; @override - String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + String get typhoonLabelPosition => 'Centre location'; @override - String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; + String get weatherRankingBy => 'Ayon sa'; @override - String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; + String get typhoonIntensityMild => 'Mild typhoon'; @override - String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + String get windForecastGlobalOutlineHint => + 'Panlabas na balangkas ng bawat bansa'; @override - String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; + String get rainInterval1h => '1 oras'; @override - String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; + String get eewLocalIntensity => 'Tantiya sa lokasyon'; @override - String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + String get mapLayerRadar => 'Composite Radar Reflectivity'; @override - String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + String get restroomCategoryReligious => 'Relihiyosong lugar'; @override - String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; + String get mapLayerSatelliteCloudCloudy => 'Cloudy'; @override - String get mapLayerSatelliteTruecolor => 'Himawari True Color'; + String get skyTimeSunrise => 'Pagsikat ng araw'; @override - String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => 'Himawari Ash'; + String get onboardingPermNotifyDesc => + 'Ihatid ang mga alerto sa lindol, panahon, at sakuna sa sandaling maganap ang mga ito.'; @override - String get mapLayerSatelliteDust => 'Himawari Dust'; + String get radarTownOutline => 'Mga hangganan ng bayan'; @override - String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + String get mapLayerStyleSection => 'Colour style'; @override - String get mapLayerSatelliteNightmicrophysics => - 'Himawari Night Microphysics'; + String get disasterMapOverlayMenuTooltip => 'Disaster map layers'; @override - String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; + String get typhoonLabelSw => 'SW'; @override - String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + String typhoonForecastLead(String hours) { + return 'Forecast +$hours h'; + } @override - String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + String get dpmDisasterTsunami => 'Tsunami'; @override - String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; + String get changelogTypeStable => 'Stable'; @override - String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; + String get mapLayerSatelliteTransparentClear => + 'Clear sky = transparent, the basemap shows'; @override - String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + String get mapOverlaySectionReference => 'Layer ng sanggunian'; @override - String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; + String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; @override - String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; + String get reportListLocalFelt => 'Lokal na naramdaman'; @override - String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + String get weatherRankingEmpty => 'Walang obserbasyon na iraranggo'; @override - String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + String get notifySectionOther => 'Iba pa'; @override - String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; + String weatherRankingMeta(String time, int count) { + return 'Oras ng datos: $time\n$count istasyon'; + } @override - String get mapLayerSatelliteGlobalOutline => 'Country border'; + String get onboardingTermsAgree => + 'Nabasa ko na at sumasang-ayon ako sa Mga Tuntunin ng Serbisyo'; @override - String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + String get mapLayerSatelliteTransparentNoVegetation => + 'Below 0.1 = transparent (no vegetation)'; @override - String get mapLayerSatelliteCloudClear => 'Clear'; + String get notifyOptLocalIntensity4 => 'Lokal na intensidad 4 pataas'; @override - String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + String get eewArrived => 'Dumating'; @override - String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => 'Cloudy'; + String get mapLayerCategoryLife => 'Pang-araw-araw na buhay'; @override - String get mapLayerSatelliteTransparentWarm => - 'Clear sky (warm end) = transparent, the basemap shows'; + String get reportFilterSortIntensity => 'Intensity'; @override - String get mapLayerSatelliteTransparentReflectance => - 'Low reflectance / night = transparent, the basemap shows'; + String get typhoonMotion => 'Gumagalaw'; @override - String get mapLayerSatelliteTransparentZero => - 'Zero difference = transparent (no signal)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => - 'Night = transparent, the basemap shows'; + String get typhoonIntensityIntense => 'Intense typhoon'; @override - String get mapLayerSatelliteTransparentNoData => - 'No data (land) = transparent'; + String get mapLayerOrderTitle => 'Ayusin ang ayos ng layer'; @override - String get mapLayerSatelliteTransparentNoVegetation => - 'Below 0.1 = transparent (no vegetation)'; + String get dpmYes => 'Oo'; @override - String get mapLayerSatelliteTransparentNoWater => - '≤ 0 = transparent (no water)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => - 'Clear sky = transparent, the basemap shows'; + String get reportDetailLocalIntensityUnavailable => + 'Walang datos ng intensity'; @override - String get mapLayerStyleSection => 'Colour style'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => 'Colour style'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => 'Grayscale (JMA)'; + String get reportFilterDepth => 'Depth'; @override - String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; + String get onboardingScrollHint => 'Mag-scroll pababa para magpatuloy'; @override - String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + String get mapNavQpesums => 'Pagtaya'; @override - String get mapLayerStyleJmaTooltip => - 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; + String get navMap => 'Mapa'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => 'Mga advisory sa panahon'; @override - String get mapLayerStyleBdTooltip => - 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; + String get reportFilterReset => 'I-reset'; @override - String get mapLayerQpesums => 'Pagtaya ng ulan sa susunod na 1 oras'; + String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; @override - String get mapLayerLightning => 'Kidlat'; + String get typhoonOverlaySectionStorm => 'Storm wind'; @override - String lightningLegendCg(int minutes) { - return 'Ulap–lupa · $minutes min'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return 'Ulap–ulap · $minutes min'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => 'Ngayon'; + String get weatherDynamicStateSubtitle => + 'I-override ang panahon sa background ng home'; @override - String get mapTimelinePast => 'Nakaraan'; + String get reportFilterIntensityInfoModernTitle => 'Bago (mula 2020)'; @override - String get mapTimelineFuture => 'Hinaharap'; + String typhoonDataTime(String time) { + return 'Data time\n$time'; + } @override - String get mapTimelineObserved => 'Naobserbahan'; + String get restroomTypeAccessible => 'Palikurang may accessibility'; @override - String get mapTimelineForecast => 'Pagtaya'; + String get moreSectionAbout => 'Tungkol'; @override - String mapTimelineDataTime(String time) { - return 'Oras ng data $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => 'Mga setting ng notipikasyon'; + String get onboardingIntroBody => + 'Ang DPIP ang iyong kasama sa pag-iwas sa sakuna. Pinagsasama-sama nito ang mga maagang babala sa lindol, ulat ng lindol, panahon, at impormasyon sa panganib, at inaalertuhan ka sa sandaling mahalaga ito.\n\n• Mga lindol: mga maagang babala, ulat ng intensidad, at detalyadong ulat\n• Panahon: real-time na mensahe ng kulog at kidlat at mga advisory sa panahon\n• Impormasyon sa tsunami at sakuna\n\nSusunod, hihilingin naming basahin mo ang Mga Tuntunin ng Serbisyo at magbigay ng ilang pahintulot para maprotektahan ka ng DPIP nang real time.'; @override - String get notifyTitle => 'Mga Notipikasyon'; + String get shelterCapacityLabel => 'Kapasidad'; @override - String get notifyUnavailable => - 'Hindi pa handa ang push notifications — subukan muli mamaya.'; + String get reportDetailImage => 'Larawan ng Ulat'; @override - String get notifySetFailed => 'Hindi ma-save ang setting. Pakisubukan muli.'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => 'Maagang babala sa lindol'; + String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; @override - String get notifySectionEarthquake => 'Lindol'; + String get onboardingPermNotify => 'Mga Notipikasyon'; @override - String get notifySectionWeather => 'Panahon'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => 'Tsunami'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => 'Iba pa'; + String get defaultMapLayerSettings => 'Default na layer ng mapa'; @override - String get notifyEew => 'Emergency na alerto sa lindol'; + String get moreSectionNotify => 'Mga Abiso'; @override - String get notifyMonitor => 'Monitor ng malakas na paggalaw'; + String get notifyUnavailable => + 'Hindi pa handa ang push notifications — subukan muli mamaya.'; @override - String get notifyReport => 'Ulat ng lindol'; + String get mapLayerOrderReset => 'I-reset ang ayos'; @override - String get notifyIntensity => 'Ulat ng intensidad'; + String get dpmAddress => 'Address'; @override - String get notifyThunderstorm => 'Mga alerto sa kulog at kidlat'; + String get weatherRankingMergeCounty => 'Lalawigan'; @override - String get notifyAdvisory => 'Mga advisory sa panahon'; + String get moreSectionApp => 'Kunin ang app'; @override - String get notifyEvacuation => 'Impormasyon sa sakuna'; + String get reportFilterIntensityInfoLegacyBody => + 'Antas 0–7 lang; walang 5−/5+/6−/6+.'; @override - String get notifyTsunami => 'Impormasyon sa tsunami'; + String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; @override - String get notifyAnnouncement => 'Mga Anunsyo'; + String get qpesumsOverlayMenuTooltip => + 'Mga opsyon sa layer ng pagtataya ng pag-ulan'; @override - String get notifyOptOff => 'Naka-off'; + String get mapTimelineFuture => 'Hinaharap'; @override - String get notifyOptAll => 'Tumanggap ng lahat'; + String get typhoonLegendCircleAvg => 'Average circle'; @override - String get notifyOptLocalIntensity4 => 'Lokal na intensidad 4 pataas'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => 'Lokal na intensidad 1 pataas'; + String get typhoonLabelSe => 'SE'; @override - String get notifyOptWeatherLocal => 'Kasalukuyang lokasyon lamang'; + String get radarTownOutlineHint => 'Mas pinong hati'; @override - String get notifyOptTsunamiWarning => 'Mga babala sa tsunami lamang'; + String eewCountdown(int seconds) { + return '$seconds segundo'; + } @override - String get notifyOptTsunamiAll => 'Mga abiso at babala sa tsunami'; + String get typhoonLabelGust => 'Peak gust'; @override - String get onboardingNext => 'Susunod'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => 'Bumalik'; + String get sponsorTerms => 'Mga Tuntunin ng Paggamit'; @override - String get onboardingScrollHint => 'Mag-scroll pababa para magpatuloy'; + String get restroomTypeGenderNeutral => 'Palikurang neutral sa kasarian'; @override - String get onboardingIntroTitle => 'Maligayang pagdating sa DPIP'; + String get notifyThunderstorm => 'Mga alerto sa kulog at kidlat'; @override - String get onboardingIntroBody => - 'Ang DPIP ang iyong kasama sa pag-iwas sa sakuna. Pinagsasama-sama nito ang mga maagang babala sa lindol, ulat ng lindol, panahon, at impormasyon sa panganib, at inaalertuhan ka sa sandaling mahalaga ito.\n\n• Mga lindol: mga maagang babala, ulat ng intensidad, at detalyadong ulat\n• Panahon: real-time na mensahe ng kulog at kidlat at mga advisory sa panahon\n• Impormasyon sa tsunami at sakuna\n\nSusunod, hihilingin naming basahin mo ang Mga Tuntunin ng Serbisyo at magbigay ng ilang pahintulot para maprotektahan ka ng DPIP nang real time.'; + String get skyTimeGolden => 'Gintong oras'; @override - String get onboardingTermsTitle => 'Mga Tuntunin ng Serbisyo'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'Mangyaring basahin ang mga sumusunod na paunawa bago gamitin ang DPIP:\n\n• Ang lahat ng impormasyon ay dapat sumunod sa nilalamang inilathala ng Central Weather Administration (CWA).\n\n• Depende sa kalagayan ng network, server, app, at pinagmumulan ng datos, may posibilidad na hindi matanggap ang impormasyon; ginagawa namin ang lahat ng aming makakaya upang maiwasan ito ngunit hindi namin magagarantiya na hindi ito mangyayari.\n\n• Maaaring maunang makarating sa iyong lokasyon ang malakas na pagyanig bago pa dumating ang notipikasyon.\n\n• Ang mga maagang babala sa lindol ay mabilis na kinakalkulang resulta na maaaring magtaglay ng malaking pagkakamali — unawain ito at gamitin nang may pag-iingat.\n\n• Anumang gawaing hindi pinahihintulutan ng mga awtoridad ay maaaring magdala ng panganib sa batas; mangyaring sundin ang lahat ng naaangkop na regulasyon.\n\nBukod dito, upang magbigay ng lokal na mga alerto, kinokolekta at ini-upload ng serbisyong ito ang iyong tinatayang lokasyon at push identifier — sa foreground at background — para lamang matukoy kung aling mga alerto ang ipapadala sa iyo.\n\nSa pamamagitan ng pag-tap sa \"Sumang-ayon at magpatuloy\" kinukumpirma mo na nabasa, naunawaan, at sinasang-ayunan mo ang nasa itaas.'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => - 'Nabasa ko na at sumasang-ayon ako sa Mga Tuntunin ng Serbisyo'; + String weatherRankingAnalysisCurrent(String value) { + return 'Ngayon $value°C'; + } @override - String get onboardingAgreeContinue => 'Sumang-ayon at magpatuloy'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => 'Mga Pahintulot'; + String get homeForecastUnavailable => + 'Pumili ng bayan para makita ang forecast'; @override - String get onboardingPermsBody => - 'Para maalertuhan ka ng DPIP sa sandaling maganap ang sakuna, mangyaring ibigay ang mga sumusunod. Maaari mo itong baguhin anumang oras sa mga setting ng system.'; + String get mapLayers => 'Mga Layer'; @override - String get onboardingPermNotify => 'Mga Notipikasyon'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => - 'Ihatid ang mga alerto sa lindol, panahon, at sakuna sa sandaling maganap ang mga ito.'; + String get languageSettings => 'Wika'; @override - String get onboardingPermCritical => 'Mga kritikal na alerto'; + String get dpmDisasterNuclear => 'Aksidente sa nukleyar'; @override - String get onboardingPermCriticalDesc => - 'Hayaang tumunog ang mga nakamamatay na babala sa lindol kahit sa silent mode o Do Not Disturb.'; + String get language => 'Wika'; @override - String get onboardingPermLocation => 'Lokasyon'; + String homeForecastFeelsLike(String temp) { + return 'Pakiramdam $temp°'; + } @override - String get onboardingPermLocationDesc => - 'Itutok ang mga alerto sa kinaroroonan mo.'; + String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; @override - String get onboardingPermBackground => 'Lokasyon sa background'; + String get skyTimeDawn => 'Bukang-liwayway'; @override - String get onboardingPermBackgroundDesc => - 'Payagan ang \"Always\" para patuloy kang matukoy ng mga alerto kahit sarado ang app.'; + String get skyTimeAfternoon => 'Hapon'; @override - String get onboardingPermBattery => 'Exemption sa baterya'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'Payagan ang DPIP na patuloy na tumakbo sa background para hindi maantala o mapalampas ang mga alerto.'; + String get typhoonWarningTitle => 'Typhoon warning'; @override - String get onboardingGrant => 'Ibigay'; + String get moreSourceCode => 'Source code'; @override - String get onboardingGranted => 'Naibigay na'; + String get mapLayerCategoryWeather => 'Obserbasyon sa panahon'; @override - String get onboardingStart => 'Magsimula'; + String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; @override - String get language => 'Wika'; + String get windForecastTownOutlineHint => 'Ang mas pinong mesh'; @override - String get languageSettings => 'Wika'; + String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; @override - String get languageSystem => 'Default ng system'; + String get mapAppCopyCoordinates => 'Kopyahin ang coordinates'; @override - String get locationBannerServiceOff => - 'Naka-off ang mga serbisyo ng lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.'; + String get reportFilterIntensityInfoIntro => + 'Pinalitan ng CWA ang intensity scale noong 1 Ene 2020 (oras ng Taipei).'; @override - String get locationBannerPermission => - 'Naka-off ang pahintulot sa lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.'; + String get mapNavEarthquake => 'Lindol'; @override - String get locationBannerFix => 'Buksan ang mga setting'; + String get typhoonGust => 'Ugong'; @override - String get notifyBannerDisabled => - 'Naka-off ang mga notification — hindi ka makakatanggap ng mga alerto sa sakuna.'; + String get restroomGradeAverage => 'Katamtaman'; @override - String get onboardingSkipTitle => 'Hindi pa naibibigay ang mga pahintulot'; + String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; @override - String get onboardingSkipBody => - 'Kung walang lokasyon at mga notification, hindi ka maaalertuhan ng DPIP nang real time sa mga lindol at sakuna malapit sa iyo. Maaari mo pa ring ibigay ang mga ito sa ibang pagkakataon sa Settings.'; + String get onboardingPermBackgroundDesc => + 'Payagan ang \"Always\" para patuloy kang matukoy ng mga alerto kahit sarado ang app.'; @override - String get onboardingSkipStay => 'Bumalik'; + String get mapTimelineForecast => 'Pagtaya'; @override - String get onboardingSkipLeave => 'Laktawan pa rin'; + String get restroomTypeLabel => 'Uri'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => 'Lindol'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => + 'Level-10 wind field + average circle (yellow)'; @override - String get moreSourceCode => 'Source code'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => 'Kunin ang app'; + String get reportDetailTitle => 'Ulat ng Lindol'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'Ulat ng pagtukoy ng TREM'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · Oras ng datos $time'; + } @override - String get displaySettings => 'Pagpapakita'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => 'Default na layer ng mapa'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - 'Bubukas ang tab ng Mapa sa layer na ito. Susunod ang icon at label ng bottom navigation.'; + String get radarCountyOutline => 'Mga hangganan ng lalawigan'; @override - String get mapNavRadar => 'Radar'; + String get onboardingGranted => 'Naibigay na'; @override - String get mapNavQpesums => 'Pagtaya'; + String get commonClose => 'Isara'; @override - String get mapNavSatellite => 'Satellite'; + String get restroomGradeLabel => 'Baitang'; @override - String get mapNavLightning => 'Kidlat'; + String get rainIntervalNow => 'Ngayon'; @override - String get mapNavTyphoon => 'Bagyo'; + String get changelogCurrentVersion => 'Kasalukuyan'; @override - String get mapNavEarthquake => 'Lindol'; + String get typhoonLabelPressure => 'Central pressure'; @override - String get mapNavTemperature => 'Temperatura'; + String get typhoonOverlayForecastCalloutsTooltip => + 'Show forecast-point detail cards when zoomed in'; @override - String get mapNavHumidity => 'Halumigmig'; + String get aedOpenRemark => 'Tala sa oras'; @override - String get mapNavPressure => 'Presyon'; + String get onboardingPermsBody => + 'Para maalertuhan ka ng DPIP sa sandaling maganap ang sakuna, mangyaring ibigay ang mga sumusunod. Maaari mo itong baguhin anumang oras sa mga setting ng system.'; @override - String get mapNavWind => 'Hangin'; + String get typhoonOverlaySectionWeather => 'Weather underlay'; @override - String get mapNavRain => 'Ulan'; + String get notifyOptWeatherLocal => 'Kasalukuyang lokasyon lamang'; @override - String get mapNavDisaster => 'Sakuna'; + String get mapNavRain => 'Ulan'; @override - String get displayTheme => 'Tema'; + String get moonDays => 'days'; @override - String get themeSystem => 'Sistema'; + String mapLegendUnit(String unit) { + return 'Yunit: $unit'; + } @override - String get themeLight => 'Maliwanag'; + String get weatherModeClear => 'Maaliwalas'; @override - String get themeDark => 'Madilim'; + String get meshtasticRadio => 'Radio'; @override - String get moreSectionAbout => 'Tungkol'; + String get commonEmpty => 'Walang Maipakita'; @override - String get termsOfService => 'Mga Tuntunin ng Serbisyo'; + String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; @override - String get faq => 'Mga FAQ'; + String get meshtasticExternalPower => 'External power'; @override - String get openSourceLicenses => 'Mga lisensya ng open-source'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get sponsorTitle => 'Suportahan ang DPIP'; + String get reportFilterOrderAsc => 'Pataas'; @override - String get sponsorIntro => - 'Nakatuon ang DPIP sa pagbibigay ng real-time na impormasyon sa pag-iwas sa sakuna, nang walang ad o iba pang modelo ng kita. Tumutulong ang inyong suporta na mapanatili ang mga server at magpatuloy sa pagbuo.'; + String get reportFilterApply => 'I-apply'; + + @override + String get reportDetailImageUnavailable => + 'Wala pang available na larawan ng ulat'; + + @override + String get weatherRankingHighest => 'Pinakamataas'; + + @override + String get reportDetailReplay => 'I-replay'; + + @override + String get mapLayerRestroom => 'Pampublikong Palikuran'; + + @override + String get restroomCategoryWelfare => 'Institusyon ng kapakanan'; + + @override + String get restroomGradeExcellent => 'Napakahusay'; + + @override + String get meshtasticLastSent => 'Last sent'; + + @override + String get meshtasticName => 'Name'; + + @override + String get meshtasticScan => 'Scan'; + + @override + String get mapLayerCategoryForecast => 'Numerical forecast'; + + @override + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; + + @override + String get themeSystem => 'Sistema'; + + @override + String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + + @override + String get typhoonLegendForecast => 'Tinatayang landas'; + + @override + String typhoonValueHpa(String n) { + return '$n hPa'; + } + + @override + String get weatherPrecipitation => 'Pag-ulan'; + + @override + String get moonNextFullMoon => 'Next full moon'; + + @override + String get dpmSheetEmpty => 'I-tap ang marker sa mapa para sa detalye'; + + @override + String get onboardingSkipLeave => 'Laktawan pa rin'; + + @override + String get onboardingBack => 'Bumalik'; + + @override + String get aedPlaceDesc => 'Lokasyon ng paglagay'; + + @override + String get onboardingSkipTitle => 'Hindi pa naibibigay ang mga pahintulot'; + + @override + String get restroomTypeFamily => 'Palikuran ng pamilya'; + + @override + String typhoonValueKm(String n) { + return '$n km'; + } + + @override + String get typhoonPressure => 'Presyon'; + + @override + String get onboardingPermBattery => 'Exemption sa baterya'; + + @override + String get typhoonLabelNw => 'NW'; + + @override + String get dpmDisasterFlood => 'Baha'; + + @override + String get moonPhaseWaxingCrescent => 'Waxing crescent'; + + @override + String get restroomCategoryLeisure => 'Lugar ng libangan'; + + @override + String get mapLayerTemperature => 'Temperatura'; + + @override + String get aedCategory => 'Kategorya'; + + @override + String get meshtasticChannels => 'Channels'; + + @override + String get monitorWaiting => 'Naghihintay ng data…'; + + @override + String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + + @override + String get reportDetailEpicenter => 'Coordinates ng Epicenter'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => 'Hangin'; + + @override + String get reportDetailMagnitude => 'Magnitude'; + + @override + String get reportDetailAreaIntensity => 'Intensity ayon sa lugar'; + + @override + String get rainInterval12h => '12 oras'; + + @override + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } + + @override + String get dpmDisasterLandslide => 'Pagguho ng lupa'; + + @override + String get notifyMonitor => 'Monitor ng malakas na paggalaw'; + + @override + String get onboardingStart => 'Magsimula'; + + @override + String sponsorPerMonth(String price) { + return '$price / buwan'; + } + + @override + String get mapLayerPressure => 'Presyon'; + + @override + String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + + @override + String get mapLayerSatelliteTransparentZero => + 'Zero difference = transparent (no signal)'; + + @override + String get shelterIndoorLabel => 'Silungan sa loob'; + + @override + String get notifyOptOff => 'Naka-off'; + + @override + String get reportFilterSortTime => 'Oras'; + + @override + String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + + @override + String get weatherModeThunderstorm => 'Kulog at Kidlat'; + + @override + String get homeViewOnMap => 'Tingnan sa mapa'; + + @override + String get reportFilterIntensityInfoLegacyTitle => 'Luma (bago ang 2020)'; + + @override + String get typhoonLabelSpeed => 'Past movement speed'; + + @override + String mapAppOpenFailed(String app) { + return 'Hindi mabuksan ang $app'; + } + + @override + String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + + @override + String get meshtasticReceived => 'Received'; + + @override + String get weatherRankingExtremeLow => 'Pinakamababa ngayong araw'; + + @override + String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + + @override + String get mapLayerSatelliteTransparentNoWater => + '≤ 0 = transparent (no water)'; + + @override + String get shelterCategoryLabel => 'Mga uri ng kalamidad'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => 'Bugso'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => 'Mga uri ng sakuna sa silungan'; + + @override + String get moreServerStatus => 'Katayuan ng server'; + + @override + String get notifySectionWeather => 'Panahon'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => 'Seismic'; + + @override + String get changelogBodyEmpty => 'Walang tala para sa release na ito.'; + + @override + String get radarGlobalOutline => 'Mga hangganan ng bansa'; + + @override + String get notifyEew => 'Emergency na alerto sa lindol'; + + @override + String get regionNationwide => 'Buong bansa'; + + @override + String get moreNotifyLog => 'Log ng notipikasyon ng DPIP'; + + @override + String get regionCurrent => 'Kasalukuyang lokasyon'; + + @override + String get dpmFilterSectionRestroom => 'Mga uri ng lugar'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => 'Niyebe'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => 'Impormasyon sa debug'; + + @override + String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => 'Kidlat'; + + @override + String get homeForecastEmpty => 'Walang forecast'; + + @override + String get sponsorOneTime => 'Isang beses'; + + @override + String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + + @override + String get onboardingPermBackground => 'Lokasyon sa background'; + + @override + String get aedEmergencyPhone => 'Emergency phone'; + + @override + String get dpmOpenInMaps => 'Buksan sa mapa'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + 'Hayaang tumunog ang mga nakamamatay na babala sa lindol kahit sa silent mode o Do Not Disturb.'; + + @override + String get mapLayerSatelliteTransparentWarm => + 'Clear sky (warm end) = transparent, the basemap shows'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => '24-oras na forecast'; + + @override + String get typhoonLegendWarningAreas => 'Warning areas'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; + } + + @override + String get notifyOptLocalIntensity1 => 'Lokal na intensidad 1 pataas'; + + @override + String get mapTimelinePast => 'Nakaraan'; + + @override + String get restroomTypeFemale => 'Palikuran ng babae'; + + @override + String get reportListToday => 'Ngayon'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => 'Naglo-load…'; + + @override + String get typhoonIntensityModerate => 'Moderate typhoon'; + + @override + String get typhoonWind => 'Hangin'; + + @override + String get mapLayerSatelliteAsh => 'Himawari Ash'; + + @override + String get rainInterval3h => '3 oras'; + + @override + String get reportListSearch => 'Maghanap'; + + @override + String get mapLayerCategorySatellite => 'Satellite'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => 'Lokasyon'; + + @override + String get mapLayerSatelliteNightmicrophysics => + 'Himawari Night Microphysics'; + + @override + String get typhoonIntensityTd => 'Tropical depression'; + + @override + String get reportFilterDate => 'Petsa'; + + @override + String get sponsorRestoreUnavailable => + 'Hindi maabot ang store. Pakisubukan muli mamaya.'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => 'Wala pang naka-save na rehiyon'; + + @override + String get onboardingPermBatteryDesc => + 'Payagan ang DPIP na patuloy na tumakbo sa background para hindi maantala o mapalampas ang mga alerto.'; + + @override + String get mapNavDisaster => 'Sakuna'; + + @override + String get radarScanRangeSubtitle => + 'Ipinapakita ang aktwal na saklaw ng apat na radar.'; + + @override + String get aedHoursSunday => 'Oras sa Linggo'; + + @override + String get reportDetailOriginTime => 'Oras ng pangyayari'; + + @override + String get trendNoData => 'Walang trend data'; + + @override + String get onboardingPermLocation => 'Lokasyon'; + + @override + String get moreDiscord => 'Komunidad sa Discord'; + + @override + String get mapNavPressure => 'Presyon'; + + @override + String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => 'Wala pang release notes'; + + @override + String get reportFilterDateStartNote => 'Start day: from 00:00(Taipei)'; + + @override + String get eewTitle => 'Maagang babala sa lindol'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '$count/$max ang napili'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => 'Makulimlim'; + + @override + String get reportDetailDepth => 'Lalim ng Hypocenter'; + + @override + String get typhoonOverlayWarningTooltip => + 'Highlight counties under a typhoon warning'; + + @override + String get reportFilterDatePick => 'Pumili ng petsa'; + + @override + String get onboardingSkipStay => 'Bumalik'; + + @override + String get commonFetchFailed => 'Hindi ma-load ang data. Pakisubukan muli.'; + + @override + String get shelterOutdoorLabel => 'Silungan sa labas'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => 'Radar'; + + @override + String get mapLayerSatelliteCloudClear => 'Clear'; + + @override + String eewSummary(String magnitude, String depth) { + return 'M$magnitude · lalim $depth km'; + } + + @override + String get locationBannerPermission => + 'Naka-off ang pahintulot sa lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.'; + + @override + String get typhoonOverlayWeatherNoneTooltip => + 'No radar or infrared underlay'; + + @override + String get radarCountyOutlineHint => 'Iginuguhit sa ibabaw ng echo'; + + @override + String get windForecastCountyOutlineHint => + 'Iginuhit sa itaas ng patlang ng hangin'; + + @override + String get homeRainTrendTitle => 'Ulan sa susunod na oras'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => 'Bagyo'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => 'Pinagsamang palikuran'; + + @override + String get restroomGradeGood => 'Mahusay'; + + @override + String get notifyTsunami => 'Impormasyon sa tsunami'; + + @override + String get navData => 'Datos'; + + @override + String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => 'Hindi makatawag ang device na ito'; + + @override + String get reportFilterAny => 'Lahat'; + + @override + String get weatherRankingMergeTo => 'Pagsamahin'; + + @override + String get notifyIntensity => 'Ulat ng intensidad'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => 'Bintana ng akumulasyon'; + + @override + String get reportDetailLocalFelt => 'Lokal na Naramdamang Lindol'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => 'Ibigay'; + + @override + String get weatherModeRain => 'Ulan'; + + @override + String get shelterVulnerableOkLabel => 'Angkop para sa mahihina'; + + @override + String get stationSheetEmpty => 'I-tap ang istasyon para makita ang datos'; + + @override + String get typhoonLegendProbability => 'Strike probability'; + + @override + String get reportFilterMagnitude => 'Magnitude'; + + @override + String get skyTimeMorning => 'Umaga'; + + @override + String get experimentalFeatures => 'Mga experimental na feature'; + + @override + String get onboardingTermsBody => + 'Mangyaring basahin ang mga sumusunod na paunawa bago gamitin ang DPIP:\n\n• Ang lahat ng impormasyon ay dapat sumunod sa nilalamang inilathala ng Central Weather Administration (CWA).\n\n• Depende sa kalagayan ng network, server, app, at pinagmumulan ng datos, may posibilidad na hindi matanggap ang impormasyon; ginagawa namin ang lahat ng aming makakaya upang maiwasan ito ngunit hindi namin magagarantiya na hindi ito mangyayari.\n\n• Maaaring maunang makarating sa iyong lokasyon ang malakas na pagyanig bago pa dumating ang notipikasyon.\n\n• Ang mga maagang babala sa lindol ay mabilis na kinakalkulang resulta na maaaring magtaglay ng malaking pagkakamali — unawain ito at gamitin nang may pag-iingat.\n\n• Anumang gawaing hindi pinahihintulutan ng mga awtoridad ay maaaring magdala ng panganib sa batas; mangyaring sundin ang lahat ng naaangkop na regulasyon.\n\nBukod dito, upang magbigay ng lokal na mga alerto, kinokolekta at ini-upload ng serbisyong ito ang iyong tinatayang lokasyon at push identifier — sa foreground at background — para lamang matukoy kung aling mga alerto ang ipapadala sa iyo.\n\nSa pamamagitan ng pag-tap sa \"Sumang-ayon at magpatuloy\" kinukumpirma mo na nabasa, naunawaan, at sinasang-ayunan mo ang nasa itaas.'; + + @override + String get reportFilterTitle => 'Mga filter'; + + @override + String get onboardingPermCritical => 'Mga kritikal na alerto'; + + @override + String trendCumulativeTotal(String total) { + return 'Kabuuang $total mm'; + } + + @override + String get languageName => 'Filipino'; + + @override + String get reportListEmptyFiltered => + 'Walang ulat na tumutugma sa mga filter'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => 'Bagyo'; + + @override + String get weatherModeSand => 'Alikabok'; + + @override + String get typhoonSatelliteTitle => 'Satellite'; + + @override + String get notifyReport => 'Ulat ng lindol'; + + @override + String get mapAppCoordinatesCopied => 'Na-kopya ang coordinates'; + + @override + String get skyTimeNight => 'Gabi'; + + @override + String get sponsorRecommended => 'Inirerekomenda'; + + @override + String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + + @override + String get weatherRankingWind => 'Bilis ng hangin'; + + @override + String get feedStale => 'Maaaring luma na ang datos'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · Force $level'; + } + + @override + String get navHome => 'Tahanan'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => 'Mga lisensya ng open-source'; + + @override + String get weatherRankingLowest => 'Pinakamababa'; + + @override + String get reportFilterSortDepth => 'Lalim'; + + @override + String mapTimelineDataTime(String time) { + return 'Oras ng data $time'; + } + + @override + String get radarScanRange => 'Ipakita ang saklaw ng pag-scan'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return 'Saklaw $value°C'; + } + + @override + String get weatherRankingExtremeHigh => 'Pinakamataas ngayong araw'; + + @override + String get changelogVersionDetails => 'Detalye ng release'; + + @override + String get sponsorPrivacy => 'Patakaran sa Privacy'; + + @override + String get reportDetailLocalIntensity => 'Intensity sa iyong lokasyon'; + + @override + String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; @override - String get sponsorSubscriptions => 'Mga subscription'; + String shelterCapacityValue(int n) { + return '$n katao'; + } @override - String get sponsorRecommended => 'Inirerekomenda'; + String lightningLegendCc(int minutes) { + return 'Ulap–ulap · $minutes min'; + } @override - String get sponsorOneTime => 'Isang beses'; + String get meshtasticSendHint => 'Message to broadcast'; @override - String sponsorPerMonth(String price) { - return '$price / buwan'; + String monitorDelay(String value) { + return 'Pagkaantala $value s'; } @override - String get sponsorRestore => 'Ibalik ang mga pagbili'; + String get dpmNo => 'Hindi'; @override - String get sponsorTerms => 'Mga Tuntunin ng Paggamit'; + String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; @override - String get sponsorPrivacy => 'Patakaran sa Privacy'; + String get meshtasticReconnecting => 'Reconnecting…'; @override - String get sponsorRestoring => 'Ibinabalik ang mga pagbili…'; + String get radarTownOutlineSubtitle => + 'Nananatiling mababasa ang mga hangganan ng bayan sa ilalim ng radar echo.'; @override - String get sponsorRestoreUnavailable => - 'Hindi maabot ang store. Pakisubukan muli mamaya.'; + String get typhoonOverlayWeatherSatelliteTooltip => + 'Infrared closest to the typhoon bulletin time'; @override - String get commonClose => 'Isara'; + String get radarScanRangeHint => 'Sa labas: hindi naoobserbahan'; @override - String get mapLayerTemperature => 'Temperatura'; + String typhoonPickerTd(String no) { + return 'Tropical depression TD $no'; + } @override - String get trendRange24h => '24 oras'; + String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; @override - String get trendRange7d => '7 araw'; + String get regionAddButton => 'Magdagdag ng rehiyon'; @override - String get trendNoData => 'Walang trend data'; + String get displaySettings => 'Pagpapakita'; @override - String trendCumulativeTotal(String total) { - return 'Kabuuang $total mm'; + String get restroomGradePoor => 'Mas mababa sa pamantayan'; + + @override + String get restroomCategoryTourist => 'Lugar para sa turista'; + + @override + String get locationBannerServiceOff => + 'Naka-off ang mga serbisyo ng lokasyon — hindi matutukoy ng mga lokal na alerto ang iyong lugar.'; + + @override + String get mapLayerStyleTooltip => 'Colour style'; + + @override + String lightningLegendCg(int minutes) { + return 'Ulap–lupa · $minutes min'; } @override - String chartHourLabel(int hour) { - return '${hour}h'; + String get skyTimeAuto => 'Awtomatiko'; + + @override + String get appLogs => 'Mga log ng app'; + + @override + String get feedConnecting => 'Kumokonekta…'; + + @override + String get notifyBannerDisabled => + 'Naka-off ang mga notification — hindi ka makakatanggap ng mga alerto sa sakuna.'; + + @override + String get weatherHumidity => 'Halumigmig'; + + @override + String typhoonValueMs(String n) { + return '$n m/s'; } @override - String get mapLayerHumidity => 'Halumigmig'; + String homeForecastHumidity(String value) { + return 'Halumigmig $value%'; + } @override - String get mapLayerPressure => 'Presyon'; + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; @override - String get mapLayerWind => 'Hangin'; + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; @override - String get mapLayerRain => 'Ulan'; + String get restroomCategoryTransport => 'Transportasyon'; @override - String get rainIntervalMenu => 'Bintana ng akumulasyon'; + String get reportFilterLocationHint => 'hal. Hualien, offshore'; @override - String get rainIntervalNow => 'Ngayon'; + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; @override - String get rainInterval10m => '10 min'; + String get meshtasticBattery => 'Battery'; @override - String get rainInterval1h => '1 oras'; + String get meshtasticDistance => 'Distansya'; @override - String get rainInterval3h => '3 oras'; + String get meshtasticSnrTrend => 'Trend ng signal (SNR)'; @override - String get rainInterval6h => '6 oras'; + String get meshtasticBatteryTrend => 'Trend ng baterya'; @override - String get rainInterval12h => '12 oras'; + String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; @override - String get rainInterval24h => '24 oras'; + String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; @override - String get rainInterval2d => '2 araw'; + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } @override - String get rainInterval3d => '3 araw'; + String get notifySectionEarthquake => 'Lindol'; @override - String get mapLayerTyphoon => 'Bagyo'; + String get mapLayerDisasterMap => 'Disaster Map'; @override - String get typhoonNoActive => 'Walang aktibong bagyo'; + String get weatherModeFog => 'Makapal na Hamog'; @override - String get typhoonWind => 'Hangin'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } @override - String get typhoonGust => 'Ugong'; + String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; @override - String get typhoonPressure => 'Presyon'; + String get moreAnnouncements => 'Mga Anunsyo'; @override - String get typhoonMotion => 'Gumagalaw'; + String get mapLayerSatelliteTransparentNoData => + 'No data (land) = transparent'; @override - String get typhoonLabelPosition => 'Centre location'; + String get restroomCategoryGovernment => 'Opisina ng gobyerno'; @override - String get typhoonLabelDirection => 'Past movement direction'; + String get typhoonLegendCurrent => 'Kasalukuyang sentro'; @override - String get typhoonLabelSpeed => 'Past movement speed'; + String get aedAddress => 'Address'; @override - String get typhoonLabelPressure => 'Central pressure'; + String get mapLayerAed => 'AED'; @override - String get typhoonLabelWind => 'Max. sustained wind near centre'; + String get changelogTypePrerelease => 'Beta'; @override - String get typhoonLabelGust => 'Peak gust'; + String get reportFilterIntensityInfoModernBody => + 'Antas 0–4, 5−, 5+, 6−, 6+, 7. Gamit ng filter ang bagong scale; ang mga lumang event ay may legacy label sa listahan.'; @override - String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; + String get typhoonOverlayWeatherNone => 'None'; @override - String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; + String get mapLayerStyleGray => 'Grayscale (JMA)'; + + @override + String get weatherModeAuto => 'Awtomatiko'; @override String get typhoonLabelProbCircle => '70% probability circle'; @override - String typhoonForecastLead(String hours) { - return 'Forecast +$hours h'; + String get notifyOptAll => 'Tumanggap ng lahat'; + + @override + String get displayTheme => 'Tema'; + + @override + String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + + @override + String get typhoonLabelDirection => 'Past movement direction'; + + @override + String get regionManageTitle => 'Mga naka-save na rehiyon'; + + @override + String get typhoonLegendCone => 'Kono ng forecast'; + + @override + String get moreCwaEew => 'Maagang babala sa lindol ng CWA'; + + @override + String get onboardingPermsTitle => 'Mga Pahintulot'; + + @override + String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + + @override + String get rainInterval10m => '10 min'; + + @override + String weatherRankingAnalysisLow(String value) { + return 'Mababa $value'; } @override - String get typhoonLabelNw => 'NW'; + String get meshtasticConnectAnyway => 'Connect anyway'; @override - String get typhoonLabelNe => 'NE'; + String reportListDayCount(int count) { + return '$count'; + } @override - String get typhoonLabelSw => 'SW'; + String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; @override - String get typhoonLabelSe => 'SE'; + String get mapLayerSatelliteTransparentReflectance => + 'Low reflectance / night = transparent, the basemap shows'; + + @override + String chartHourLabel(int hour) { + return '${hour}h'; + } + + @override + String get mapLayerShelter => 'Silungan'; + + @override + String get typhoonOverlayProbabilityTooltip => + 'Show strike probability (hides the forecast cone)'; + + @override + String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => 'Ipakita ang mga silungan'; + + @override + String get mapNavHumidity => 'Halumigmig'; + + @override + String get reportDetailSortByIntensity => 'Ayusin ayon sa intensity'; + + @override + String get homeRainTrendNoData => 'Walang data'; + + @override + String get mapLayerCategoryRadar => 'Radar'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + + @override + String get typhoonTrackDetail => 'Track detail'; + + @override + String get dataSectionWeather => 'Panahon'; + + @override + String get aedHoursWeekday => 'Oras sa weekday'; + + @override + String get homeActiveEventsTitle => 'Mga aktibong event'; + + @override + String weatherRankingAnalysisHigh(String value) { + return 'Mataas $value'; + } + + @override + String get faq => 'Mga FAQ'; + + @override + String get typhoonHistoryLive => 'Live'; + + @override + String eewSerial(int serial) { + return 'Ulat $serial'; + } + + @override + String get reportFilterSort => 'Pagkakasunud-sunod'; + + @override + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; + + @override + String get dataEarthquakeSubtitle => 'Mga ulat ng lindol'; + + @override + String get typhoonNoActive => 'Walang aktibong bagyo'; + + @override + String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; + + @override + String get navEvents => 'Mga Kaganapan'; @override - String typhoonValueLat(String lat) { - return '$lat°N'; - } + String get onboardingTermsTitle => 'Mga Tuntunin ng Serbisyo'; @override - String typhoonValueLon(String lon) { - return '$lon°E'; - } + String get mapTownLabels => 'Mga pangalan ng bayan'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => 'Hindi ma-save ang setting. Pakisubukan muli.'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '$n m/s'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return 'Data time\n$time'; - } + String get notifyAnnouncement => 'Mga Anunsyo'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'Maligayang pagdating sa DPIP'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => + 'Hindi makuha ang kasalukuyang lokasyon'; @override - String get mapLayerMonitor => 'Seismic Monitor'; + String get languageSystem => 'Default ng system'; @override - String get mapLayerDisasterMap => 'Disaster Map'; + String get skyTimeSunset => 'Paglubog ng araw'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'Himawari Dust'; @override - String get disasterMapOverlayMenuTooltip => 'Disaster map layers'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => 'Layers'; + String get regionEdit => 'I-edit'; @override - String get disasterMapOverlayAedTooltip => 'Show AED locations'; + String get weatherDynamicState => 'Animation ng panahon'; @override - String get aedAddress => 'Address'; + String get mapPlaceholderDisabled => 'Mapa (pansamantalang naka-disable)'; @override - String get aedRegion => 'Rehiyon'; + String get moonNow => 'Ngayon'; @override - String get aedCategory => 'Kategorya'; + String get moonSectionAppearance => 'Anyo'; @override - String get aedType => 'Uri'; + String get moonSectionRiseSet => 'Pagsikat at paglubog'; @override - String get aedPlaceDesc => 'Lokasyon ng paglagay'; + String get moonSectionUpcoming => 'Susunod'; @override - String get aedDescription => 'Tala'; + String get moonSectionCalendar => 'Kalendaryo'; @override - String get aedHoursWeekday => 'Oras sa weekday'; + String get moonDistance => 'Distansya'; @override - String get aedHoursSaturday => 'Oras sa Sabado'; + String get moonKilometres => 'km'; @override - String get aedHoursSunday => 'Oras sa Linggo'; + String get moonApparentSize => 'Lapad sa langit'; @override - String get aedOpenRemark => 'Tala sa oras'; + String get moonRise => 'Pagsikat ng buwan'; @override - String get aedEmergencyPhone => 'Emergency phone'; + String get moonSet => 'Paglubog ng buwan'; @override - String get mapLayerRestroom => 'Pampublikong Palikuran'; + String get moonNextNewMoon => 'Susunod na bagong buwan'; @override - String get mapLayerShelter => 'Silungan'; + String get moonAlwaysUp => 'Nasa itaas buong araw'; @override - String get disasterMapOverlayRestroomTooltip => - 'Ipakita ang mga pampublikong palikuran'; + String get moonNoEvent => 'Wala sa araw na ito'; @override - String get disasterMapOverlayShelterTooltip => 'Ipakita ang mga silungan'; + String get sunTitle => 'Araw'; @override - String get dpmOpenInMaps => 'Buksan sa mapa'; + String get sunSubtitle => 'Pagsikat, takipsilim at solar terms'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => 'Liwanag ng araw'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => 'Takipsilim'; @override - String mapAppDefault(String app) { - return '$app (default)'; - } + String get sunSectionLight => 'Liwanag'; @override - String get mapAppCopyCoordinates => 'Kopyahin ang coordinates'; + String get sunSectionSundial => 'Orasang araw'; @override - String get mapAppCoordinatesCopied => 'Na-kopya ang coordinates'; + String get sunSectionTerms => 'Solar terms'; @override - String mapAppOpenFailed(String app) { - return 'Hindi mabuksan ang $app'; - } + String get sunRise => 'Pagsikat ng araw'; @override - String get mapAppCallFailed => 'Hindi makatawag ang device na ito'; + String get sunSet => 'Paglubog ng araw'; @override - String get mapOverlaySectionReference => 'Layer ng sanggunian'; + String get sunNoon => 'Tanghaling tapat'; @override - String get mapLayerCategoryEarthquake => 'Lindol'; + String get sunDayLength => 'Haba ng araw'; @override - String get mapLayerCategoryTyphoon => 'Bagyo'; + String get sunTwilightCivil => 'Sibil'; @override - String get mapLayerCategoryWeather => 'Obserbasyon sa panahon'; + String get sunTwilightNautical => 'Nautical'; @override - String get mapLayerCategorySatellite => 'Satellite'; + String get sunTwilightAstronomical => 'Astronomical'; @override - String get mapLayerCategoryRadar => 'Radar'; + String get sunGoldenHourMorning => 'Golden hour sa umaga'; @override - String get mapLayerCategoryLife => 'Pang-araw-araw na buhay'; + String get sunGoldenHourEvening => 'Golden hour sa hapon'; @override - String get mapLayerCategoryForecast => 'Numerical forecast'; + String get sunBlueHour => 'Blue hour'; @override - String get mapOverlaySectionMap => 'Mapa'; + String get sunEquationOfTime => 'Equation of time'; @override - String get rainIntervalSection => 'Window ng oras'; + String get sunMinutes => 'min'; @override - String get mapTownLabels => 'Mga pangalan ng bayan'; + String get solarTermNext => 'Susunod na termino'; @override - String get mapTownLabelsHint => - 'Ipakita ang mga pangalan ng bayan kapag naka-zoom'; + String get planetsTitle => 'Mga planeta'; @override - String get mapTerrainRelief => 'Rehiyebo ng terrain'; + String get planetsSubtitle => 'Nasaan ngayong gabi, at gaano kaliwanag'; @override - String get mapTerrainReliefHint => 'Ipakita ang anino ng terrain sa base map'; + String get planetsSectionTonight => 'Ngayon'; @override - String get dpmSheetEmpty => 'I-tap ang marker sa mapa para sa detalye'; + String get planetUp => 'Nasa itaas'; @override - String get dpmAddress => 'Address'; + String get planetDown => 'Nasa ibaba'; @override - String get restroomTypeLabel => 'Uri'; + String get planetInGlare => 'Malapit sa araw'; @override - String get restroomCategoryLabel => 'Kategorya'; + String get planetMagnitude => 'Magnitude'; @override - String get restroomGradeLabel => 'Baitang'; + String get planetElongation => 'Elongation'; @override - String get restroomTypeFemale => 'Palikuran ng babae'; + String get planetSky => 'Panahon'; @override - String get restroomTypeMale => 'Palikuran ng lalaki'; + String get planetEvening => 'Gabi'; @override - String get restroomTypeMixed => 'Pinagsamang palikuran'; + String get planetMorning => 'Umaga'; @override - String get restroomTypeAccessible => 'Palikurang may accessibility'; + String get planetDistance => 'Distansya'; @override - String get restroomTypeGenderNeutral => 'Palikurang neutral sa kasarian'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => 'Palikuran ng pamilya'; + String get planetAltitude => 'Taas'; @override - String get restroomTypeUnspecified => 'Hindi natukoy'; + String get planetMercury => 'Mercury'; @override - String get restroomCategoryTransport => 'Transportasyon'; + String get planetVenus => 'Venus'; @override - String get restroomCategoryPark => 'Parke'; + String get planetMars => 'Mars'; @override - String get restroomCategoryCommercial => 'Komersyal na establisyimento'; + String get planetJupiter => 'Jupiter'; @override - String get restroomCategoryReligious => 'Relihiyosong lugar'; + String get planetSaturn => 'Saturn'; @override - String get restroomCategoryCultural => 'Pook na pangkultura'; + String get planetUranus => 'Uranus'; @override - String get restroomCategoryGovernment => 'Opisina ng gobyerno'; + String get planetNeptune => 'Neptune'; @override - String get restroomCategoryWelfare => 'Institusyon ng kapakanan'; + String get solarTermVernalEquinox => 'Vernal Equinox'; @override - String get restroomCategoryTourist => 'Lugar para sa turista'; + String get solarTermPureBrightness => 'Pure Brightness'; @override - String get restroomCategoryLeisure => 'Lugar ng libangan'; + String get solarTermGrainRain => 'Grain Rain'; @override - String get restroomCategoryOther => 'Iba pa'; + String get solarTermStartOfSummer => 'Simula ng Tag-init'; @override - String get restroomGradeExcellent => 'Napakahusay'; + String get solarTermGrainFull => 'Grain Full'; @override - String get restroomGradeGood => 'Mahusay'; + String get solarTermGrainInEar => 'Grain in Ear'; @override - String get restroomGradeAverage => 'Katamtaman'; + String get solarTermSummerSolstice => 'Summer Solstice'; @override - String get restroomGradePoor => 'Mas mababa sa pamantayan'; + String get solarTermMinorHeat => 'Minor Heat'; @override - String get shelterAddressLabel => 'Address'; + String get solarTermMajorHeat => 'Major Heat'; @override - String get shelterCapacityLabel => 'Kapasidad'; + String get solarTermStartOfAutumn => 'Simula ng Taglagas'; @override - String shelterCapacityValue(int n) { - return '$n katao'; - } + String get solarTermEndOfHeat => 'End of Heat'; @override - String get shelterCategoryLabel => 'Mga uri ng kalamidad'; + String get solarTermWhiteDew => 'White Dew'; @override - String get shelterIndoorLabel => 'Silungan sa loob'; + String get solarTermAutumnalEquinox => 'Autumnal Equinox'; @override - String get shelterOutdoorLabel => 'Silungan sa labas'; + String get solarTermColdDew => 'Cold Dew'; @override - String get shelterVulnerableOkLabel => 'Angkop para sa mahihina'; + String get solarTermFrostDescent => 'Frost Descent'; @override - String get dpmYes => 'Oo'; + String get solarTermStartOfWinter => 'Simula ng Taglamig'; @override - String get dpmNo => 'Hindi'; + String get solarTermMinorSnow => 'Minor Snow'; @override - String get stationSheetEmpty => 'I-tap ang istasyon para makita ang datos'; + String get solarTermMajorSnow => 'Major Snow'; @override - String monitorDelay(String value) { - return 'Pagkaantala $value s'; - } + String get solarTermWinterSolstice => 'Winter Solstice'; @override - String get monitorWaiting => 'Naghihintay ng data…'; + String get solarTermMinorCold => 'Minor Cold'; @override - String mapLegendUnit(String unit) { - return 'Yunit: $unit'; - } + String get solarTermMajorCold => 'Major Cold'; @override - String get typhoonLegendPast => 'Aktwal na landas'; + String get solarTermStartOfSpring => 'Simula ng Tagsibol'; @override - String get typhoonIntensityTd => 'Tropical depression'; + String get solarTermRainWater => 'Rain Water'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => 'Awakening of Insects'; @override - String typhoonPickerTd(String no) { - return 'Tropical depression TD $no'; - } + String get tonightTitle => 'Ngayong gabi'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => 'Ano ang makikita, at kailan'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => 'Oras ng obserbasyon'; @override - String get typhoonIntensityMild => 'Mild typhoon'; + String get tonightAstronomicalNight => 'Astronomical na gabi'; @override - String get typhoonIntensityModerate => 'Moderate typhoon'; + String get tonightNeverDark => 'Hindi tuluyang dumidilim'; @override - String get typhoonIntensityIntense => 'Intense typhoon'; + String get tonightDarkWindow => 'Madilim na yugto'; @override - String get typhoonLegendForecast => 'Tinatayang landas'; + String get tonightMoonAllNight => 'Buwan nasa langit buong gabi'; @override - String get typhoonLegendForecastPoint => 'Punto ng forecast'; + String get tonightDarkTotal => 'Kabuuang dilim'; @override - String get typhoonLegendCurrent => 'Kasalukuyang sentro'; + String get tonightMoonlight => 'Liwanag ng buwan'; @override - String get typhoonLegendCone => 'Kono ng forecast'; + String get tonightSectionShowers => 'Mga meteor shower'; @override - String get mapLegendExpand => 'Alamat'; + String get tonightRadiantDown => 'Hindi sumisikat ang radiant'; @override - String get mapLegendCollapse => 'Itago ang alamat'; + String get tonightPerHour => '/oras'; @override - String get mapMyLocation => 'Aking lokasyon'; + String get tonightSectionSatellites => 'Pagdaan ng satelayt'; @override - String get mapResetNorth => 'Bumalik sa hilaga'; + String get tonightSectionTargets => 'Nakikita ngayon'; @override - String get typhoonLegendCircle15 => 'Gale circle (L7)'; + String get showerQuadrantids => 'Quadrantids'; @override - String get typhoonLegendCircleAvg => 'Average circle'; + String get showerLyrids => 'Lyrids'; @override - String get typhoonLegendCircle25 => 'Storm circle (L10)'; + String get showerEtaAquariids => 'Eta Aquariids'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => 'Delta Aquariids'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => 'Perseids'; @override - String get typhoonLegendProbability => 'Strike probability'; + String get showerOrionids => 'Orionids'; @override - String get typhoonLegendWarningAreas => 'Warning areas'; + String get showerSouthernTaurids => 'Southern Taurids'; @override - String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + String get showerLeonids => 'Leonids'; @override - String get typhoonOverlaySectionStorm => 'Storm wind'; + String get showerGeminids => 'Geminids'; @override - String get typhoonOverlaySectionExtra => 'Overlays'; + String get showerUrsids => 'Ursids'; @override - String get typhoonOverlayStormBandSubtitle => 'With average circle'; + String get deepSkyOpenCluster => 'Open cluster'; @override - String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; + String get deepSkyGlobularCluster => 'Globular cluster'; @override - String get typhoonOverlayProbabilityTooltip => - 'Show strike probability (hides the forecast cone)'; + String get deepSkySpiralGalaxy => 'Spiral galaxy'; @override - String get typhoonOverlayWarningTooltip => - 'Highlight counties under a typhoon warning'; + String get deepSkyEllipticalGalaxy => 'Elliptical galaxy'; @override - String get typhoonOverlayStormL7Tooltip => - 'Level-7 wind field + average circle (purple)'; + String get deepSkyIrregularGalaxy => 'Irregular galaxy'; @override - String get typhoonOverlayStormL10Tooltip => - 'Level-10 wind field + average circle (yellow)'; + String get deepSkyPlanetaryNebula => 'Planetary nebula'; @override - String get typhoonOverlaySectionWeather => 'Weather underlay'; + String get deepSkySupernovaRemnant => 'Supernova remnant'; @override - String get typhoonOverlayWeatherNone => 'None'; + String get deepSkyEmissionNebula => 'Emission nebula'; @override - String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; + String get deepSkyReflectionNebula => 'Reflection nebula'; @override - String get typhoonOverlayWeatherNoneTooltip => - 'No radar or infrared underlay'; + String get deepSkyAsterism => 'Asterism'; @override - String get typhoonOverlayWeatherRadarTooltip => - 'Radar echo closest to the typhoon bulletin time'; + String get almanacTitle => 'Almanake'; @override - String get typhoonOverlayWeatherSatelliteTooltip => - 'Infrared closest to the typhoon bulletin time'; + String get almanacSubtitle => 'Petsang lunisolar at mga eklipse'; @override - String get typhoonWarningTitle => 'Typhoon warning'; + String get almanacSectionToday => 'Ngayon'; @override - String typhoonWarningAreas(String areas) { - return 'Areas: $areas'; - } + String get almanacGregorian => 'Gregorian'; @override - String get typhoonTrackDetail => 'Track detail'; + String get almanacLunar => 'Lunisolar'; @override - String get typhoonHistoryTitle => 'Dataset time'; + String get almanacYear => 'Taon'; @override - String get typhoonHistoryLive => 'Live'; + String get almanacMonthLength => 'Haba ng buwan'; @override - String get typhoonSatelliteTitle => 'Satellite'; + String get almanacLongMonth => '30 araw'; @override - String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + String get almanacShortMonth => '29 araw'; @override - String get typhoonOverlayForecastCalloutsTooltip => - 'Show forecast-point detail cards when zoomed in'; + String get almanacLeapPrefix => 'Leap '; @override - String get dpmFilterSectionRestroom => 'Mga uri ng lugar'; + String get almanacSectionLunarEclipses => 'Eklipse ng buwan'; @override - String get dpmFilterSectionRestroomType => 'Mga uri ng banyo'; + String get almanacSectionSolarEclipses => 'Eklipse ng araw'; @override - String get dpmFilterSectionShelter => 'Mga uri ng sakuna sa silungan'; + String get almanacNoSolarEclipse => 'Wala sa saklaw'; @override - String get dpmDisasterFlood => 'Baha'; + String get eclipseTotal => 'Total'; @override - String get dpmDisasterEarthquake => 'Lindol'; + String get eclipsePartial => 'Parsyal'; @override - String get dpmDisasterLandslide => 'Pagguho ng lupa'; + String get eclipseAnnular => 'Annular'; @override - String get dpmDisasterTsunami => 'Tsunami'; + String get eclipsePenumbral => 'Penumbral'; @override - String get dpmDisasterSlope => 'Panganib sa dalisdis'; + String get zodiacRat => 'Daga'; @override - String get dpmDisasterNuclear => 'Aksidente sa nukleyar'; + String get zodiacOx => 'Baka'; @override - String get skyTime => 'Oras ng langit'; + String get zodiacTiger => 'Tigre'; @override - String get skyTimeAuto => 'Awtomatiko'; + String get zodiacRabbit => 'Kuneho'; @override - String get skyTimeDawn => 'Bukang-liwayway'; + String get zodiacDragon => 'Dragon'; @override - String get skyTimeSunrise => 'Pagsikat ng araw'; + String get zodiacSnake => 'Ahas'; @override - String get skyTimeMorning => 'Umaga'; + String get zodiacHorse => 'Kabayo'; @override - String get skyTimeNoon => 'Tanghali'; + String get zodiacGoat => 'Kambing'; @override - String get skyTimeAfternoon => 'Hapon'; + String get zodiacMonkey => 'Unggoy'; @override - String get skyTimeGolden => 'Gintong oras'; + String get zodiacRooster => 'Manok'; @override - String get skyTimeSunset => 'Paglubog ng araw'; + String get zodiacDog => 'Aso'; @override - String get skyTimeDusk => 'Takipsilim'; + String get zodiacPig => 'Baboy'; @override - String get skyTimeNight => 'Gabi'; + String get tideTitle => 'Taog'; @override - String get weatherModeCloudy => 'Maulap'; + String get tideSubtitle => 'Spring, neap at hila ng buwan'; @override - String get weatherModeOvercast => 'Makulimlim'; + String get tideDisclaimer => + 'Astronomikal na puwersa lamang — hindi talaan ng taog sa daungan. Para sa lebel ng tubig, gamitin ang talaan ng CWA.'; @override - String get weatherModeSnow => 'Niyebe'; + String get tideSectionNow => 'Ngayon'; @override - String get weatherModeSand => 'Alikabok'; + String get tidePhase => 'Siklo'; @override - String get radarScanRange => 'Ipakita ang saklaw ng pag-scan'; + String get tideSpring => 'Spring'; @override - String get radarScanRangeSubtitle => - 'Ipinapakita ang aktwal na saklaw ng apat na radar.'; + String get tideNeap => 'Neap'; @override - String get radarScanRangeHint => 'Sa labas: hindi naoobserbahan'; + String get tideMiddling => 'Katamtaman'; @override - String get radarOverlayMenuTooltip => 'Mga opsyon sa layer ng radar'; + String get tideLunarDistanceFactor => 'Hila ng buwan'; @override - String get radarCountyOutline => 'Mga hangganan ng lalawigan'; + String get tideEquilibrium => 'Equilibrium tide'; @override - String get radarGlobalOutline => 'Mga hangganan ng bansa'; + String get tideMetres => 'm'; @override - String get radarGlobalOutlineHint => 'Panlabas na balangkas ng bawat bansa'; + String get tidePerigeanSpring => 'Susunod na perigean spring'; @override - String get radarCountyOutlineHint => 'Iginuguhit sa ibabaw ng echo'; + String get tideSectionTurningPoints => 'Mga turning point'; @override - String get radarCountyOutlineSubtitle => - 'Nananatiling mababasa ang mga hangganan sa ilalim ng radar echo.'; + String get tideHigh => 'Taas'; @override - String get radarTownOutline => 'Mga hangganan ng bayan'; + String get tideLow => 'Baba'; @override - String get radarTownOutlineHint => 'Mas pinong hati'; + String get skyChartTitle => 'Mapa ng langit'; @override - String get radarTownOutlineSubtitle => - 'Nananatiling mababasa ang mga hangganan ng bayan sa ilalim ng radar echo.'; + String get skyChartSubtitle => 'Ang langit sa itaas mo'; @override - String get qpesumsOverlayMenuTooltip => - 'Mga opsyon sa layer ng pagtataya ng pag-ulan'; + String get skyChartNorth => 'H'; @override - String get windForecastOverlayMenuTooltip => - 'Mga opsyon sa layer ng pagtataya ng hangin'; + String get skyChartEast => 'S'; @override - String get windForecastCountyOutlineHint => - 'Iginuhit sa itaas ng patlang ng hangin'; + String get skyChartSouth => 'T'; @override - String get windForecastGlobalOutlineHint => - 'Panlabas na balangkas ng bawat bansa'; + String get skyChartWest => 'K'; @override - String get windForecastTownOutlineHint => 'Ang mas pinong mesh'; + String tonightElementAge(int days) { + return '$days araw nang luma ang elements'; + } @override - String eewSerial(int serial) { - return 'Ulat $serial'; + String almanacLunarDate(String leap, int month, int day) { + return '${leap}buwan $month, araw $day'; } @override - String get eewMaxIntensity => 'Pinakamataas na intensidad'; + String get tonightNoShowers => 'Walang shower ngayon'; @override - String get eewLocalIntensity => 'Tantiya sa lokasyon'; + String get tonightNoPasses => 'Walang nakikitang pass sa 48 oras'; @override - String get eewSWave => 'S wave'; + String get tonightSatellitesUnavailable => 'Hindi mabasa ang orbit data'; @override - String get eewArrived => 'Dumating'; + String get tonightNoTargets => 'Walang sapat na taas'; @override - String eewCountdown(int seconds) { - return '$seconds segundo'; - } + String get skyChartUnavailable => 'Hindi mabasa ang star catalogue'; } diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index c403bae7c..da6f2e7d5 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,511 +10,504 @@ class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); @override - String get languageName => 'Bahasa Indonesia'; + String typhoonValueLat(String lat) { + return '$lat°N'; + } @override - String get navHome => 'Beranda'; + String get onboardingSkipBody => + 'Tanpa lokasi dan notifikasi, DPIP tidak dapat memperingatkan Anda tentang gempa dan bencana di sekitar Anda secara waktu nyata. Anda masih dapat memberikannya nanti di Pengaturan.'; @override - String get navEvents => 'Kejadian'; + String get rainInterval24h => '24 jam'; @override - String get navMap => 'Peta'; + String homeRainTrendHeavyStopping(int minutes) { + return 'Hujan deras diperkirakan berhenti dalam $minutes menit'; + } @override - String get navData => 'Data'; + String get mapTimelineObserved => 'Diamati'; @override - String get navEarthquake => 'Gempa Bumi'; + String get regionSelectTitle => 'Pilih wilayah'; @override - String get dataSectionSeismic => 'Seismik'; + String get skyTimeNoon => 'Siang'; @override - String get dataEarthquakeSubtitle => 'Laporan gempa'; + String get radarCountyOutlineSubtitle => + 'Menjaga batas wilayah tetap terbaca di bawah gema radar.'; @override - String get dataSectionWeather => 'Cuaca'; + String get dpmFilterSectionRestroomType => 'Jenis toilet'; @override - String get dataWeatherRankingSubtitle => 'Peringkat stasiun langsung'; + String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; @override - String get weatherRankingTitle => 'Peringkat observasi'; + String get reportFilterIntensity => 'Intensitas'; @override - String weatherRankingMeta(String time, int count) { - return 'Waktu data: $time\n$count stasiun'; - } + String get mapLayerLightning => 'Petir'; @override - String get weatherRankingEmpty => 'Tidak ada observasi untuk diurutkan'; + String get restroomTypeMale => 'Toilet pria'; @override - String get weatherRankingBy => 'Urut'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => 'Tertinggi'; + String get reportDetailSortByCounty => 'Urutkan menurut wilayah'; @override - String get weatherRankingLowest => 'Terendah'; + String get homeRainTrendScattered => 'Kemungkinan hujan ringan'; @override - String get weatherRankingMergeTo => 'Gabung'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => 'Kecamatan'; + String get weatherRankingTempExtremes => 'Ekstrem suhu'; @override - String get weatherRankingMergeCounty => 'Kabupaten'; + String get themeLight => 'Terang'; @override - String get weatherRankingWind => 'Kecepatan angin'; + String get mapTerrainReliefHint => 'Tampilkan relief terrain di peta dasar'; @override - String get weatherRankingGust => 'Hembusan'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => 'Ekstrem suhu'; + String get moreSectionRegion => 'Wilayah'; @override - String get weatherRankingExtremeHigh => 'Maksimum hari ini'; + String get dpmDisasterEarthquake => 'Gempa'; @override - String get weatherRankingExtremeLow => 'Minimum hari ini'; + String get mapLayerSatellite => 'Himawari Infrared (B13)'; @override - String get weatherRankingExtremeRange => 'Rentang harian'; + String get aedHoursSaturday => 'Jam Sabtu'; @override - String weatherRankingRecordedAt(String time) { - return 'Tercatat pukul $time'; - } + String get dpmDisasterSlope => 'Bencana lereng'; @override - String weatherRankingAnalysisCurrent(String value) { - return 'Sekarang $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return 'Maks $value'; - } + String get notifySectionEew => 'Peringatan dini gempa'; @override - String weatherRankingAnalysisLow(String value) { - return 'Min $value'; - } + String get mapResetNorth => 'Kembali ke utara'; @override - String weatherRankingAnalysisRange(String value) { - return 'Rentang $value°C'; - } + String get rainInterval2d => '2 hr'; @override - String get reportListEmpty => 'Tidak ada laporan gempa'; + String get mapTownLabelsHint => 'Tampilkan nama kecamatan saat diperbesar'; @override - String get reportListEmptyFiltered => - 'Tidak ada laporan yang cocok dengan filter'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => 'Hanya peringatan tsunami'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => 'Lanjutan'; @override - String get reportListLocalFelt => 'Terasa lokal'; + String get weatherRankingExtremeRange => 'Rentang harian'; @override - String get reportListToday => 'Hari ini'; + String get notifySettingsMenu => 'Pengaturan notifikasi'; @override - String get reportListYesterday => 'Kemarin'; + String get typhoonHistoryTitle => 'Waktu data'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app (bawaan)'; } @override - String get reportListEnd => 'Akhir daftar'; - - @override - String get reportFilterTitle => 'Filter'; + String get trendRange24h => '24 jam'; @override - String get reportFilterSort => 'Urutan'; + String get mapLayerStyleJmaTooltip => + 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; @override - String get reportFilterSortTime => 'Waktu'; + String weatherRankingRecordedAt(String time) { + return 'Tercatat pukul $time'; + } @override - String get reportFilterSortIntensity => 'Intensitas'; + String get mapLayerRain => 'Curah hujan'; @override - String get reportFilterSortMagnitude => 'Magnitudo'; + String get mapLayerQpesums => 'Prakiraan hujan 1 jam ke depan'; @override - String get reportFilterSortDepth => 'Kedalaman'; + String get mapOverlaySectionMap => 'Peta'; @override - String get reportFilterOrderDesc => 'Menurun'; + String get mapTerrainRelief => 'Relief terrain'; @override - String get reportFilterOrderAsc => 'Menaik'; + String get eewMaxIntensity => 'Intensitas maks'; @override - String get reportFilterIntensity => 'Intensitas'; + String get mapLegendCollapse => 'Sembunyikan legenda'; @override - String get reportFilterIntensityInfoTitle => 'Skala intensitas baru & lama'; + String get changelogTitle => 'Catatan pembaruan'; @override - String get reportFilterIntensityInfoIntro => - 'CWA mengganti skala intensitas pada 1 Jan 2020 (waktu Taipei).'; + String get reportFilterOrderDesc => 'Menurun'; @override - String get reportFilterIntensityInfoLegacyTitle => 'Lama (sebelum 2020)'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyBody => - 'Hanya tingkat 0–7, tanpa pemisahan 5−/5+/6−/6+.'; + String get reportFilterIntensityInfoTitle => 'Skala intensitas baru & lama'; @override - String get reportFilterIntensityInfoModernTitle => 'Baru (sejak 2020)'; + String get mapLayerTyphoon => 'Topan'; @override - String get reportFilterIntensityInfoModernBody => - 'Tingkat 0–4, 5−, 5+, 6−, 6+, 7. Slider filter memakai skala baru; peristiwa lama tetap memakai label lama di daftar.'; + String get radarOverlayMenuTooltip => 'Opsi lapisan radar'; @override - String get reportFilterMagnitude => 'Magnitudo'; + String get mapMyLocation => 'Lokasi saya'; @override - String get reportFilterDepth => 'Kedalaman'; + String get meshtasticNodes => 'Nodes'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get meshtasticSend => 'Send'; @override - String get reportFilterDate => 'Tanggal'; + String get typhoonOverlayStormL7Tooltip => + 'Level-7 wind field + average circle (purple)'; @override - String get reportFilterDatePick => 'Pilih tanggal'; + String get aedType => 'Jenis'; @override - String get reportFilterDateStartNote => 'Hari mulai: dari 00:00(Taipei)'; + String get termsOfService => 'Ketentuan Layanan'; @override - String get reportFilterDateEndNote => 'Hari akhir: hingga 24:00(Taipei)'; + String get typhoonLegendCircle25 => 'Lingkar badai'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get sponsorTitle => 'Dukung DPIP'; @override - String get reportFilterLocation => 'Lokasi'; + String get mapNavSatellite => 'Satelit'; @override - String get reportFilterLocationHint => 'mis. Hualien, lepas pantai'; + String homeRainTrendUpdated(String time) { + return 'Diperbarui $time'; + } @override - String get reportFilterAny => 'Semua'; + String get onboardingNext => 'Berikutnya'; @override - String get reportFilterApply => 'Terapkan'; + String get weatherRankingMergeTown => 'Kecamatan'; @override - String get reportFilterReset => 'Reset'; + String get mapLayerMonitor => 'Monitor Seismik'; @override - String get reportListSearch => 'Cari'; + String get moreYoutube => 'YouTube'; @override - String get reportDetailTitle => 'Laporan Gempa'; + String get sponsorSubscriptions => 'Langganan'; @override - String reportDetailNumbered(String number) { - return 'Gempa Dirasakan Signifikan No. $number'; + String typhoonValueLon(String lon) { + return '$lon°E'; } @override - String get reportDetailLocalFelt => 'Gempa Dirasakan Lokal'; + String get skyTime => 'Waktu langit'; @override - String get reportDetailInfo => 'Detail'; + String get weatherModeCloudy => 'Berawan'; @override - String get reportDetailOriginTime => 'Waktu kejadian'; + String get skyTimeDusk => 'Senja'; @override - String get reportDetailEpicenter => 'Koordinat episentrum'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailMagnitude => 'Magnitudo'; + String get reportFilterDateEndNote => 'Hari akhir: hingga 24:00(Taipei)'; @override - String get reportDetailDepth => 'Kedalaman hiposenter'; + String get reportFilterSortMagnitude => 'Magnitudo'; @override - String get reportDetailAreaIntensity => 'Intensitas per wilayah'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailLocalIntensity => 'Intensitas di lokasi Anda'; + String get mapLayerCategoryEarthquake => 'Gempa'; @override - String get reportDetailLocalIntensityUnavailable => - 'Tidak ada data intensitas'; + String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; @override - String get reportDetailSortByIntensity => 'Urutkan menurut intensitas'; + String get typhoonLegendPast => 'Jalur aktual'; @override - String get reportDetailSortByCounty => 'Urutkan menurut wilayah'; + String get restroomCategoryOther => 'Lainnya'; @override - String get reportDetailImage => 'Gambar laporan'; + String homeForecastHighLow(String high, String low) { + return 'T $high° · R $low°'; + } @override - String get reportDetailImageUnavailable => 'Gambar laporan belum tersedia'; + String get locationBannerFix => 'Buka pengaturan'; @override - String get reportDetailOpenReport => 'Halaman laporan'; + String get mapLegendExpand => 'Legenda'; @override - String get reportDetailReplay => 'Putar ulang'; + String get eewNone => 'Tidak ada peringatan dini gempa aktif'; @override - String get navMore => 'Lainnya'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get appLogs => 'Log aplikasi'; + String get notifyOptTsunamiAll => 'Imbauan dan peringatan tsunami'; @override - String get changelogTitle => 'Catatan pembaruan'; + String get meshtasticLayerOptions => 'Node options'; @override - String get changelogEmpty => 'Belum ada catatan rilis'; + String get onboardingAgreeContinue => 'Setuju dan lanjutkan'; @override - String get changelogTypePrerelease => 'Beta'; + String get commonRetry => 'Coba lagi'; @override - String get changelogTypeStable => 'Stabil'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogCurrentVersion => 'Saat ini'; + String reportDetailNumbered(String number) { + return 'Gempa Dirasakan Signifikan No. $number'; + } @override - String get changelogVersionDetails => 'Detail rilis'; + String get typhoonOverlayStormBandSubtitle => 'With average circle'; @override - String get changelogBodyEmpty => 'Tidak ada catatan untuk rilis ini.'; + String get disasterMapOverlayRestroomTooltip => 'Tampilkan toilet umum'; @override - String get mapPlaceholderDisabled => 'Peta (dinonaktifkan sementara)'; + String get weatherRankingTitle => 'Peringkat observasi'; @override - String get moreSectionRegion => 'Wilayah'; + String get homeRainTrendHeavySustained => + 'Hujan deras berlanjut selama 1 jam ke depan'; @override - String get moreSectionNotify => 'Notifikasi'; + String get notifySectionTsunami => 'Tsunami'; @override - String get moreSectionDisplay => 'Tampilan'; + String get restroomCategoryPark => 'Taman'; @override - String get regionManageTitle => 'Wilayah tersimpan'; + String get moreLinkOpenFailed => 'Tidak dapat membuka tautan'; @override - String get regionAddButton => 'Tambah wilayah'; + String get themeDark => 'Gelap'; @override - String get regionEmpty => 'Belum ada wilayah tersimpan'; + String get sponsorRestore => 'Pulihkan pembelian'; @override - String get regionSelectTitle => 'Pilih wilayah'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String regionSelectCount(int count, int max) { - return '$count/$max dipilih'; - } + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String regionSelectFull(int max) { - return 'Anda dapat menyimpan hingga $max wilayah'; - } + String get meshtasticTraffic => 'Traffic'; @override - String get regionEdit => 'Ubah'; + String get mapLayerStyleBdTooltip => + 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; @override - String get moreSectionAdvanced => 'Lanjutan'; + String get disasterMapOverlayAedTooltip => 'Tampilkan lokasi AED'; @override - String get moreDeveloper => 'Info debug'; + String get mapLayerHumidity => 'Kelembapan'; @override - String get experimentalFeatures => 'Fitur eksperimental'; + String get mapLayerSatelliteTransparentNight => + 'Night = transparent, the basemap shows'; @override - String get moreSectionLinks => 'Tautan'; + String get meshtasticScanning => 'Scanning…'; @override - String get moreCwaEew => 'Peringatan dini gempa CWA'; + String regionSelectFull(int max) { + return 'Anda dapat menyimpan hingga $max wilayah'; + } @override - String get moreTremReport => 'Laporan deteksi TREM'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreServerStatus => 'Status server'; + String get navMore => 'Lainnya'; @override - String get moreAnnouncements => 'Pengumuman'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreDiscord => 'Komunitas Discord'; + String get disasterMapOverlaySectionLayers => 'Lapisan'; @override - String get moreNotifyLog => 'Log notifikasi DPIP'; + String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; @override - String get moreLinkOpenFailed => 'Tidak dapat membuka tautan'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get weatherDynamicState => 'Animasi cuaca'; + String get typhoonLabelNe => 'NE'; @override - String get weatherDynamicStateSubtitle => 'Ganti cuaca latar beranda'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherModeAuto => 'Otomatis'; + String get reportListEmpty => 'Tidak ada laporan gempa'; @override - String get weatherModeClear => 'Cerah'; + String get reportListEnd => 'Akhir daftar'; @override - String get weatherModeRain => 'Hujan'; + String get mapLayerSatelliteTruecolor => 'Himawari True Color'; @override - String get weatherModeFog => 'Kabut'; + String get typhoonOverlaySectionExtra => 'Overlays'; @override - String get weatherModeThunderstorm => 'Badai petir'; + String get eewSWave => 'Gelombang S'; @override - String get commonLoading => 'Memuat…'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get commonRetry => 'Coba lagi'; + String get restroomCategoryCultural => 'Tempat budaya'; @override - String get commonError => 'Terjadi kesalahan'; + String get typhoonLabelWind => 'Max. sustained wind near centre'; @override - String get commonFetchFailed => 'Tidak dapat memuat data. Silakan coba lagi.'; + String get radarGlobalOutlineHint => 'Bingkai luar setiap negara'; @override - String get commonEmpty => 'Tidak ada yang ditampilkan'; + String get notifyEvacuation => 'Informasi bencana'; @override - String get feedConnecting => 'Menghubungkan…'; + String get typhoonLegendCircle15 => 'Lingkar angin kencang'; @override - String get feedStale => 'Data mungkin sudah usang'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedOffline => 'Koneksi terputus'; + String get homeRainTrendLightSustained => + 'Hujan ringan berlanjut selama 1 jam ke depan'; @override - String get eewTitle => 'Peringatan dini gempa'; + String get commonError => 'Terjadi kesalahan'; @override - String get eewNone => 'Tidak ada peringatan dini gempa aktif'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String eewSummary(String magnitude, String depth) { - return 'M$magnitude · kedalaman $depth km'; + String get meshtasticPower => 'Power'; + + @override + String get mapTimelineNow => 'Sekarang'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => 'Seluruh negeri'; + String get reportDetailOpenReport => 'Halaman laporan'; @override - String get regionCurrent => 'Lokasi saat ini'; + String get trendRange7d => '7 hari'; @override - String get regionCurrentUnavailable => - 'Tidak dapat memperoleh lokasi saat ini'; + String typhoonWarningAreas(String areas) { + return 'Wilayah: $areas'; + } @override - String get weatherPrecipitation => 'Curah hujan'; + String get rainIntervalSection => 'Jendela waktu'; @override - String get weatherHumidity => 'Kelembapan'; + String get notifyTitle => 'Notifikasi'; @override - String weatherDataTime(String station, String time) { - return '$station · Waktu data $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => 'Lihat di peta'; + String get restroomCategoryLabel => 'Kategori'; @override - String get homeForecastTitle => 'Prakiraan 24 jam'; + String get sponsorRestoring => 'Memulihkan pembelian…'; @override - String homeForecastHighLow(String high, String low) { - return 'T $high° · R $low°'; - } + String get sponsorIntro => + 'DPIP berdedikasi menyediakan informasi mitigasi bencana secara real-time, tanpa iklan atau model bisnis lainnya. Dukungan Anda membantu kami menjaga server tetap berjalan dan terus mengembangkan aplikasi.'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => 'Alamat'; @override - String homeForecastFeelsLike(String temp) { - return 'Terasa $temp°'; - } + String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; @override - String homeForecastHumidity(String value) { - return 'Kelembapan $value%'; - } + String get restroomCategoryCommercial => 'Tempat komersial'; @override - String homeForecastWind(String direction, String level) { - return '$direction · Skala $level'; - } + String get aedRegion => 'Wilayah'; @override - String get homeForecastUnavailable => 'Pilih wilayah untuk melihat prakiraan'; + String homeRainTrendLightStopping(int minutes) { + return 'Hujan ringan diperkirakan berhenti dalam $minutes menit'; + } @override - String get homeForecastEmpty => 'Tidak ada data prakiraan'; + String get reportDetailInfo => 'Detail'; @override - String get homeActiveEventsTitle => 'Peristiwa aktif'; + String get mapNavWind => 'Angin'; @override - String get homeActiveEventsEmpty => 'Tidak ada peristiwa aktif'; + String get windForecastOverlayMenuTooltip => 'Opsi lapisan prakiraan angin'; @override - String get homeRainTrendTitle => 'Hujan 1 jam ke depan'; + String get dataWeatherRankingSubtitle => 'Peringkat stasiun langsung'; @override String homeRainTrendMinute(int minute) { @@ -521,565 +515,693 @@ class AppLocalizationsId extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return 'Diperbarui $time'; - } + String get rainInterval6h => '6 jam'; @override - String get homeRainTrendNoData => 'Tidak ada data'; + String get restroomTypeUnspecified => 'Tidak ditentukan'; @override - String get homeRainTrendScattered => 'Kemungkinan hujan ringan'; + String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; @override - String get homeRainTrendLightSustained => - 'Hujan ringan berlanjut selama 1 jam ke depan'; + String get mapLayerSatelliteGlobalOutline => 'Country border'; @override - String homeRainTrendLightStopping(int minutes) { - return 'Hujan ringan diperkirakan berhenti dalam $minutes menit'; - } + String get mapNavTemperature => 'Suhu'; @override - String get homeRainTrendHeavySustained => - 'Hujan deras berlanjut selama 1 jam ke depan'; + String get typhoonLegendForecastPoint => 'Titik prakiraan'; @override - String homeRainTrendHeavyStopping(int minutes) { - return 'Hujan deras diperkirakan berhenti dalam $minutes menit'; - } + String get reportListYesterday => 'Kemarin'; @override - String get mapLayers => 'Lapisan'; + String get moreSectionLinks => 'Tautan'; @override - String get mapLayerOrderTitle => 'Urutkan lapisan'; + String get feedOffline => 'Koneksi terputus'; @override - String get mapLayerOrderReset => 'Atur ulang urutan'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => 'Radar Komposit'; + String get moreSectionDisplay => 'Tampilan'; @override - String get mapLayerSatellite => 'Himawari Infrared (B13)'; + String get rainInterval3d => '3 hr'; @override - String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; + String get defaultMapLayerSubtitle => + 'Tab Peta membuka lapisan ini. Ikon dan label navigasi bawah ikut pilihan ini.'; @override - String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; + String get aedDescription => 'Catatan'; @override - String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; + String get typhoonOverlayWeatherRadarTooltip => + 'Radar echo closest to the typhoon bulletin time'; @override - String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + String get onboardingPermLocationDesc => + 'Menargetkan peringatan ke lokasi Anda.'; @override - String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; + String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; @override - String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + String get homeActiveEventsEmpty => 'Tidak ada peristiwa aktif'; @override - String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + String get typhoonLabelPosition => 'Centre location'; @override - String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; + String get weatherRankingBy => 'Urut'; @override - String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; + String get typhoonIntensityMild => 'Mild typhoon'; @override - String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + String get windForecastGlobalOutlineHint => 'Bingkai luar setiap negara'; @override - String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; + String get rainInterval1h => '1 jam'; @override - String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; + String get eewLocalIntensity => 'Perkiraan di lokasi'; @override - String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + String get mapLayerRadar => 'Radar Komposit'; @override - String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + String get restroomCategoryReligious => 'Tempat ibadah'; @override - String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; + String get mapLayerSatelliteCloudCloudy => 'Cloudy'; @override - String get mapLayerSatelliteTruecolor => 'Himawari True Color'; + String get skyTimeSunrise => 'Matahari terbit'; @override - String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => 'Himawari Ash'; + String get onboardingPermNotifyDesc => + 'Menyampaikan peringatan gempa, cuaca, dan bencana pada saat terjadi.'; @override - String get mapLayerSatelliteDust => 'Himawari Dust'; + String get radarTownOutline => 'Batas kecamatan'; @override - String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + String get mapLayerStyleSection => 'Colour style'; @override - String get mapLayerSatelliteNightmicrophysics => - 'Himawari Night Microphysics'; + String get disasterMapOverlayMenuTooltip => 'Lapisan peta bencana'; @override - String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; + String get typhoonLabelSw => 'SW'; @override - String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + String typhoonForecastLead(String hours) { + return 'Forecast +$hours h'; + } @override - String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + String get dpmDisasterTsunami => 'Tsunami'; @override - String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; + String get changelogTypeStable => 'Stabil'; @override - String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; + String get mapLayerSatelliteTransparentClear => + 'Clear sky = transparent, the basemap shows'; @override - String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + String get mapOverlaySectionReference => 'Lapisan referensi'; @override - String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; + String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; @override - String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; + String get reportListLocalFelt => 'Terasa lokal'; @override - String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + String get weatherRankingEmpty => 'Tidak ada observasi untuk diurutkan'; @override - String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + String get notifySectionOther => 'Lainnya'; @override - String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; + String weatherRankingMeta(String time, int count) { + return 'Waktu data: $time\n$count stasiun'; + } @override - String get mapLayerSatelliteGlobalOutline => 'Country border'; + String get onboardingTermsAgree => + 'Saya telah membaca dan menyetujui Ketentuan Layanan'; @override - String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + String get mapLayerSatelliteTransparentNoVegetation => + 'Below 0.1 = transparent (no vegetation)'; @override - String get mapLayerSatelliteCloudClear => 'Clear'; + String get notifyOptLocalIntensity4 => 'Intensitas lokal 4 atau lebih'; @override - String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + String get eewArrived => 'Tiba'; @override - String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => 'Cloudy'; + String get mapLayerCategoryLife => 'Kehidupan sehari-hari'; @override - String get mapLayerSatelliteTransparentWarm => - 'Clear sky (warm end) = transparent, the basemap shows'; + String get reportFilterSortIntensity => 'Intensitas'; @override - String get mapLayerSatelliteTransparentReflectance => - 'Low reflectance / night = transparent, the basemap shows'; + String get typhoonMotion => 'Bergerak'; @override - String get mapLayerSatelliteTransparentZero => - 'Zero difference = transparent (no signal)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => - 'Night = transparent, the basemap shows'; + String get typhoonIntensityIntense => 'Intense typhoon'; @override - String get mapLayerSatelliteTransparentNoData => - 'No data (land) = transparent'; + String get mapLayerOrderTitle => 'Urutkan lapisan'; @override - String get mapLayerSatelliteTransparentNoVegetation => - 'Below 0.1 = transparent (no vegetation)'; + String get dpmYes => 'Ya'; @override - String get mapLayerSatelliteTransparentNoWater => - '≤ 0 = transparent (no water)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => - 'Clear sky = transparent, the basemap shows'; + String get reportDetailLocalIntensityUnavailable => + 'Tidak ada data intensitas'; @override - String get mapLayerStyleSection => 'Colour style'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => 'Colour style'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => 'Grayscale (JMA)'; + String get reportFilterDepth => 'Kedalaman'; @override - String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; + String get onboardingScrollHint => 'Gulir ke bawah untuk melanjutkan'; @override - String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + String get mapNavQpesums => 'Prakiraan'; @override - String get mapLayerStyleJmaTooltip => - 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; + String get navMap => 'Peta'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => 'Imbauan cuaca'; @override - String get mapLayerStyleBdTooltip => - 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; + String get reportFilterReset => 'Reset'; @override - String get mapLayerQpesums => 'Prakiraan hujan 1 jam ke depan'; + String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; @override - String get mapLayerLightning => 'Petir'; + String get typhoonOverlaySectionStorm => 'Storm wind'; @override - String lightningLegendCg(int minutes) { - return 'Awan–tanah · $minutes mnt'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return 'Awan–awan · $minutes mnt'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => 'Sekarang'; + String get weatherDynamicStateSubtitle => 'Ganti cuaca latar beranda'; @override - String get mapTimelinePast => 'Lampau'; + String get reportFilterIntensityInfoModernTitle => 'Baru (sejak 2020)'; @override - String get mapTimelineFuture => 'Mendatang'; + String typhoonDataTime(String time) { + return 'Data time\n$time'; + } @override - String get mapTimelineObserved => 'Diamati'; + String get restroomTypeAccessible => 'Toilet aksesibel'; @override - String get mapTimelineForecast => 'Prakiraan'; + String get moreSectionAbout => 'Tentang'; @override - String mapTimelineDataTime(String time) { - return 'Waktu data $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => 'Pengaturan notifikasi'; + String get onboardingIntroBody => + 'DPIP adalah pendamping pencegahan bencana Anda. DPIP menyatukan peringatan dini gempa, laporan gempa, cuaca, dan informasi bahaya, serta memberi tahu Anda pada saat yang penting.\n\n• Gempa bumi: peringatan dini, laporan intensitas, dan laporan rinci\n• Cuaca: pesan badai petir waktu nyata dan imbauan cuaca\n• Tsunami dan informasi bencana\n\nSelanjutnya, kami akan meminta Anda meninjau Ketentuan Layanan dan memberikan beberapa izin agar DPIP dapat melindungi Anda secara waktu nyata.'; @override - String get notifyTitle => 'Notifikasi'; + String get shelterCapacityLabel => 'Kapasitas'; @override - String get notifyUnavailable => - 'Notifikasi push belum siap — coba lagi sebentar lagi.'; + String get reportDetailImage => 'Gambar laporan'; @override - String get notifySetFailed => - 'Tidak dapat menyimpan pengaturan. Silakan coba lagi.'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => 'Peringatan dini gempa'; + String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; @override - String get notifySectionEarthquake => 'Gempa bumi'; + String get onboardingPermNotify => 'Notifikasi'; @override - String get notifySectionWeather => 'Cuaca'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => 'Tsunami'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => 'Lainnya'; + String get defaultMapLayerSettings => 'Lapisan peta bawaan'; @override - String get notifyEew => 'Peringatan gempa darurat'; + String get moreSectionNotify => 'Notifikasi'; @override - String get notifyMonitor => 'Pemantau getaran kuat'; + String get notifyUnavailable => + 'Notifikasi push belum siap — coba lagi sebentar lagi.'; @override - String get notifyReport => 'Laporan gempa'; + String get mapLayerOrderReset => 'Atur ulang urutan'; @override - String get notifyIntensity => 'Laporan intensitas'; + String get dpmAddress => 'Alamat'; @override - String get notifyThunderstorm => 'Peringatan badai petir'; + String get weatherRankingMergeCounty => 'Kabupaten'; @override - String get notifyAdvisory => 'Imbauan cuaca'; + String get moreSectionApp => 'Dapatkan aplikasi'; @override - String get notifyEvacuation => 'Informasi bencana'; + String get reportFilterIntensityInfoLegacyBody => + 'Hanya tingkat 0–7, tanpa pemisahan 5−/5+/6−/6+.'; @override - String get notifyTsunami => 'Informasi tsunami'; + String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; @override - String get notifyAnnouncement => 'Pengumuman'; + String get qpesumsOverlayMenuTooltip => 'Opsi lapisan prakiraan curah hujan'; @override - String get notifyOptOff => 'Nonaktif'; + String get mapTimelineFuture => 'Mendatang'; @override - String get notifyOptAll => 'Terima semua'; + String get typhoonLegendCircleAvg => 'Average circle'; @override - String get notifyOptLocalIntensity4 => 'Intensitas lokal 4 atau lebih'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => 'Intensitas lokal 1 atau lebih'; + String get typhoonLabelSe => 'SE'; @override - String get notifyOptWeatherLocal => 'Hanya lokasi saat ini'; + String get radarTownOutlineHint => 'Kisi yang lebih rapat'; @override - String get notifyOptTsunamiWarning => 'Hanya peringatan tsunami'; + String eewCountdown(int seconds) { + return '$seconds detik'; + } @override - String get notifyOptTsunamiAll => 'Imbauan dan peringatan tsunami'; + String get typhoonLabelGust => 'Peak gust'; @override - String get onboardingNext => 'Berikutnya'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => 'Kembali'; + String get sponsorTerms => 'Ketentuan Penggunaan'; @override - String get onboardingScrollHint => 'Gulir ke bawah untuk melanjutkan'; + String get restroomTypeGenderNeutral => 'Toilet netral gender'; @override - String get onboardingIntroTitle => 'Selamat datang di DPIP'; + String get notifyThunderstorm => 'Peringatan badai petir'; @override - String get onboardingIntroBody => - 'DPIP adalah pendamping pencegahan bencana Anda. DPIP menyatukan peringatan dini gempa, laporan gempa, cuaca, dan informasi bahaya, serta memberi tahu Anda pada saat yang penting.\n\n• Gempa bumi: peringatan dini, laporan intensitas, dan laporan rinci\n• Cuaca: pesan badai petir waktu nyata dan imbauan cuaca\n• Tsunami dan informasi bencana\n\nSelanjutnya, kami akan meminta Anda meninjau Ketentuan Layanan dan memberikan beberapa izin agar DPIP dapat melindungi Anda secara waktu nyata.'; + String get skyTimeGolden => 'Jam emas'; @override - String get onboardingTermsTitle => 'Ketentuan Layanan'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'Harap baca pemberitahuan berikut sebelum menggunakan DPIP:\n\n• Semua informasi harus mengacu pada konten yang diterbitkan oleh Central Weather Administration (CWA) Taiwan.\n\n• Bergantung pada kondisi jaringan, server, aplikasi, dan sumber data hulu, informasi mungkin tidak diterima; kami berupaya sebaik mungkin untuk menghindari hal ini tetapi tidak dapat menjamin bahwa hal itu tidak akan pernah terjadi.\n\n• Guncangan kuat dapat mencapai lokasi Anda sebelum notifikasi tiba.\n\n• Peringatan dini gempa adalah hasil perhitungan cepat yang mungkin mengandung kesalahan yang signifikan — pahami hal ini dan gunakan dengan hati-hati.\n\n• Setiap tindakan yang tidak disahkan oleh pihak berwenang dapat menimbulkan risiko hukum; harap patuhi semua peraturan yang berlaku.\n\nSelain itu, untuk menyediakan peringatan yang dilokalkan, layanan ini mengumpulkan dan mengunggah perkiraan lokasi Anda dan pengidentifikasi push — di latar depan maupun latar belakang — semata-mata untuk menentukan peringatan mana yang akan dikirimkan kepada Anda.\n\nDengan mengetuk \"Setuju dan lanjutkan\", Anda mengonfirmasi bahwa Anda telah membaca, memahami, dan menyetujui hal-hal di atas.'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => - 'Saya telah membaca dan menyetujui Ketentuan Layanan'; + String weatherRankingAnalysisCurrent(String value) { + return 'Sekarang $value°C'; + } @override - String get onboardingAgreeContinue => 'Setuju dan lanjutkan'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => 'Izin'; + String get homeForecastUnavailable => 'Pilih wilayah untuk melihat prakiraan'; @override - String get onboardingPermsBody => - 'Agar DPIP dapat memperingatkan Anda saat bencana terjadi, harap berikan izin berikut. Anda dapat mengubahnya kapan saja di pengaturan sistem.'; + String get mapLayers => 'Lapisan'; @override - String get onboardingPermNotify => 'Notifikasi'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => - 'Menyampaikan peringatan gempa, cuaca, dan bencana pada saat terjadi.'; + String get languageSettings => 'Bahasa'; @override - String get onboardingPermCritical => 'Peringatan kritis'; + String get dpmDisasterNuclear => 'Kecelakaan nuklir'; @override - String get onboardingPermCriticalDesc => - 'Memungkinkan peringatan gempa yang mengancam jiwa tetap berbunyi bahkan dalam mode senyap atau Jangan Ganggu.'; + String get language => 'Bahasa'; @override - String get onboardingPermLocation => 'Lokasi'; + String homeForecastFeelsLike(String temp) { + return 'Terasa $temp°'; + } @override - String get onboardingPermLocationDesc => - 'Menargetkan peringatan ke lokasi Anda.'; + String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; @override - String get onboardingPermBackground => 'Lokasi latar belakang'; + String get skyTimeDawn => 'Fajar'; @override - String get onboardingPermBackgroundDesc => - 'Izinkan \"Selalu\" agar peringatan tetap menargetkan Anda saat aplikasi ditutup.'; + String get skyTimeAfternoon => 'Sore'; @override - String get onboardingPermBattery => 'Pengecualian baterai'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'Izinkan DPIP terus berjalan di latar belakang agar peringatan tidak tertunda atau terlewat.'; + String get typhoonWarningTitle => 'Peringatan topan'; @override - String get onboardingGrant => 'Berikan'; + String get moreSourceCode => 'Kode sumber'; @override - String get onboardingGranted => 'Diberikan'; + String get mapLayerCategoryWeather => 'Pengamatan cuaca'; @override - String get onboardingStart => 'Mulai'; + String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; @override - String get language => 'Bahasa'; + String get windForecastTownOutlineHint => 'Jaring yang lebih halus'; @override - String get languageSettings => 'Bahasa'; + String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; @override - String get languageSystem => 'Bawaan sistem'; + String get mapAppCopyCoordinates => 'Salin koordinat'; @override - String get locationBannerServiceOff => - 'Layanan lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.'; + String get reportFilterIntensityInfoIntro => + 'CWA mengganti skala intensitas pada 1 Jan 2020 (waktu Taipei).'; @override - String get locationBannerPermission => - 'Izin lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.'; + String get mapNavEarthquake => 'Gempa'; @override - String get locationBannerFix => 'Buka pengaturan'; + String get typhoonGust => 'Embusan'; @override - String get notifyBannerDisabled => - 'Notifikasi mati — Anda tidak akan menerima peringatan bencana.'; + String get restroomGradeAverage => 'Sedang'; @override - String get onboardingSkipTitle => 'Izin belum diberikan'; + String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; @override - String get onboardingSkipBody => - 'Tanpa lokasi dan notifikasi, DPIP tidak dapat memperingatkan Anda tentang gempa dan bencana di sekitar Anda secara waktu nyata. Anda masih dapat memberikannya nanti di Pengaturan.'; + String get onboardingPermBackgroundDesc => + 'Izinkan \"Selalu\" agar peringatan tetap menargetkan Anda saat aplikasi ditutup.'; @override - String get onboardingSkipStay => 'Kembali'; + String get mapTimelineForecast => 'Prakiraan'; @override - String get onboardingSkipLeave => 'Tetap lewati'; + String get restroomTypeLabel => 'Jenis'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => 'Gempa Bumi'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => + 'Level-10 wind field + average circle (yellow)'; @override - String get moreSourceCode => 'Kode sumber'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => 'Dapatkan aplikasi'; + String get reportDetailTitle => 'Laporan Gempa'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'Laporan deteksi TREM'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · Waktu data $time'; + } @override - String get displaySettings => 'Tampilan'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => 'Lapisan peta bawaan'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - 'Tab Peta membuka lapisan ini. Ikon dan label navigasi bawah ikut pilihan ini.'; + String get radarCountyOutline => 'Batas kabupaten/kota'; @override - String get mapNavRadar => 'Radar'; + String get onboardingGranted => 'Diberikan'; @override - String get mapNavQpesums => 'Prakiraan'; + String get commonClose => 'Tutup'; @override - String get mapNavSatellite => 'Satelit'; + String get restroomGradeLabel => 'Nilai'; @override - String get mapNavLightning => 'Petir'; + String get rainIntervalNow => 'Hari ini'; @override - String get mapNavTyphoon => 'Topan'; + String get changelogCurrentVersion => 'Saat ini'; @override - String get mapNavEarthquake => 'Gempa'; + String get typhoonLabelPressure => 'Central pressure'; @override - String get mapNavTemperature => 'Suhu'; + String get typhoonOverlayForecastCalloutsTooltip => + 'Show forecast-point detail cards when zoomed in'; @override - String get mapNavHumidity => 'Kelembapan'; + String get aedOpenRemark => 'Catatan jam buka'; @override - String get mapNavPressure => 'Tekanan'; + String get onboardingPermsBody => + 'Agar DPIP dapat memperingatkan Anda saat bencana terjadi, harap berikan izin berikut. Anda dapat mengubahnya kapan saja di pengaturan sistem.'; @override - String get mapNavWind => 'Angin'; + String get typhoonOverlaySectionWeather => 'Weather underlay'; @override - String get mapNavRain => 'Hujan'; + String get notifyOptWeatherLocal => 'Hanya lokasi saat ini'; @override - String get mapNavDisaster => 'Bencana'; + String get mapNavRain => 'Hujan'; @override - String get displayTheme => 'Tema'; + String get moonDays => 'days'; @override - String get themeSystem => 'Sistem'; + String mapLegendUnit(String unit) { + return 'Satuan: $unit'; + } @override - String get themeLight => 'Terang'; + String get weatherModeClear => 'Cerah'; @override - String get themeDark => 'Gelap'; + String get meshtasticRadio => 'Radio'; @override - String get moreSectionAbout => 'Tentang'; + String get commonEmpty => 'Tidak ada yang ditampilkan'; @override - String get termsOfService => 'Ketentuan Layanan'; + String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; @override - String get faq => 'FAQ'; + String get meshtasticExternalPower => 'External power'; @override - String get openSourceLicenses => 'Lisensi sumber terbuka'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get sponsorTitle => 'Dukung DPIP'; + String get reportFilterOrderAsc => 'Menaik'; @override - String get sponsorIntro => - 'DPIP berdedikasi menyediakan informasi mitigasi bencana secara real-time, tanpa iklan atau model bisnis lainnya. Dukungan Anda membantu kami menjaga server tetap berjalan dan terus mengembangkan aplikasi.'; + String get reportFilterApply => 'Terapkan'; @override - String get sponsorSubscriptions => 'Langganan'; + String get reportDetailImageUnavailable => 'Gambar laporan belum tersedia'; @override - String get sponsorRecommended => 'Direkomendasikan'; + String get weatherRankingHighest => 'Tertinggi'; @override - String get sponsorOneTime => 'Sekali bayar'; + String get reportDetailReplay => 'Putar ulang'; + + @override + String get mapLayerRestroom => 'Toilet Umum'; + + @override + String get restroomCategoryWelfare => 'Lembaga kesejahteraan'; + + @override + String get restroomGradeExcellent => 'Sangat baik'; + + @override + String get meshtasticLastSent => 'Last sent'; + + @override + String get meshtasticName => 'Name'; + + @override + String get meshtasticScan => 'Scan'; + + @override + String get mapLayerCategoryForecast => 'Prakiraan numerik'; + + @override + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; + + @override + String get themeSystem => 'Sistem'; + + @override + String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + + @override + String get typhoonLegendForecast => 'Jalur prakiraan'; + + @override + String typhoonValueHpa(String n) { + return '$n hPa'; + } + + @override + String get weatherPrecipitation => 'Curah hujan'; + + @override + String get moonNextFullMoon => 'Next full moon'; + + @override + String get dpmSheetEmpty => 'Ketuk penanda di peta untuk detail'; + + @override + String get onboardingSkipLeave => 'Tetap lewati'; + + @override + String get onboardingBack => 'Kembali'; + + @override + String get aedPlaceDesc => 'Lokasi peletakan'; + + @override + String get onboardingSkipTitle => 'Izin belum diberikan'; + + @override + String get restroomTypeFamily => 'Toilet keluarga'; + + @override + String typhoonValueKm(String n) { + return '$n km'; + } + + @override + String get typhoonPressure => 'Tekanan'; + + @override + String get onboardingPermBattery => 'Pengecualian baterai'; + + @override + String get typhoonLabelNw => 'NW'; + + @override + String get dpmDisasterFlood => 'Banjir'; + + @override + String get moonPhaseWaxingCrescent => 'Waxing crescent'; + + @override + String get restroomCategoryLeisure => 'Tempat rekreasi'; + + @override + String get mapLayerTemperature => 'Suhu'; + + @override + String get aedCategory => 'Kategori'; + + @override + String get meshtasticChannels => 'Channels'; + + @override + String get monitorWaiting => 'Menunggu data…'; + + @override + String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + + @override + String get reportDetailEpicenter => 'Koordinat episentrum'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => 'Angin'; + + @override + String get reportDetailMagnitude => 'Magnitudo'; + + @override + String get reportDetailAreaIntensity => 'Intensitas per wilayah'; + + @override + String get rainInterval12h => '12 jam'; + + @override + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } + + @override + String get dpmDisasterLandslide => 'Tanah longsor'; + + @override + String get notifyMonitor => 'Pemantau getaran kuat'; + + @override + String get onboardingStart => 'Mulai'; @override String sponsorPerMonth(String price) { @@ -1087,751 +1209,1433 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get sponsorRestore => 'Pulihkan pembelian'; + String get mapLayerPressure => 'Tekanan'; + + @override + String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + + @override + String get mapLayerSatelliteTransparentZero => + 'Zero difference = transparent (no signal)'; + + @override + String get shelterIndoorLabel => 'Penampungan dalam ruangan'; + + @override + String get notifyOptOff => 'Nonaktif'; + + @override + String get reportFilterSortTime => 'Waktu'; + + @override + String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + + @override + String get weatherModeThunderstorm => 'Badai petir'; + + @override + String get homeViewOnMap => 'Lihat di peta'; + + @override + String get reportFilterIntensityInfoLegacyTitle => 'Lama (sebelum 2020)'; + + @override + String get typhoonLabelSpeed => 'Past movement speed'; + + @override + String mapAppOpenFailed(String app) { + return 'Tidak dapat membuka $app'; + } + + @override + String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + + @override + String get meshtasticReceived => 'Received'; + + @override + String get weatherRankingExtremeLow => 'Minimum hari ini'; + + @override + String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + + @override + String get mapLayerSatelliteTransparentNoWater => + '≤ 0 = transparent (no water)'; + + @override + String get shelterCategoryLabel => 'Jenis bencana'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => 'Hembusan'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => 'Jenis bencana tempat berlindung'; + + @override + String get moreServerStatus => 'Status server'; + + @override + String get notifySectionWeather => 'Cuaca'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => 'Seismik'; + + @override + String get changelogBodyEmpty => 'Tidak ada catatan untuk rilis ini.'; + + @override + String get radarGlobalOutline => 'Batas negara'; + + @override + String get notifyEew => 'Peringatan gempa darurat'; + + @override + String get regionNationwide => 'Seluruh negeri'; + + @override + String get moreNotifyLog => 'Log notifikasi DPIP'; + + @override + String get regionCurrent => 'Lokasi saat ini'; + + @override + String get dpmFilterSectionRestroom => 'Jenis tempat'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => 'Salju'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => 'Info debug'; + + @override + String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => 'Petir'; + + @override + String get homeForecastEmpty => 'Tidak ada data prakiraan'; + + @override + String get sponsorOneTime => 'Sekali bayar'; + + @override + String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + + @override + String get onboardingPermBackground => 'Lokasi latar belakang'; + + @override + String get aedEmergencyPhone => 'Telepon darurat'; + + @override + String get dpmOpenInMaps => 'Buka di peta'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + 'Memungkinkan peringatan gempa yang mengancam jiwa tetap berbunyi bahkan dalam mode senyap atau Jangan Ganggu.'; + + @override + String get mapLayerSatelliteTransparentWarm => + 'Clear sky (warm end) = transparent, the basemap shows'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => 'Prakiraan 24 jam'; + + @override + String get typhoonLegendWarningAreas => 'Area peringatan'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; + } + + @override + String get notifyOptLocalIntensity1 => 'Intensitas lokal 1 atau lebih'; + + @override + String get mapTimelinePast => 'Lampau'; + + @override + String get restroomTypeFemale => 'Toilet wanita'; + + @override + String get reportListToday => 'Hari ini'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => 'Memuat…'; + + @override + String get typhoonIntensityModerate => 'Moderate typhoon'; + + @override + String get typhoonWind => 'Angin'; + + @override + String get mapLayerSatelliteAsh => 'Himawari Ash'; + + @override + String get rainInterval3h => '3 jam'; + + @override + String get reportListSearch => 'Cari'; + + @override + String get mapLayerCategorySatellite => 'Satelit'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => 'Lokasi'; + + @override + String get mapLayerSatelliteNightmicrophysics => + 'Himawari Night Microphysics'; + + @override + String get typhoonIntensityTd => 'Tropical depression'; + + @override + String get reportFilterDate => 'Tanggal'; + + @override + String get sponsorRestoreUnavailable => + 'Tidak dapat terhubung ke toko. Coba lagi nanti.'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => 'Belum ada wilayah tersimpan'; + + @override + String get onboardingPermBatteryDesc => + 'Izinkan DPIP terus berjalan di latar belakang agar peringatan tidak tertunda atau terlewat.'; + + @override + String get mapNavDisaster => 'Bencana'; + + @override + String get radarScanRangeSubtitle => + 'Menandai area yang benar-benar dipantau keempat radar.'; + + @override + String get aedHoursSunday => 'Jam Minggu'; + + @override + String get reportDetailOriginTime => 'Waktu kejadian'; + + @override + String get trendNoData => 'Tidak ada data tren'; + + @override + String get onboardingPermLocation => 'Lokasi'; + + @override + String get moreDiscord => 'Komunitas Discord'; + + @override + String get mapNavPressure => 'Tekanan'; + + @override + String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => 'Belum ada catatan rilis'; + + @override + String get reportFilterDateStartNote => 'Hari mulai: dari 00:00(Taipei)'; + + @override + String get eewTitle => 'Peringatan dini gempa'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '$count/$max dipilih'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => 'Mendung'; + + @override + String get reportDetailDepth => 'Kedalaman hiposenter'; + + @override + String get typhoonOverlayWarningTooltip => + 'Highlight counties under a typhoon warning'; + + @override + String get reportFilterDatePick => 'Pilih tanggal'; + + @override + String get onboardingSkipStay => 'Kembali'; + + @override + String get commonFetchFailed => 'Tidak dapat memuat data. Silakan coba lagi.'; + + @override + String get shelterOutdoorLabel => 'Penampungan luar ruangan'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => 'Radar'; + + @override + String get mapLayerSatelliteCloudClear => 'Clear'; + + @override + String eewSummary(String magnitude, String depth) { + return 'M$magnitude · kedalaman $depth km'; + } + + @override + String get locationBannerPermission => + 'Izin lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.'; + + @override + String get typhoonOverlayWeatherNoneTooltip => + 'No radar or infrared underlay'; + + @override + String get radarCountyOutlineHint => 'Digambar di atas gema'; + + @override + String get windForecastCountyOutlineHint => 'Digambar di atas bidang angin'; + + @override + String get homeRainTrendTitle => 'Hujan 1 jam ke depan'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => 'Topan'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => 'Toilet campuran'; + + @override + String get restroomGradeGood => 'Baik'; + + @override + String get notifyTsunami => 'Informasi tsunami'; + + @override + String get navData => 'Data'; + + @override + String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => + 'Perangkat ini tidak dapat melakukan panggilan telepon'; + + @override + String get reportFilterAny => 'Semua'; + + @override + String get weatherRankingMergeTo => 'Gabung'; + + @override + String get notifyIntensity => 'Laporan intensitas'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => 'Jendela akumulasi'; + + @override + String get reportDetailLocalFelt => 'Gempa Dirasakan Lokal'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => 'Berikan'; + + @override + String get weatherModeRain => 'Hujan'; + + @override + String get shelterVulnerableOkLabel => 'Ramah kelompok rentan'; + + @override + String get stationSheetEmpty => 'Ketuk stasiun untuk melihat bacaannya'; + + @override + String get typhoonLegendProbability => 'Probabilitas serangan'; + + @override + String get reportFilterMagnitude => 'Magnitudo'; + + @override + String get skyTimeMorning => 'Pagi'; + + @override + String get experimentalFeatures => 'Fitur eksperimental'; + + @override + String get onboardingTermsBody => + 'Harap baca pemberitahuan berikut sebelum menggunakan DPIP:\n\n• Semua informasi harus mengacu pada konten yang diterbitkan oleh Central Weather Administration (CWA) Taiwan.\n\n• Bergantung pada kondisi jaringan, server, aplikasi, dan sumber data hulu, informasi mungkin tidak diterima; kami berupaya sebaik mungkin untuk menghindari hal ini tetapi tidak dapat menjamin bahwa hal itu tidak akan pernah terjadi.\n\n• Guncangan kuat dapat mencapai lokasi Anda sebelum notifikasi tiba.\n\n• Peringatan dini gempa adalah hasil perhitungan cepat yang mungkin mengandung kesalahan yang signifikan — pahami hal ini dan gunakan dengan hati-hati.\n\n• Setiap tindakan yang tidak disahkan oleh pihak berwenang dapat menimbulkan risiko hukum; harap patuhi semua peraturan yang berlaku.\n\nSelain itu, untuk menyediakan peringatan yang dilokalkan, layanan ini mengumpulkan dan mengunggah perkiraan lokasi Anda dan pengidentifikasi push — di latar depan maupun latar belakang — semata-mata untuk menentukan peringatan mana yang akan dikirimkan kepada Anda.\n\nDengan mengetuk \"Setuju dan lanjutkan\", Anda mengonfirmasi bahwa Anda telah membaca, memahami, dan menyetujui hal-hal di atas.'; + + @override + String get reportFilterTitle => 'Filter'; + + @override + String get onboardingPermCritical => 'Peringatan kritis'; + + @override + String trendCumulativeTotal(String total) { + return 'Total $total mm'; + } + + @override + String get languageName => 'Bahasa Indonesia'; + + @override + String get reportListEmptyFiltered => + 'Tidak ada laporan yang cocok dengan filter'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => 'Topan'; + + @override + String get weatherModeSand => 'Debu'; + + @override + String get typhoonSatelliteTitle => 'Satelit'; + + @override + String get notifyReport => 'Laporan gempa'; + + @override + String get mapAppCoordinatesCopied => 'Koordinat disalin'; + + @override + String get skyTimeNight => 'Malam'; + + @override + String get sponsorRecommended => 'Direkomendasikan'; + + @override + String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + + @override + String get weatherRankingWind => 'Kecepatan angin'; + + @override + String get feedStale => 'Data mungkin sudah usang'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · Skala $level'; + } + + @override + String get navHome => 'Beranda'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => 'Lisensi sumber terbuka'; + + @override + String get weatherRankingLowest => 'Terendah'; + + @override + String get reportFilterSortDepth => 'Kedalaman'; + + @override + String mapTimelineDataTime(String time) { + return 'Waktu data $time'; + } + + @override + String get radarScanRange => 'Tampilkan jangkauan pindai'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return 'Rentang $value°C'; + } + + @override + String get weatherRankingExtremeHigh => 'Maksimum hari ini'; + + @override + String get changelogVersionDetails => 'Detail rilis'; + + @override + String get sponsorPrivacy => 'Kebijakan Privasi'; + + @override + String get reportDetailLocalIntensity => 'Intensitas di lokasi Anda'; + + @override + String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; + + @override + String shelterCapacityValue(int n) { + return '$n orang'; + } + + @override + String lightningLegendCc(int minutes) { + return 'Awan–awan · $minutes mnt'; + } @override - String get sponsorTerms => 'Ketentuan Penggunaan'; + String get meshtasticSendHint => 'Message to broadcast'; @override - String get sponsorPrivacy => 'Kebijakan Privasi'; + String monitorDelay(String value) { + return 'Latensi $value s'; + } @override - String get sponsorRestoring => 'Memulihkan pembelian…'; + String get dpmNo => 'Tidak'; @override - String get sponsorRestoreUnavailable => - 'Tidak dapat terhubung ke toko. Coba lagi nanti.'; + String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; @override - String get commonClose => 'Tutup'; + String get meshtasticReconnecting => 'Reconnecting…'; @override - String get mapLayerTemperature => 'Suhu'; + String get radarTownOutlineSubtitle => + 'Menjaga batas kecamatan tetap terbaca di bawah gema radar.'; @override - String get trendRange24h => '24 jam'; + String get typhoonOverlayWeatherSatelliteTooltip => + 'Infrared closest to the typhoon bulletin time'; @override - String get trendRange7d => '7 hari'; + String get radarScanRangeHint => 'Di luar kotak berarti tak terpantau'; @override - String get trendNoData => 'Tidak ada data tren'; + String typhoonPickerTd(String no) { + return 'Tropical depression TD $no'; + } @override - String trendCumulativeTotal(String total) { - return 'Total $total mm'; + String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + + @override + String get regionAddButton => 'Tambah wilayah'; + + @override + String get displaySettings => 'Tampilan'; + + @override + String get restroomGradePoor => 'Di bawah standar'; + + @override + String get restroomCategoryTourist => 'Kawasan wisata'; + + @override + String get locationBannerServiceOff => + 'Layanan lokasi mati — peringatan lokal tidak dapat menargetkan wilayah Anda.'; + + @override + String get mapLayerStyleTooltip => 'Colour style'; + + @override + String lightningLegendCg(int minutes) { + return 'Awan–tanah · $minutes mnt'; } @override - String chartHourLabel(int hour) { - return '${hour}j'; + String get skyTimeAuto => 'Otomatis'; + + @override + String get appLogs => 'Log aplikasi'; + + @override + String get feedConnecting => 'Menghubungkan…'; + + @override + String get notifyBannerDisabled => + 'Notifikasi mati — Anda tidak akan menerima peringatan bencana.'; + + @override + String get weatherHumidity => 'Kelembapan'; + + @override + String typhoonValueMs(String n) { + return '$n m/s'; } @override - String get mapLayerHumidity => 'Kelembapan'; + String homeForecastHumidity(String value) { + return 'Kelembapan $value%'; + } @override - String get mapLayerPressure => 'Tekanan'; + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; @override - String get mapLayerWind => 'Angin'; + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; @override - String get mapLayerRain => 'Curah hujan'; + String get restroomCategoryTransport => 'Transportasi'; @override - String get rainIntervalMenu => 'Jendela akumulasi'; + String get reportFilterLocationHint => 'mis. Hualien, lepas pantai'; @override - String get rainIntervalNow => 'Hari ini'; + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; @override - String get rainInterval10m => '10 mnt'; + String get meshtasticBattery => 'Battery'; @override - String get rainInterval1h => '1 jam'; + String get meshtasticDistance => 'Jarak'; @override - String get rainInterval3h => '3 jam'; + String get meshtasticSnrTrend => 'Tren sinyal (SNR)'; @override - String get rainInterval6h => '6 jam'; + String get meshtasticBatteryTrend => 'Tren baterai'; @override - String get rainInterval12h => '12 jam'; + String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; @override - String get rainInterval24h => '24 jam'; + String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; @override - String get rainInterval2d => '2 hr'; + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } @override - String get rainInterval3d => '3 hr'; + String get notifySectionEarthquake => 'Gempa bumi'; @override - String get mapLayerTyphoon => 'Topan'; + String get mapLayerDisasterMap => 'Peta Bencana'; @override - String get typhoonNoActive => 'Tidak ada topan aktif'; + String get weatherModeFog => 'Kabut'; @override - String get typhoonWind => 'Angin'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } @override - String get typhoonGust => 'Embusan'; + String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; @override - String get typhoonPressure => 'Tekanan'; + String get moreAnnouncements => 'Pengumuman'; @override - String get typhoonMotion => 'Bergerak'; + String get mapLayerSatelliteTransparentNoData => + 'No data (land) = transparent'; @override - String get typhoonLabelPosition => 'Centre location'; + String get restroomCategoryGovernment => 'Kantor pelayanan publik'; @override - String get typhoonLabelDirection => 'Past movement direction'; + String get typhoonLegendCurrent => 'Pusat saat ini'; @override - String get typhoonLabelSpeed => 'Past movement speed'; + String get aedAddress => 'Alamat'; @override - String get typhoonLabelPressure => 'Central pressure'; + String get mapLayerAed => 'AED'; @override - String get typhoonLabelWind => 'Max. sustained wind near centre'; + String get changelogTypePrerelease => 'Beta'; @override - String get typhoonLabelGust => 'Peak gust'; + String get reportFilterIntensityInfoModernBody => + 'Tingkat 0–4, 5−, 5+, 6−, 6+, 7. Slider filter memakai skala baru; peristiwa lama tetap memakai label lama di daftar.'; @override - String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; + String get typhoonOverlayWeatherNone => 'None'; @override - String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; + String get mapLayerStyleGray => 'Grayscale (JMA)'; + + @override + String get weatherModeAuto => 'Otomatis'; @override String get typhoonLabelProbCircle => '70% probability circle'; @override - String typhoonForecastLead(String hours) { - return 'Forecast +$hours h'; + String get notifyOptAll => 'Terima semua'; + + @override + String get displayTheme => 'Tema'; + + @override + String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + + @override + String get typhoonLabelDirection => 'Past movement direction'; + + @override + String get regionManageTitle => 'Wilayah tersimpan'; + + @override + String get typhoonLegendCone => 'Kerucut prakiraan'; + + @override + String get moreCwaEew => 'Peringatan dini gempa CWA'; + + @override + String get onboardingPermsTitle => 'Izin'; + + @override + String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + + @override + String get rainInterval10m => '10 mnt'; + + @override + String weatherRankingAnalysisLow(String value) { + return 'Min $value'; } @override - String get typhoonLabelNw => 'NW'; + String get meshtasticConnectAnyway => 'Connect anyway'; @override - String get typhoonLabelNe => 'NE'; + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => + 'Low reflectance / night = transparent, the basemap shows'; + + @override + String chartHourLabel(int hour) { + return '${hour}j'; + } + + @override + String get mapLayerShelter => 'Tempat Evakuasi'; + + @override + String get typhoonOverlayProbabilityTooltip => + 'Show strike probability (hides the forecast cone)'; + + @override + String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => 'Tampilkan tempat evakuasi'; + + @override + String get mapNavHumidity => 'Kelembapan'; + + @override + String get reportDetailSortByIntensity => 'Urutkan menurut intensitas'; + + @override + String get homeRainTrendNoData => 'Tidak ada data'; + + @override + String get mapLayerCategoryRadar => 'Radar'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + + @override + String get typhoonTrackDetail => 'Detail jalur'; + + @override + String get dataSectionWeather => 'Cuaca'; + + @override + String get aedHoursWeekday => 'Jam hari kerja'; + + @override + String get homeActiveEventsTitle => 'Peristiwa aktif'; + + @override + String weatherRankingAnalysisHigh(String value) { + return 'Maks $value'; + } + + @override + String get faq => 'FAQ'; + + @override + String get typhoonHistoryLive => 'Langsung'; + + @override + String eewSerial(int serial) { + return 'Laporan $serial'; + } + + @override + String get reportFilterSort => 'Urutan'; + + @override + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; + + @override + String get dataEarthquakeSubtitle => 'Laporan gempa'; + + @override + String get typhoonNoActive => 'Tidak ada topan aktif'; @override - String get typhoonLabelSw => 'SW'; + String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; @override - String get typhoonLabelSe => 'SE'; + String get navEvents => 'Kejadian'; @override - String typhoonValueLat(String lat) { - return '$lat°N'; - } + String get onboardingTermsTitle => 'Ketentuan Layanan'; @override - String typhoonValueLon(String lon) { - return '$lon°E'; - } + String get mapTownLabels => 'Nama kecamatan'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => + 'Tidak dapat menyimpan pengaturan. Silakan coba lagi.'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '$n m/s'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return 'Data time\n$time'; - } + String get notifyAnnouncement => 'Pengumuman'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'Selamat datang di DPIP'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => + 'Tidak dapat memperoleh lokasi saat ini'; @override - String get mapLayerMonitor => 'Monitor Seismik'; + String get languageSystem => 'Bawaan sistem'; @override - String get mapLayerDisasterMap => 'Peta Bencana'; + String get skyTimeSunset => 'Matahari terbenam'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'Himawari Dust'; @override - String get disasterMapOverlayMenuTooltip => 'Lapisan peta bencana'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => 'Lapisan'; + String get regionEdit => 'Ubah'; @override - String get disasterMapOverlayAedTooltip => 'Tampilkan lokasi AED'; + String get weatherDynamicState => 'Animasi cuaca'; @override - String get aedAddress => 'Alamat'; + String get mapPlaceholderDisabled => 'Peta (dinonaktifkan sementara)'; @override - String get aedRegion => 'Wilayah'; + String get moonNow => 'Sekarang'; @override - String get aedCategory => 'Kategori'; + String get moonSectionAppearance => 'Penampakan'; @override - String get aedType => 'Jenis'; + String get moonSectionRiseSet => 'Terbit dan terbenam'; @override - String get aedPlaceDesc => 'Lokasi peletakan'; + String get moonSectionUpcoming => 'Mendatang'; @override - String get aedDescription => 'Catatan'; + String get moonSectionCalendar => 'Kalender'; @override - String get aedHoursWeekday => 'Jam hari kerja'; + String get moonDistance => 'Jarak'; @override - String get aedHoursSaturday => 'Jam Sabtu'; + String get moonKilometres => 'km'; @override - String get aedHoursSunday => 'Jam Minggu'; + String get moonApparentSize => 'Ukuran tampak'; @override - String get aedOpenRemark => 'Catatan jam buka'; + String get moonRise => 'Bulan terbit'; @override - String get aedEmergencyPhone => 'Telepon darurat'; + String get moonSet => 'Bulan terbenam'; @override - String get mapLayerRestroom => 'Toilet Umum'; + String get moonNextNewMoon => 'Bulan baru berikutnya'; @override - String get mapLayerShelter => 'Tempat Evakuasi'; + String get moonAlwaysUp => 'Di atas ufuk sepanjang hari'; @override - String get disasterMapOverlayRestroomTooltip => 'Tampilkan toilet umum'; + String get moonNoEvent => 'Tidak ada hari ini'; @override - String get disasterMapOverlayShelterTooltip => 'Tampilkan tempat evakuasi'; + String get sunTitle => 'Matahari'; @override - String get dpmOpenInMaps => 'Buka di peta'; + String get sunSubtitle => 'Matahari terbit, senja, dan istilah surya'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => 'Cahaya siang'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => 'Senja'; @override - String mapAppDefault(String app) { - return '$app (bawaan)'; - } + String get sunSectionLight => 'Cahaya'; @override - String get mapAppCopyCoordinates => 'Salin koordinat'; + String get sunSectionSundial => 'Jam matahari'; @override - String get mapAppCoordinatesCopied => 'Koordinat disalin'; + String get sunSectionTerms => 'Istilah surya'; @override - String mapAppOpenFailed(String app) { - return 'Tidak dapat membuka $app'; - } + String get sunRise => 'Matahari terbit'; @override - String get mapAppCallFailed => - 'Perangkat ini tidak dapat melakukan panggilan telepon'; + String get sunSet => 'Matahari terbenam'; @override - String get mapOverlaySectionReference => 'Lapisan referensi'; + String get sunNoon => 'Tengah hari surya'; @override - String get mapLayerCategoryEarthquake => 'Gempa'; + String get sunDayLength => 'Panjang hari'; @override - String get mapLayerCategoryTyphoon => 'Topan'; + String get sunTwilightCivil => 'Sipil'; @override - String get mapLayerCategoryWeather => 'Pengamatan cuaca'; + String get sunTwilightNautical => 'Nautika'; @override - String get mapLayerCategorySatellite => 'Satelit'; + String get sunTwilightAstronomical => 'Astronomi'; @override - String get mapLayerCategoryRadar => 'Radar'; + String get sunGoldenHourMorning => 'Golden hour pagi'; @override - String get mapLayerCategoryLife => 'Kehidupan sehari-hari'; + String get sunGoldenHourEvening => 'Golden hour sore'; @override - String get mapLayerCategoryForecast => 'Prakiraan numerik'; + String get sunBlueHour => 'Blue hour'; @override - String get mapOverlaySectionMap => 'Peta'; + String get sunEquationOfTime => 'Persamaan waktu'; @override - String get rainIntervalSection => 'Jendela waktu'; + String get sunMinutes => 'mnt'; @override - String get mapTownLabels => 'Nama kecamatan'; + String get solarTermNext => 'Istilah berikutnya'; @override - String get mapTownLabelsHint => 'Tampilkan nama kecamatan saat diperbesar'; + String get planetsTitle => 'Planet'; @override - String get mapTerrainRelief => 'Relief terrain'; + String get planetsSubtitle => 'Di mana malam ini, dan seberapa terang'; @override - String get mapTerrainReliefHint => 'Tampilkan relief terrain di peta dasar'; + String get planetsSectionTonight => 'Saat ini'; @override - String get dpmSheetEmpty => 'Ketuk penanda di peta untuk detail'; + String get planetUp => 'Di atas ufuk'; @override - String get dpmAddress => 'Alamat'; + String get planetDown => 'Di bawah ufuk'; @override - String get restroomTypeLabel => 'Jenis'; + String get planetInGlare => 'Terlalu dekat Matahari'; @override - String get restroomCategoryLabel => 'Kategori'; + String get planetMagnitude => 'Magnitudo'; @override - String get restroomGradeLabel => 'Nilai'; + String get planetElongation => 'Elongasi'; @override - String get restroomTypeFemale => 'Toilet wanita'; + String get planetSky => 'Waktu'; @override - String get restroomTypeMale => 'Toilet pria'; + String get planetEvening => 'Petang'; @override - String get restroomTypeMixed => 'Toilet campuran'; + String get planetMorning => 'Pagi'; @override - String get restroomTypeAccessible => 'Toilet aksesibel'; + String get planetDistance => 'Jarak'; @override - String get restroomTypeGenderNeutral => 'Toilet netral gender'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => 'Toilet keluarga'; + String get planetAltitude => 'Ketinggian'; @override - String get restroomTypeUnspecified => 'Tidak ditentukan'; + String get planetMercury => 'Merkurius'; @override - String get restroomCategoryTransport => 'Transportasi'; + String get planetVenus => 'Venus'; @override - String get restroomCategoryPark => 'Taman'; + String get planetMars => 'Mars'; @override - String get restroomCategoryCommercial => 'Tempat komersial'; + String get planetJupiter => 'Jupiter'; @override - String get restroomCategoryReligious => 'Tempat ibadah'; + String get planetSaturn => 'Saturnus'; @override - String get restroomCategoryCultural => 'Tempat budaya'; + String get planetUranus => 'Uranus'; @override - String get restroomCategoryGovernment => 'Kantor pelayanan publik'; + String get planetNeptune => 'Neptunus'; @override - String get restroomCategoryWelfare => 'Lembaga kesejahteraan'; + String get solarTermVernalEquinox => 'Ekuinoks Musim Semi'; @override - String get restroomCategoryTourist => 'Kawasan wisata'; + String get solarTermPureBrightness => 'Pure Brightness'; @override - String get restroomCategoryLeisure => 'Tempat rekreasi'; + String get solarTermGrainRain => 'Grain Rain'; @override - String get restroomCategoryOther => 'Lainnya'; + String get solarTermStartOfSummer => 'Awal Musim Panas'; @override - String get restroomGradeExcellent => 'Sangat baik'; + String get solarTermGrainFull => 'Grain Full'; @override - String get restroomGradeGood => 'Baik'; + String get solarTermGrainInEar => 'Grain in Ear'; @override - String get restroomGradeAverage => 'Sedang'; + String get solarTermSummerSolstice => 'Solstis Musim Panas'; @override - String get restroomGradePoor => 'Di bawah standar'; + String get solarTermMinorHeat => 'Minor Heat'; @override - String get shelterAddressLabel => 'Alamat'; + String get solarTermMajorHeat => 'Major Heat'; @override - String get shelterCapacityLabel => 'Kapasitas'; + String get solarTermStartOfAutumn => 'Awal Musim Gugur'; @override - String shelterCapacityValue(int n) { - return '$n orang'; - } + String get solarTermEndOfHeat => 'End of Heat'; @override - String get shelterCategoryLabel => 'Jenis bencana'; + String get solarTermWhiteDew => 'White Dew'; @override - String get shelterIndoorLabel => 'Penampungan dalam ruangan'; + String get solarTermAutumnalEquinox => 'Ekuinoks Musim Gugur'; @override - String get shelterOutdoorLabel => 'Penampungan luar ruangan'; + String get solarTermColdDew => 'Cold Dew'; @override - String get shelterVulnerableOkLabel => 'Ramah kelompok rentan'; + String get solarTermFrostDescent => 'Frost Descent'; @override - String get dpmYes => 'Ya'; + String get solarTermStartOfWinter => 'Awal Musim Dingin'; @override - String get dpmNo => 'Tidak'; + String get solarTermMinorSnow => 'Minor Snow'; @override - String get stationSheetEmpty => 'Ketuk stasiun untuk melihat bacaannya'; + String get solarTermMajorSnow => 'Major Snow'; @override - String monitorDelay(String value) { - return 'Latensi $value s'; - } + String get solarTermWinterSolstice => 'Solstis Musim Dingin'; @override - String get monitorWaiting => 'Menunggu data…'; + String get solarTermMinorCold => 'Minor Cold'; @override - String mapLegendUnit(String unit) { - return 'Satuan: $unit'; - } + String get solarTermMajorCold => 'Major Cold'; @override - String get typhoonLegendPast => 'Jalur aktual'; + String get solarTermStartOfSpring => 'Awal Musim Semi'; @override - String get typhoonIntensityTd => 'Tropical depression'; + String get solarTermRainWater => 'Rain Water'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => 'Awakening of Insects'; @override - String typhoonPickerTd(String no) { - return 'Tropical depression TD $no'; - } + String get tonightTitle => 'Malam ini'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => 'Apa yang terlihat, dan kapan'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => 'Jendela pengamatan'; @override - String get typhoonIntensityMild => 'Mild typhoon'; + String get tonightAstronomicalNight => 'Malam astronomis'; @override - String get typhoonIntensityModerate => 'Moderate typhoon'; + String get tonightNeverDark => 'Tak pernah gelap total'; @override - String get typhoonIntensityIntense => 'Intense typhoon'; + String get tonightDarkWindow => 'Jendela gelap'; @override - String get typhoonLegendForecast => 'Jalur prakiraan'; + String get tonightMoonAllNight => 'Bulan terbit sepanjang malam'; @override - String get typhoonLegendForecastPoint => 'Titik prakiraan'; + String get tonightDarkTotal => 'Total gelap'; @override - String get typhoonLegendCurrent => 'Pusat saat ini'; + String get tonightMoonlight => 'Cahaya bulan'; @override - String get typhoonLegendCone => 'Kerucut prakiraan'; + String get tonightSectionShowers => 'Hujan meteor'; @override - String get mapLegendExpand => 'Legenda'; + String get tonightRadiantDown => 'Radian tidak terbit'; @override - String get mapLegendCollapse => 'Sembunyikan legenda'; + String get tonightPerHour => '/jam'; @override - String get mapMyLocation => 'Lokasi saya'; + String get tonightSectionSatellites => 'Lintasan satelit'; @override - String get mapResetNorth => 'Kembali ke utara'; + String get tonightSectionTargets => 'Sasaran yang terlihat'; @override - String get typhoonLegendCircle15 => 'Lingkar angin kencang'; + String get showerQuadrantids => 'Quadrantids'; @override - String get typhoonLegendCircleAvg => 'Average circle'; + String get showerLyrids => 'Lyrids'; @override - String get typhoonLegendCircle25 => 'Lingkar badai'; + String get showerEtaAquariids => 'Eta Aquariids'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => 'Delta Aquariids'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => 'Perseids'; @override - String get typhoonLegendProbability => 'Probabilitas serangan'; + String get showerOrionids => 'Orionids'; @override - String get typhoonLegendWarningAreas => 'Area peringatan'; + String get showerSouthernTaurids => 'Taurids Selatan'; @override - String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + String get showerLeonids => 'Leonids'; @override - String get typhoonOverlaySectionStorm => 'Storm wind'; + String get showerGeminids => 'Geminids'; @override - String get typhoonOverlaySectionExtra => 'Overlays'; + String get showerUrsids => 'Ursids'; @override - String get typhoonOverlayStormBandSubtitle => 'With average circle'; + String get deepSkyOpenCluster => 'Gugus terbuka'; @override - String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; + String get deepSkyGlobularCluster => 'Gugus bola'; @override - String get typhoonOverlayProbabilityTooltip => - 'Show strike probability (hides the forecast cone)'; + String get deepSkySpiralGalaxy => 'Galaksi spiral'; @override - String get typhoonOverlayWarningTooltip => - 'Highlight counties under a typhoon warning'; + String get deepSkyEllipticalGalaxy => 'Galaksi elips'; @override - String get typhoonOverlayStormL7Tooltip => - 'Level-7 wind field + average circle (purple)'; + String get deepSkyIrregularGalaxy => 'Galaksi tak beraturan'; @override - String get typhoonOverlayStormL10Tooltip => - 'Level-10 wind field + average circle (yellow)'; + String get deepSkyPlanetaryNebula => 'Nebula planeter'; @override - String get typhoonOverlaySectionWeather => 'Weather underlay'; + String get deepSkySupernovaRemnant => 'Sisa supernova'; @override - String get typhoonOverlayWeatherNone => 'None'; + String get deepSkyEmissionNebula => 'Nebula emisi'; @override - String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; + String get deepSkyReflectionNebula => 'Nebula refleksi'; @override - String get typhoonOverlayWeatherNoneTooltip => - 'No radar or infrared underlay'; + String get deepSkyAsterism => 'Asterisme'; @override - String get typhoonOverlayWeatherRadarTooltip => - 'Radar echo closest to the typhoon bulletin time'; + String get almanacTitle => 'Almanak'; @override - String get typhoonOverlayWeatherSatelliteTooltip => - 'Infrared closest to the typhoon bulletin time'; + String get almanacSubtitle => 'Tanggal lunisolar dan gerhana mendatang'; @override - String get typhoonWarningTitle => 'Peringatan topan'; + String get almanacSectionToday => 'Hari ini'; @override - String typhoonWarningAreas(String areas) { - return 'Wilayah: $areas'; - } + String get almanacGregorian => 'Masehi'; @override - String get typhoonTrackDetail => 'Detail jalur'; + String get almanacLunar => 'Lunisolar'; @override - String get typhoonHistoryTitle => 'Waktu data'; + String get almanacYear => 'Tahun'; @override - String get typhoonHistoryLive => 'Langsung'; + String get almanacMonthLength => 'Panjang bulan'; @override - String get typhoonSatelliteTitle => 'Satelit'; + String get almanacLongMonth => '30 hari'; @override - String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + String get almanacShortMonth => '29 hari'; @override - String get typhoonOverlayForecastCalloutsTooltip => - 'Show forecast-point detail cards when zoomed in'; + String get almanacLeapPrefix => 'Kabisat '; @override - String get dpmFilterSectionRestroom => 'Jenis tempat'; + String get almanacSectionLunarEclipses => 'Gerhana bulan'; @override - String get dpmFilterSectionRestroomType => 'Jenis toilet'; + String get almanacSectionSolarEclipses => 'Gerhana matahari'; @override - String get dpmFilterSectionShelter => 'Jenis bencana tempat berlindung'; + String get almanacNoSolarEclipse => 'Tidak ada'; @override - String get dpmDisasterFlood => 'Banjir'; + String get eclipseTotal => 'Total'; @override - String get dpmDisasterEarthquake => 'Gempa'; + String get eclipsePartial => 'Sebagian'; @override - String get dpmDisasterLandslide => 'Tanah longsor'; + String get eclipseAnnular => 'Cincin'; @override - String get dpmDisasterTsunami => 'Tsunami'; + String get eclipsePenumbral => 'Penumbra'; @override - String get dpmDisasterSlope => 'Bencana lereng'; + String get zodiacRat => 'Tikus'; @override - String get dpmDisasterNuclear => 'Kecelakaan nuklir'; + String get zodiacOx => 'Kerbau'; @override - String get skyTime => 'Waktu langit'; + String get zodiacTiger => 'Macan'; @override - String get skyTimeAuto => 'Otomatis'; + String get zodiacRabbit => 'Kelinci'; @override - String get skyTimeDawn => 'Fajar'; + String get zodiacDragon => 'Naga'; @override - String get skyTimeSunrise => 'Matahari terbit'; + String get zodiacSnake => 'Ular'; @override - String get skyTimeMorning => 'Pagi'; + String get zodiacHorse => 'Kuda'; @override - String get skyTimeNoon => 'Siang'; + String get zodiacGoat => 'Kambing'; @override - String get skyTimeAfternoon => 'Sore'; + String get zodiacMonkey => 'Monyet'; @override - String get skyTimeGolden => 'Jam emas'; + String get zodiacRooster => 'Ayam'; @override - String get skyTimeSunset => 'Matahari terbenam'; + String get zodiacDog => 'Anjing'; @override - String get skyTimeDusk => 'Senja'; + String get zodiacPig => 'Babi'; @override - String get skyTimeNight => 'Malam'; + String get tideTitle => 'Pasang surut'; @override - String get weatherModeCloudy => 'Berawan'; + String get tideSubtitle => 'Purnama, perbani, dan tarikan Bulan'; @override - String get weatherModeOvercast => 'Mendung'; + String get tideDisclaimer => + 'Hanya gaya astronomis — bukan tabel pasang surut pelabuhan. Untuk tinggi muka air gunakan tabel CWA.'; @override - String get weatherModeSnow => 'Salju'; + String get tideSectionNow => 'Saat ini'; @override - String get weatherModeSand => 'Debu'; + String get tidePhase => 'Siklus'; @override - String get radarScanRange => 'Tampilkan jangkauan pindai'; + String get tideSpring => 'Purnama'; @override - String get radarScanRangeSubtitle => - 'Menandai area yang benar-benar dipantau keempat radar.'; + String get tideNeap => 'Perbani'; @override - String get radarScanRangeHint => 'Di luar kotak berarti tak terpantau'; + String get tideMiddling => 'Sedang'; @override - String get radarOverlayMenuTooltip => 'Opsi lapisan radar'; + String get tideLunarDistanceFactor => 'Tarikan Bulan'; @override - String get radarCountyOutline => 'Batas kabupaten/kota'; + String get tideEquilibrium => 'Pasang setimbang'; @override - String get radarGlobalOutline => 'Batas negara'; + String get tideMetres => 'm'; @override - String get radarGlobalOutlineHint => 'Bingkai luar setiap negara'; + String get tidePerigeanSpring => 'Purnama perigee berikutnya'; @override - String get radarCountyOutlineHint => 'Digambar di atas gema'; + String get tideSectionTurningPoints => 'Titik balik'; @override - String get radarCountyOutlineSubtitle => - 'Menjaga batas wilayah tetap terbaca di bawah gema radar.'; + String get tideHigh => 'Tinggi'; @override - String get radarTownOutline => 'Batas kecamatan'; + String get tideLow => 'Rendah'; @override - String get radarTownOutlineHint => 'Kisi yang lebih rapat'; + String get skyChartTitle => 'Peta langit'; @override - String get radarTownOutlineSubtitle => - 'Menjaga batas kecamatan tetap terbaca di bawah gema radar.'; + String get skyChartSubtitle => 'Langit yang terlihat mata telanjang'; @override - String get qpesumsOverlayMenuTooltip => 'Opsi lapisan prakiraan curah hujan'; + String get skyChartNorth => 'U'; @override - String get windForecastOverlayMenuTooltip => 'Opsi lapisan prakiraan angin'; + String get skyChartEast => 'T'; @override - String get windForecastCountyOutlineHint => 'Digambar di atas bidang angin'; + String get skyChartSouth => 'S'; @override - String get windForecastGlobalOutlineHint => 'Bingkai luar setiap negara'; + String get skyChartWest => 'B'; @override - String get windForecastTownOutlineHint => 'Jaring yang lebih halus'; + String tonightElementAge(int days) { + return 'elemen orbit $days hari lalu'; + } @override - String eewSerial(int serial) { - return 'Laporan $serial'; + String almanacLunarDate(String leap, int month, int day) { + return '${leap}bulan $month, hari $day'; } @override - String get eewMaxIntensity => 'Intensitas maks'; + String get tonightNoShowers => 'Tidak ada hujan meteor'; @override - String get eewLocalIntensity => 'Perkiraan di lokasi'; + String get tonightNoPasses => 'Tidak ada lintasan terlihat dalam 48 jam'; @override - String get eewSWave => 'Gelombang S'; + String get tonightSatellitesUnavailable => 'Data orbit tidak terbaca'; @override - String get eewArrived => 'Tiba'; + String get tonightNoTargets => 'Tidak ada sasaran cukup tinggi'; @override - String eewCountdown(int seconds) { - return '$seconds detik'; - } + String get skyChartUnavailable => 'Katalog bintang tidak terbaca'; } diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index 9c21c9b80..22e455c1a 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,508 +10,497 @@ class AppLocalizationsJa extends AppLocalizations { AppLocalizationsJa([String locale = 'ja']) : super(locale); @override - String get languageName => '日本語'; + String typhoonValueLat(String lat) { + return '北緯 $lat 度'; + } @override - String get navHome => 'ホーム'; + String get onboardingSkipBody => + '位置情報と通知を許可しないと、DPIP はお近くの地震や災害をリアルタイムでお知らせできません。設定から後で許可することもできます。'; @override - String get navEvents => 'イベント'; + String get rainInterval24h => '24時間'; @override - String get navMap => '地図'; + String homeRainTrendHeavyStopping(int minutes) { + return '$minutes分後に大雨が止む見込みです'; + } @override - String get navData => 'データ'; + String get mapTimelineObserved => '観測'; @override - String get navEarthquake => '地震'; + String get regionSelectTitle => '地域を選択'; @override - String get dataSectionSeismic => '地震'; + String get skyTimeNoon => '正午'; @override - String get dataEarthquakeSubtitle => '地震報告'; + String get radarCountyOutlineSubtitle => 'レーダーエコーの下でも県市境界が見えるようにします。'; @override - String get dataSectionWeather => '気象'; + String get dpmFilterSectionRestroomType => 'トイレの種類'; @override - String get dataWeatherRankingSubtitle => '即時観測ランキング'; + String get mapLayerSatelliteB03 => 'ひまわり 可視赤(B03)'; @override - String get weatherRankingTitle => '観測ランキング'; + String get reportFilterIntensity => '震度'; @override - String weatherRankingMeta(String time, int count) { - return 'データ時刻:$time\n観測点 $count'; - } + String get mapLayerLightning => '雷'; @override - String get weatherRankingEmpty => '並べ替え可能な観測がありません'; + String get restroomTypeMale => '男性用トイレ'; @override - String get weatherRankingBy => '並び'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => '最高'; + String get reportDetailSortByCounty => '地域順に並べ替え'; @override - String get weatherRankingLowest => '最低'; + String get homeRainTrendScattered => 'にわか雨の可能性があります'; @override - String get weatherRankingMergeTo => '統合'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => '町村'; + String get weatherRankingTempExtremes => '気温極値'; @override - String get weatherRankingMergeCounty => '県市'; + String get themeLight => 'ライト'; @override - String get weatherRankingWind => '風速'; + String get mapTerrainReliefHint => 'ベースマップに地形の陰影を表示'; @override - String get weatherRankingGust => '突風'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => '気温極値'; + String get moreSectionRegion => '地域'; @override - String get weatherRankingExtremeHigh => '今日の最高'; + String get dpmDisasterEarthquake => '震災'; @override - String get weatherRankingExtremeLow => '今日の最低'; + String get mapLayerSatellite => 'ひまわり 赤外線(B13)'; @override - String get weatherRankingExtremeRange => '日較差'; + String get aedHoursSaturday => '土曜の開館時間'; @override - String weatherRankingRecordedAt(String time) { - return '記録時刻 $time'; - } + String get dpmDisasterSlope => '斜面災害'; @override - String weatherRankingAnalysisCurrent(String value) { - return '現在 $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return '最高 $value'; - } + String get notifySectionEew => '緊急地震速報'; @override - String weatherRankingAnalysisLow(String value) { - return '最低 $value'; - } + String get mapResetNorth => '北を上にする'; @override - String weatherRankingAnalysisRange(String value) { - return '較差 $value°C'; - } + String get rainInterval2d => '2日'; @override - String get reportListEmpty => '地震報告はありません'; + String get mapTownLabelsHint => '拡大すると郷鎮名を表示'; @override - String get reportListEmptyFiltered => '条件に一致する地震報告はありません'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => '津波警報のみ'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => '詳細設定'; @override - String get reportListLocalFelt => '局地有感'; + String get weatherRankingExtremeRange => '日較差'; @override - String get reportListToday => '今日'; + String get notifySettingsMenu => '通知設定'; @override - String get reportListYesterday => '昨日'; + String get typhoonHistoryTitle => '資料時刻'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app(デフォルト)'; } @override - String get reportListEnd => 'これ以上ありません'; + String get trendRange24h => '24時間'; @override - String get reportFilterTitle => '絞り込み'; + String get mapLayerStyleJmaTooltip => 'グレースケールをベースに −40 °C 以下を着色し、雲頂高度を強調'; @override - String get reportFilterSort => '並び替え'; + String weatherRankingRecordedAt(String time) { + return '記録時刻 $time'; + } @override - String get reportFilterSortTime => '時間'; + String get mapLayerRain => '降水量'; @override - String get reportFilterSortIntensity => '震度'; + String get mapLayerQpesums => '1時間降水量予報'; @override - String get reportFilterSortMagnitude => '規模'; + String get mapOverlaySectionMap => '地図'; @override - String get reportFilterSortDepth => '深さ'; + String get mapTerrainRelief => '地形の立体感'; @override - String get reportFilterOrderDesc => '降順'; + String get eewMaxIntensity => '最大震度'; @override - String get reportFilterOrderAsc => '昇順'; + String get mapLegendCollapse => '凡例を閉じる'; @override - String get reportFilterIntensity => '震度'; + String get changelogTitle => '更新履歴'; @override - String get reportFilterIntensityInfoTitle => '震度の新制と旧制'; + String get reportFilterOrderDesc => '降順'; @override - String get reportFilterIntensityInfoIntro => - '気象署は 2020 年 1 月 1 日(台北時間)から新制震度を採用しています。'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyTitle => '旧制(2020 年より前)'; + String get reportFilterIntensityInfoTitle => '震度の新制と旧制'; @override - String get reportFilterIntensityInfoLegacyBody => - '震度は 0–7 のみ。5弱/5強/6弱/6強の区分はありません。'; + String get mapLayerTyphoon => '台風'; @override - String get reportFilterIntensityInfoModernTitle => '新制(2020 年以降)'; + String get radarOverlayMenuTooltip => 'レーダーレイヤー設定'; @override - String get reportFilterIntensityInfoModernBody => - '震度は 0–4、5弱、5強、6弱、6強、7。フィルタは新制に準拠し、それ以前の地震はリストで旧制表記になります。'; + String get mapMyLocation => '現在地'; @override - String get reportFilterMagnitude => 'マグニチュード'; + String get meshtasticNodes => 'Nodes'; @override - String get reportFilterDepth => '深さ'; + String get meshtasticSend => 'Send'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get typhoonOverlayStormL7Tooltip => '強風域 + 平均円(紫)'; @override - String get reportFilterDate => '日付'; + String get aedType => '種類'; @override - String get reportFilterDatePick => '日付を選択'; + String get termsOfService => '利用規約'; @override - String get reportFilterDateStartNote => '開始日:当日 00:00(台北時間)'; + String get typhoonLegendCircle25 => '暴風域(50kt)'; @override - String get reportFilterDateEndNote => '終了日:当日 24:00(台北時間)'; + String get sponsorTitle => 'DPIP を支援'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get mapNavSatellite => '衛星'; @override - String get reportFilterLocation => '場所'; + String homeRainTrendUpdated(String time) { + return '更新 $time'; + } @override - String get reportFilterLocationHint => '例:花蓮、海域'; + String get onboardingNext => '次へ'; @override - String get reportFilterAny => '指定なし'; + String get weatherRankingMergeTown => '町村'; @override - String get reportFilterApply => '適用'; + String get mapLayerMonitor => '強震モニタ'; @override - String get reportFilterReset => 'リセット'; + String get moreYoutube => 'YouTube'; @override - String get reportListSearch => '検索'; + String get sponsorSubscriptions => 'サブスクリプション'; @override - String get reportDetailTitle => '地震レポート'; + String typhoonValueLon(String lon) { + return '東経 $lon 度'; + } @override - String reportDetailNumbered(String number) { - return 'No.$number 顕著有感地震'; - } + String get skyTime => '空の時刻'; @override - String get reportDetailLocalFelt => '局地的な有感地震'; + String get weatherModeCloudy => '曇り'; @override - String get reportDetailInfo => '詳細情報'; + String get skyTimeDusk => '薄暮'; @override - String get reportDetailOriginTime => '発震時刻'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailEpicenter => '震央座標'; + String get reportFilterDateEndNote => '終了日:当日 24:00(台北時間)'; @override - String get reportDetailMagnitude => '地震規模'; + String get reportFilterSortMagnitude => '規模'; @override - String get reportDetailDepth => '震源の深さ'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailAreaIntensity => '地域別震度'; + String get mapLayerCategoryEarthquake => '地震'; @override - String get reportDetailLocalIntensity => '現在地の震度'; + String get mapLayerSatelliteB12 => 'ひまわり オゾン(B12)'; @override - String get reportDetailLocalIntensityUnavailable => '震度情報なし'; + String get typhoonLegendPast => '実況経路'; @override - String get reportDetailSortByIntensity => '震度順に並べ替え'; + String get restroomCategoryOther => 'その他'; @override - String get reportDetailSortByCounty => '地域順に並べ替え'; + String homeForecastHighLow(String high, String low) { + return '高 $high° · 低 $low°'; + } @override - String get reportDetailImage => '地震レポート画像'; + String get locationBannerFix => '設定を開く'; @override - String get reportDetailImageUnavailable => 'レポート画像はまだありません'; + String get mapLegendExpand => '凡例'; @override - String get reportDetailOpenReport => 'レポートページ'; + String get eewNone => '現在、緊急地震速報はありません'; @override - String get reportDetailReplay => 'リプレイ'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get navMore => 'その他'; + String get notifyOptTsunamiAll => '津波情報・津波警報'; @override - String get appLogs => 'アプリログ'; + String get meshtasticLayerOptions => 'Node options'; @override - String get changelogTitle => '更新履歴'; + String get onboardingAgreeContinue => '同意して続行'; @override - String get changelogEmpty => 'リリースノートはまだありません'; + String get commonRetry => '再試行'; @override - String get changelogTypePrerelease => 'ベータ'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogTypeStable => '正式'; + String reportDetailNumbered(String number) { + return 'No.$number 顕著有感地震'; + } @override - String get changelogCurrentVersion => '現行'; + String get typhoonOverlayStormBandSubtitle => '平均円付き'; @override - String get changelogVersionDetails => 'リリース詳細'; + String get disasterMapOverlayRestroomTooltip => 'トイレを表示'; @override - String get changelogBodyEmpty => 'このリリースの説明はありません。'; + String get weatherRankingTitle => '観測ランキング'; @override - String get mapPlaceholderDisabled => '地図(一時的に無効)'; + String get homeRainTrendHeavySustained => '今後1時間は大雨が続きます'; @override - String get moreSectionRegion => '地域'; + String get notifySectionTsunami => '津波'; @override - String get moreSectionNotify => '通知'; + String get restroomCategoryPark => '公園'; @override - String get moreSectionDisplay => '表示'; + String get moreLinkOpenFailed => 'リンクを開けませんでした'; @override - String get regionManageTitle => '登録地域'; + String get themeDark => 'ダーク'; @override - String get regionAddButton => '地域を追加'; + String get sponsorRestore => '購入を復元'; @override - String get regionEmpty => '登録地域がありません'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String get regionSelectTitle => '地域を選択'; + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String regionSelectCount(int count, int max) { - return '$count/$max 件選択中'; - } + String get meshtasticTraffic => 'Traffic'; @override - String regionSelectFull(int max) { - return '地域は最大 $max 件まで登録できます'; - } + String get mapLayerStyleBdTooltip => 'Dvorak BD カーブ——熱帯低気圧の強度解析に使う階段グレースケール'; @override - String get regionEdit => '変更'; + String get disasterMapOverlayAedTooltip => 'AEDの位置を表示'; @override - String get moreSectionAdvanced => '詳細設定'; + String get mapLayerHumidity => '湿度'; @override - String get moreDeveloper => 'デバッグ情報'; + String get mapLayerSatelliteTransparentNight => '夜間 = 透明、地図が透ける'; @override - String get experimentalFeatures => '実験的機能'; + String get meshtasticScanning => 'Scanning…'; @override - String get moreSectionLinks => '関連リンク'; + String regionSelectFull(int max) { + return '地域は最大 $max 件まで登録できます'; + } @override - String get moreCwaEew => '中央気象署 緊急地震速報'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreTremReport => 'TREM 検知レポート'; + String get navMore => 'その他'; @override - String get moreServerStatus => 'サーバー状態'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreAnnouncements => 'お知らせ'; + String get disasterMapOverlaySectionLayers => 'レイヤー'; @override - String get moreDiscord => 'Discord コミュニティ'; + String get mapLayerSatelliteB05 => 'ひまわり 近赤外(B05)'; @override - String get moreNotifyLog => 'DPIP 通知送信履歴'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get moreLinkOpenFailed => 'リンクを開けませんでした'; + String get typhoonLabelNe => '北東'; @override - String get weatherDynamicState => '天気アニメーション'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherDynamicStateSubtitle => 'ホーム背景の天気を上書きします'; + String get reportListEmpty => '地震報告はありません'; @override - String get weatherModeAuto => '自動'; + String get reportListEnd => 'これ以上ありません'; @override - String get weatherModeClear => '晴れ'; + String get mapLayerSatelliteTruecolor => 'ひまわり トゥルーカラー'; @override - String get weatherModeRain => '雨'; + String get typhoonOverlaySectionExtra => 'オーバーレイ'; @override - String get weatherModeFog => '霧'; + String get eewSWave => 'S波'; @override - String get weatherModeThunderstorm => '雷雨'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get commonLoading => '読み込み中…'; + String get restroomCategoryCultural => '文化・娯楽施設'; @override - String get commonRetry => '再試行'; + String get typhoonLabelWind => '中心付近の最大風速'; @override - String get commonError => '問題が発生しました'; + String get radarGlobalOutlineHint => '各国の国境外枠'; @override - String get commonFetchFailed => 'データを取得できませんでした。しばらくしてから再度お試しください。'; + String get notifyEvacuation => '防災情報'; @override - String get commonEmpty => '表示する項目がありません'; + String get typhoonLegendCircle15 => '強風域(30kt)'; @override - String get feedConnecting => '接続中…'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedStale => 'データが最新でない可能性があります'; + String get homeRainTrendLightSustained => '今後1時間は小雨が続きます'; @override - String get feedOffline => '接続が切断されました'; + String get commonError => '問題が発生しました'; @override - String get eewTitle => '緊急地震速報'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String get eewNone => '現在、緊急地震速報はありません'; + String get meshtasticPower => 'Power'; @override - String eewSummary(String magnitude, String depth) { - return 'M$magnitude・深さ $depth km'; + String get mapTimelineNow => '現在'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => '全国'; + String get reportDetailOpenReport => 'レポートページ'; @override - String get regionCurrent => '現在地'; + String get trendRange7d => '7日間'; @override - String get regionCurrentUnavailable => '現在地を取得できません'; + String typhoonWarningAreas(String areas) { + return '対象地域:$areas'; + } @override - String get weatherPrecipitation => '降水量'; + String get rainIntervalSection => '集計時間'; @override - String get weatherHumidity => '湿度'; + String get notifyTitle => '通知'; @override - String weatherDataTime(String station, String time) { - return '$station · データ時刻 $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => '地図で見る'; + String get restroomCategoryLabel => '区分'; @override - String get homeForecastTitle => '24時間予報'; + String get sponsorRestoring => '購入を復元しています…'; @override - String homeForecastHighLow(String high, String low) { - return '高 $high° · 低 $low°'; - } + String get sponsorIntro => + 'DPIP はリアルタイムの防災情報の提供に取り組んでおり、広告やその他の収益モデルはありません。皆さまのご支援はサーバーの運用と継続的な開発に役立ちます。'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => '住所'; @override - String homeForecastFeelsLike(String temp) { - return '体感 $temp°'; - } + String get typhoonLabelStormAvg => '暴風域の平均半径'; @override - String homeForecastHumidity(String value) { - return '湿度 $value%'; - } + String get restroomCategoryCommercial => '商業・営業施設'; @override - String homeForecastWind(String direction, String level) { - return '$direction · 風力$level'; - } + String get aedRegion => '地域'; @override - String get homeForecastUnavailable => '地域を選ぶと予報を表示します'; + String homeRainTrendLightStopping(int minutes) { + return '$minutes分後に小雨が止む見込みです'; + } @override - String get homeForecastEmpty => '予報データがありません'; + String get reportDetailInfo => '詳細情報'; @override - String get homeActiveEventsTitle => '発生中の事象'; + String get mapNavWind => '風向'; @override - String get homeActiveEventsEmpty => '発生中の事象はありません'; + String get windForecastOverlayMenuTooltip => '風予報レイヤー設定'; @override - String get homeRainTrendTitle => '今後1時間の雨'; + String get dataWeatherRankingSubtitle => '即時観測ランキング'; @override String homeRainTrendMinute(int minute) { @@ -518,1283 +508,2096 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return '更新 $time'; - } + String get rainInterval6h => '6時間'; @override - String get homeRainTrendNoData => 'データなし'; + String get restroomTypeUnspecified => '未設定'; @override - String get homeRainTrendScattered => 'にわか雨の可能性があります'; + String get typhoonOverlayProbabilityHint => '予報円を隠します'; @override - String get homeRainTrendLightSustained => '今後1時間は小雨が続きます'; + String get mapLayerSatelliteGlobalOutline => '国境線'; @override - String homeRainTrendLightStopping(int minutes) { - return '$minutes分後に小雨が止む見込みです'; - } + String get mapNavTemperature => '気温'; @override - String get homeRainTrendHeavySustained => '今後1時間は大雨が続きます'; + String get typhoonLegendForecastPoint => '予報点'; @override - String homeRainTrendHeavyStopping(int minutes) { - return '$minutes分後に大雨が止む見込みです'; - } + String get reportListYesterday => '昨日'; @override - String get mapLayers => 'レイヤー'; + String get moreSectionLinks => '関連リンク'; @override - String get mapLayerOrderTitle => 'レイヤーの順番'; + String get feedOffline => '接続が切断されました'; @override - String get mapLayerOrderReset => '既定の順序に戻す'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => 'レーダー合成エコー図'; + String get moreSectionDisplay => '表示'; @override - String get mapLayerSatellite => 'ひまわり 赤外線(B13)'; + String get rainInterval3d => '3日'; @override - String get mapLayerSatelliteB01 => 'ひまわり 可視青(B01)'; + String get defaultMapLayerSubtitle => + '地図タブを開いたときに表示するレイヤーです。下部ナビのアイコンとラベルもこれに合わせます。'; @override - String get mapLayerSatelliteB02 => 'ひまわり 可視緑(B02)'; + String get aedDescription => '備考'; @override - String get mapLayerSatelliteB03 => 'ひまわり 可視赤(B03)'; + String get typhoonOverlayWeatherRadarTooltip => '通報時刻に最も近いレーダー'; @override - String get mapLayerSatelliteB04 => 'ひまわり 近赤外(B04)'; + String get onboardingPermLocationDesc => 'あなたの所在地に合わせて警報を配信します。'; @override - String get mapLayerSatelliteB05 => 'ひまわり 近赤外(B05)'; + String get mapLayerSatelliteB16 => 'ひまわり 二酸化炭素(B16)'; @override - String get mapLayerSatelliteB06 => 'ひまわり 近赤外(B06)'; + String get homeActiveEventsEmpty => '発生中の事象はありません'; @override - String get mapLayerSatelliteB07 => 'ひまわり 短波長赤外(B07)'; + String get typhoonLabelPosition => '中心位置'; @override - String get mapLayerSatelliteB08 => 'ひまわり 上層水蒸気(B08)'; + String get weatherRankingBy => '並び'; @override - String get mapLayerSatelliteB09 => 'ひまわり 中層水蒸気(B09)'; + String get typhoonIntensityMild => '弱い台風'; @override - String get mapLayerSatelliteB10 => 'ひまわり 下層水蒸気(B10)'; + String get windForecastGlobalOutlineHint => '各国の国境外枠'; @override - String get mapLayerSatelliteB11 => 'ひまわり 二酸化硫黄/雲相(B11)'; + String get rainInterval1h => '1時間'; @override - String get mapLayerSatelliteB12 => 'ひまわり オゾン(B12)'; + String get eewLocalIntensity => '現在地の推定'; @override - String get mapLayerSatelliteB13 => 'ひまわり 赤外線(B13)'; + String get mapLayerRadar => 'レーダー合成エコー図'; @override - String get mapLayerSatelliteB14 => 'ひまわり 長波長赤外線(B14)'; + String get restroomCategoryReligious => '宗教・礼拝施設'; @override - String get mapLayerSatelliteB15 => 'ひまわり 長波長赤外線(B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => 'ひまわり 二酸化炭素(B16)'; + String get mapLayerSatelliteCloudCloudy => '雲'; @override - String get mapLayerSatelliteTruecolor => 'ひまわり トゥルーカラー'; + String get skyTimeSunrise => '日の出'; @override - String get mapLayerSatelliteNaturalcolor => 'ひまわり ナチュラルカラー'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + String get onboardingPermNotifyDesc => '地震、天気、災害の発生時に、警報をすぐお届けします。'; @override - String get mapLayerSatelliteDust => 'ひまわり 黄砂'; + String get radarTownOutline => '市町村境界'; @override - String get mapLayerSatelliteAirmass => 'ひまわり エアマス'; + String get mapLayerStyleSection => '色調'; @override - String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; + String get disasterMapOverlayMenuTooltip => '防災マップのレイヤー'; @override - String get mapLayerSatelliteWatervapor => 'ひまわり 水蒸気'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'ひまわり スプリットウィンドウ'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; + String get typhoonLabelSw => '南西'; @override - String get mapLayerSatelliteBtdWvirw => 'ひまわり オーバーシューティングトップ'; + String typhoonForecastLead(String hours) { + return '予報 +$hours 時間'; + } @override - String get mapLayerSatelliteBtdSo2 => 'ひまわり 二酸化硫黄/雲相'; + String get dpmDisasterTsunami => '津波'; @override - String get mapLayerSatelliteBtdCo2 => 'ひまわり 巻雲/雲頂高度'; + String get changelogTypeStable => '正式'; @override - String get mapLayerSatelliteBtdOzone => 'ひまわり 対流圏界面'; + String get mapLayerSatelliteTransparentClear => '晴れ = 透明、地図が透ける'; @override - String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂温度'; + String get mapOverlaySectionReference => '参照レイヤー'; @override - String get mapLayerSatelliteCloudmask => 'ひまわり 雲マスク'; + String get mapLayerSatelliteB02 => 'ひまわり 可視緑(B02)'; @override - String get mapLayerSatelliteSst => 'ひまわり 海面水温'; + String get reportListLocalFelt => '局地有感'; @override - String get mapLayerSatelliteNdvi => 'ひまわり NDVI'; + String get weatherRankingEmpty => '並べ替え可能な観測がありません'; @override - String get mapLayerSatelliteNdwi => 'ひまわり NDWI'; + String get notifySectionOther => 'その他'; @override - String get mapLayerSatelliteMndwi => 'ひまわり MNDWI'; + String weatherRankingMeta(String time, int count) { + return 'データ時刻:$time\n観測点 $count'; + } @override - String get mapLayerSatelliteGlobalOutline => '国境線'; + String get onboardingTermsAgree => 'サービス利用規約を読み、同意します'; @override - String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA レシピ)'; + String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(植生なし)'; @override - String get mapLayerSatelliteCloudClear => '晴れ'; + String get notifyOptLocalIntensity4 => '所在地の震度4以上'; @override - String get mapLayerSatelliteCloudProbablyClear => 'おそらく晴れ'; + String get eewArrived => '到達'; @override - String get mapLayerSatelliteCloudProbablyCloudy => 'おそらく雲'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => '雲'; + String get mapLayerCategoryLife => '生活'; @override - String get mapLayerSatelliteTransparentWarm => '晴れ(暖域) = 透明、地図が透ける'; + String get reportFilterSortIntensity => '震度'; @override - String get mapLayerSatelliteTransparentReflectance => '低反射率・夜間 = 透明、地図が透ける'; + String get typhoonMotion => '進行'; @override - String get mapLayerSatelliteTransparentZero => '差ゼロ = 透明(信号なし)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => '夜間 = 透明、地図が透ける'; + String get typhoonIntensityIntense => '強い台風'; @override - String get mapLayerSatelliteTransparentNoData => 'データなし(陸上) = 透明'; + String get mapLayerOrderTitle => 'レイヤーの順番'; @override - String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(植生なし)'; + String get dpmYes => 'はい'; @override - String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(水域なし)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => '晴れ = 透明、地図が透ける'; + String get reportDetailLocalIntensityUnavailable => '震度情報なし'; @override - String get mapLayerStyleSection => '色調'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => '色調'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => 'グレースケール(JMA)'; + String get reportFilterDepth => '深さ'; @override - String get mapLayerStyleGrayTooltip => '気象庁の赤外画像の慣例:温度が低いほど白'; + String get onboardingScrollHint => '下にスクロールして続行してください'; @override - String get mapLayerStyleJma => '雲頂強調(JMA)'; + String get mapNavQpesums => '予報'; @override - String get mapLayerStyleJmaTooltip => 'グレースケールをベースに −40 °C 以下を着色し、雲頂高度を強調'; + String get navMap => '地図'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => '気象警報・注意報'; @override - String get mapLayerStyleBdTooltip => 'Dvorak BD カーブ——熱帯低気圧の強度解析に使う階段グレースケール'; + String get reportFilterReset => 'リセット'; @override - String get mapLayerQpesums => '1時間降水量予報'; + String get mapLayerSatelliteMndwi => 'ひまわり MNDWI'; @override - String get mapLayerLightning => '雷'; + String get typhoonOverlaySectionStorm => '暴風域'; @override - String lightningLegendCg(int minutes) { - return '対地 · $minutes 分以内'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return '雲間 · $minutes 分以内'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => '現在'; + String get weatherDynamicStateSubtitle => 'ホーム背景の天気を上書きします'; @override - String get mapTimelinePast => '過去'; + String get reportFilterIntensityInfoModernTitle => '新制(2020 年以降)'; @override - String get mapTimelineFuture => '未来'; + String typhoonDataTime(String time) { + return '資料時刻\n$time'; + } @override - String get mapTimelineObserved => '観測'; + String get restroomTypeAccessible => 'バリアフリートイレ'; @override - String get mapTimelineForecast => '予報'; + String get moreSectionAbout => '情報'; @override - String mapTimelineDataTime(String time) { - return 'データ時刻 $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => '通知設定'; + String get onboardingIntroBody => + 'DPIP はあなたと共にある防災パートナーです。緊急地震速報、地震報告、天気、各種災害情報を統合し、重要な瞬間にすぐお知らせします。\n\n• 地震:緊急地震速報、震度速報、地震報告\n• 天気:雷雨即時情報、気象警報・注意報\n• 津波・防災情報\n\n次に、サービス利用規約をご確認いただき、DPIP がリアルタイムであなたを守れるよう、いくつかの権限の許可をお願いします。'; @override - String get notifyTitle => '通知'; + String get shelterCapacityLabel => '収容人数'; @override - String get notifyUnavailable => 'プッシュ通知はまだ準備できていません。しばらくしてから再度お試しください。'; + String get reportDetailImage => '地震レポート画像'; @override - String get notifySetFailed => '設定を保存できませんでした。もう一度お試しください。'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => '緊急地震速報'; + String get typhoonLabelGaleAvg => '強風域の平均半径'; @override - String get notifySectionEarthquake => '地震'; + String get onboardingPermNotify => '通知'; @override - String get notifySectionWeather => '天気'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => '津波'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => 'その他'; + String get defaultMapLayerSettings => '地図の初期レイヤー'; @override - String get notifyEew => '緊急地震速報'; + String get moreSectionNotify => '通知'; @override - String get notifyMonitor => '強震モニタ'; + String get notifyUnavailable => 'プッシュ通知はまだ準備できていません。しばらくしてから再度お試しください。'; @override - String get notifyReport => '地震報告'; + String get mapLayerOrderReset => '既定の順序に戻す'; @override - String get notifyIntensity => '震度速報'; + String get dpmAddress => '住所'; @override - String get notifyThunderstorm => '雷雨情報'; + String get weatherRankingMergeCounty => '県市'; @override - String get notifyAdvisory => '気象警報・注意報'; + String get moreSectionApp => 'アプリを入手'; @override - String get notifyEvacuation => '防災情報'; + String get reportFilterIntensityInfoLegacyBody => + '震度は 0–7 のみ。5弱/5強/6弱/6強の区分はありません。'; @override - String get notifyTsunami => '津波情報'; + String get mapLayerSatelliteSst => 'ひまわり 海面水温'; @override - String get notifyAnnouncement => 'お知らせ'; + String get qpesumsOverlayMenuTooltip => '定量降水予報レイヤー設定'; @override - String get notifyOptOff => 'オフ'; + String get mapTimelineFuture => '未来'; @override - String get notifyOptAll => 'すべて受信'; + String get typhoonLegendCircleAvg => '平均円'; @override - String get notifyOptLocalIntensity4 => '所在地の震度4以上'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => '所在地の震度1以上'; + String get typhoonLabelSe => '南東'; @override - String get notifyOptWeatherLocal => '現在地のみ'; + String get radarTownOutlineHint => 'より細かい区分'; @override - String get notifyOptTsunamiWarning => '津波警報のみ'; + String eewCountdown(int seconds) { + return 'あと $seconds 秒'; + } @override - String get notifyOptTsunamiAll => '津波情報・津波警報'; + String get typhoonLabelGust => '最大瞬間風速'; @override - String get onboardingNext => '次へ'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => '戻る'; + String get sponsorTerms => '利用規約'; @override - String get onboardingScrollHint => '下にスクロールして続行してください'; + String get restroomTypeGenderNeutral => 'ジェンダーニュートラルトイレ'; @override - String get onboardingIntroTitle => 'DPIP へようこそ'; + String get notifyThunderstorm => '雷雨情報'; @override - String get onboardingIntroBody => - 'DPIP はあなたと共にある防災パートナーです。緊急地震速報、地震報告、天気、各種災害情報を統合し、重要な瞬間にすぐお知らせします。\n\n• 地震:緊急地震速報、震度速報、地震報告\n• 天気:雷雨即時情報、気象警報・注意報\n• 津波・防災情報\n\n次に、サービス利用規約をご確認いただき、DPIP がリアルタイムであなたを守れるよう、いくつかの権限の許可をお願いします。'; + String get skyTimeGolden => 'ゴールデンアワー'; @override - String get onboardingTermsTitle => 'サービス利用規約'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'DPIP をご利用になる前に、以下の注意事項を必ずお読みください:\n\n• すべての情報は、台湾中央気象署(CWA)が発表する内容を優先してください。\n\n• ネットワーク、サーバー、アプリ、上流のデータソースの状態によっては、情報を受信できない場合があります。可能な限り回避に努めますが、決して発生しないことを保証するものではありません。\n\n• 強い揺れが、通知より先にあなたの所在地へ到達する場合があります。\n\n• 緊急地震速報は高速に計算された結果であり、大きな誤差を含む可能性があります。この点を理解したうえで、慎重にご利用ください。\n\n• 公的機関に認められていない行為には法的リスクが伴う可能性があります。関連する規定を必ずお守りください。\n\nまた、地域に応じた警報を提供するため、本サービスは、どの警報をあなたに送信するかを判断する目的にのみ、あなたのおおよその位置情報とプッシュ識別子を、フォアグラウンドおよびバックグラウンドで収集・アップロードします。\n\n下部の「同意して続行」をタップすることで、上記を読み、理解し、同意したものとみなされます。'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => 'サービス利用規約を読み、同意します'; + String weatherRankingAnalysisCurrent(String value) { + return '現在 $value°C'; + } @override - String get onboardingAgreeContinue => '同意して続行'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => '権限の許可'; + String get homeForecastUnavailable => '地域を選ぶと予報を表示します'; @override - String get onboardingPermsBody => - '災害が発生した瞬間に DPIP がお知らせできるよう、以下の権限を許可してください。これらはシステム設定でいつでも変更できます。'; + String get mapLayers => 'レイヤー'; @override - String get onboardingPermNotify => '通知'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => '地震、天気、災害の発生時に、警報をすぐお届けします。'; + String get languageSettings => '言語設定'; @override - String get onboardingPermCritical => '重大な通知'; + String get dpmDisasterNuclear => '原子力事故'; @override - String get onboardingPermCriticalDesc => - '生命に関わる緊急地震速報を、消音モードやおやすみモードでも鳴らせるようにします。'; + String get language => '言語'; @override - String get onboardingPermLocation => '位置情報'; + String homeForecastFeelsLike(String temp) { + return '体感 $temp°'; + } @override - String get onboardingPermLocationDesc => 'あなたの所在地に合わせて警報を配信します。'; + String get typhoonOverlayWeatherHint => '通報時刻に合わせる'; @override - String get onboardingPermBackground => 'バックグラウンドの位置情報'; + String get skyTimeDawn => '夜明け前'; @override - String get onboardingPermBackgroundDesc => - '「常に許可」を選択すると、アプリを閉じていても所在地に合わせて警報を配信できます。'; + String get skyTimeAfternoon => '午後'; @override - String get onboardingPermBattery => 'バッテリー最適化の除外'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'DPIP がバックグラウンドで動作し続けられるようにして、警報の遅延や取りこぼしを防ぎます。'; + String get typhoonWarningTitle => '台風警報'; @override - String get onboardingGrant => '許可'; + String get moreSourceCode => 'ソースコード'; @override - String get onboardingGranted => '許可済み'; + String get mapLayerCategoryWeather => '気象観測'; @override - String get onboardingStart => 'はじめる'; + String get mapLayerSatelliteB09 => 'ひまわり 中層水蒸気(B09)'; @override - String get language => '言語'; + String get windForecastTownOutlineHint => 'より細かいメッシュ'; @override - String get languageSettings => '言語設定'; + String get mapLayerSatelliteCloudmask => 'ひまわり 雲マスク'; @override - String get languageSystem => 'システムの既定'; + String get mapAppCopyCoordinates => '座標をコピー'; @override - String get locationBannerServiceOff => '位置情報サービスがオフです。所在地に合わせた警報を配信できません。'; + String get reportFilterIntensityInfoIntro => + '気象署は 2020 年 1 月 1 日(台北時間)から新制震度を採用しています。'; @override - String get locationBannerPermission => '位置情報の許可がオフです。所在地に合わせた警報を配信できません。'; + String get mapNavEarthquake => '地震'; @override - String get locationBannerFix => '設定を開く'; + String get typhoonGust => '最大瞬間風速'; @override - String get notifyBannerDisabled => '通知がオフです — 災害警報を受け取れません。'; + String get restroomGradeAverage => '普通'; @override - String get onboardingSkipTitle => '権限が許可されていません'; + String get mapLayerSatelliteBtdCo2 => 'ひまわり 巻雲/雲頂高度'; @override - String get onboardingSkipBody => - '位置情報と通知を許可しないと、DPIP はお近くの地震や災害をリアルタイムでお知らせできません。設定から後で許可することもできます。'; + String get onboardingPermBackgroundDesc => + '「常に許可」を選択すると、アプリを閉じていても所在地に合わせて警報を配信できます。'; @override - String get onboardingSkipStay => '戻って許可'; + String get mapTimelineForecast => '予報'; @override - String get onboardingSkipLeave => 'このままスキップ'; + String get restroomTypeLabel => '種別'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => '地震'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => '暴風域 + 平均円(黄)'; @override - String get moreSourceCode => 'ソースコード'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => 'アプリを入手'; + String get reportDetailTitle => '地震レポート'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'TREM 検知レポート'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · データ時刻 $time'; + } @override - String get displaySettings => '表示'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => '地図の初期レイヤー'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - '地図タブを開いたときに表示するレイヤーです。下部ナビのアイコンとラベルもこれに合わせます。'; + String get radarCountyOutline => '県市境界'; @override - String get mapNavRadar => 'レーダー'; + String get onboardingGranted => '許可済み'; @override - String get mapNavQpesums => '予報'; + String get commonClose => '閉じる'; @override - String get mapNavSatellite => '衛星'; + String get restroomGradeLabel => '等級'; @override - String get mapNavLightning => '稲妻'; + String get rainIntervalNow => '今日'; @override - String get mapNavTyphoon => '台風'; + String get changelogCurrentVersion => '現行'; @override - String get mapNavEarthquake => '地震'; + String get typhoonLabelPressure => '中心気圧'; @override - String get mapNavTemperature => '気温'; + String get typhoonOverlayForecastCalloutsTooltip => '拡大時に予報点の詳細カードを表示'; @override - String get mapNavHumidity => '湿度'; + String get aedOpenRemark => '開館時間メモ'; @override - String get mapNavPressure => '気圧'; + String get onboardingPermsBody => + '災害が発生した瞬間に DPIP がお知らせできるよう、以下の権限を許可してください。これらはシステム設定でいつでも変更できます。'; @override - String get mapNavWind => '風向'; + String get typhoonOverlaySectionWeather => '天気下敷き'; @override - String get mapNavRain => '雨量'; + String get notifyOptWeatherLocal => '現在地のみ'; @override - String get mapNavDisaster => '防災'; + String get mapNavRain => '雨量'; @override - String get displayTheme => 'テーマ'; + String get moonDays => 'days'; @override - String get themeSystem => 'システム'; + String mapLegendUnit(String unit) { + return '単位:$unit'; + } @override - String get themeLight => 'ライト'; + String get weatherModeClear => '晴れ'; @override - String get themeDark => 'ダーク'; + String get meshtasticRadio => 'Radio'; @override - String get moreSectionAbout => '情報'; + String get commonEmpty => '表示する項目がありません'; @override - String get termsOfService => '利用規約'; + String get mapLayerSatelliteB01 => 'ひまわり 可視青(B01)'; @override - String get faq => 'よくある質問'; + String get meshtasticExternalPower => 'External power'; @override - String get openSourceLicenses => 'オープンソースライセンス'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get sponsorTitle => 'DPIP を支援'; + String get reportFilterOrderAsc => '昇順'; @override - String get sponsorIntro => - 'DPIP はリアルタイムの防災情報の提供に取り組んでおり、広告やその他の収益モデルはありません。皆さまのご支援はサーバーの運用と継続的な開発に役立ちます。'; + String get reportFilterApply => '適用'; @override - String get sponsorSubscriptions => 'サブスクリプション'; + String get reportDetailImageUnavailable => 'レポート画像はまだありません'; @override - String get sponsorRecommended => 'おすすめ'; + String get weatherRankingHighest => '最高'; @override - String get sponsorOneTime => '一回限りの支援'; + String get reportDetailReplay => 'リプレイ'; @override - String sponsorPerMonth(String price) { - return '$price / 月'; - } + String get mapLayerRestroom => 'トイレ'; @override - String get sponsorRestore => '購入を復元'; + String get restroomCategoryWelfare => '社会福祉施設・集会所'; @override - String get sponsorTerms => '利用規約'; + String get restroomGradeExcellent => '最上級'; @override - String get sponsorPrivacy => 'プライバシーポリシー'; + String get meshtasticLastSent => 'Last sent'; @override - String get sponsorRestoring => '購入を復元しています…'; + String get meshtasticName => 'Name'; @override - String get sponsorRestoreUnavailable => 'ストアに接続できません。しばらくしてからもう一度お試しください。'; + String get meshtasticScan => 'Scan'; @override - String get commonClose => '閉じる'; + String get mapLayerCategoryForecast => '数値予報'; @override - String get mapLayerTemperature => '気温'; + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; @override - String get trendRange24h => '24時間'; + String get themeSystem => 'システム'; @override - String get trendRange7d => '7日間'; + String get mapLayerSatelliteNdvi => 'ひまわり NDVI'; @override - String get trendNoData => 'トレンドデータがありません'; + String get typhoonLegendForecast => '予報経路'; @override - String trendCumulativeTotal(String total) { - return '累計 $total mm'; + String typhoonValueHpa(String n) { + return '$n hPa'; } @override - String chartHourLabel(int hour) { - return '$hour時'; - } + String get weatherPrecipitation => '降水量'; @override - String get mapLayerHumidity => '湿度'; + String get moonNextFullMoon => 'Next full moon'; @override - String get mapLayerPressure => '気圧'; + String get dpmSheetEmpty => '地図上のマーカーをタップして詳細を表示'; @override - String get mapLayerWind => '風向'; + String get onboardingSkipLeave => 'このままスキップ'; @override - String get mapLayerRain => '降水量'; + String get onboardingBack => '戻る'; @override - String get rainIntervalMenu => '累積期間'; + String get aedPlaceDesc => '設置場所'; @override - String get rainIntervalNow => '今日'; + String get onboardingSkipTitle => '権限が許可されていません'; @override - String get rainInterval10m => '10分'; + String get restroomTypeFamily => '親子トイレ'; @override - String get rainInterval1h => '1時間'; - + String typhoonValueKm(String n) { + return '$n km'; + } + @override - String get rainInterval3h => '3時間'; + String get typhoonPressure => '気圧'; @override - String get rainInterval6h => '6時間'; + String get onboardingPermBattery => 'バッテリー最適化の除外'; + + @override + String get typhoonLabelNw => '北西'; + + @override + String get dpmDisasterFlood => '洪水'; + + @override + String get moonPhaseWaxingCrescent => 'Waxing crescent'; + + @override + String get restroomCategoryLeisure => 'レジャー・娯楽施設'; + + @override + String get mapLayerTemperature => '気温'; + + @override + String get aedCategory => '分類'; + + @override + String get meshtasticChannels => 'Channels'; + + @override + String get monitorWaiting => 'データ待機中…'; + + @override + String get typhoonOverlayForecastCallouts => '予報点の情報'; + + @override + String get reportDetailEpicenter => '震央座標'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => '風向'; + + @override + String get reportDetailMagnitude => '地震規模'; + + @override + String get reportDetailAreaIntensity => '地域別震度'; @override String get rainInterval12h => '12時間'; @override - String get rainInterval24h => '24時間'; + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } @override - String get rainInterval2d => '2日'; + String get dpmDisasterLandslide => '土石流'; @override - String get rainInterval3d => '3日'; + String get notifyMonitor => '強震モニタ'; @override - String get mapLayerTyphoon => '台風'; + String get onboardingStart => 'はじめる'; @override - String get typhoonNoActive => '発生中の台風なし'; + String sponsorPerMonth(String price) { + return '$price / 月'; + } @override - String get typhoonWind => '風速'; + String get mapLayerPressure => '気圧'; @override - String get typhoonGust => '最大瞬間風速'; + String get mapLayerSatelliteB04 => 'ひまわり 近赤外(B04)'; @override - String get typhoonPressure => '気圧'; + String get mapLayerSatelliteTransparentZero => '差ゼロ = 透明(信号なし)'; @override - String get typhoonMotion => '進行'; + String get shelterIndoorLabel => '屋内収容'; @override - String get typhoonLabelPosition => '中心位置'; + String get notifyOptOff => 'オフ'; @override - String get typhoonLabelDirection => 'これまでの進行方向'; + String get reportFilterSortTime => '時間'; + + @override + String get mapLayerSatelliteCloudProbablyClear => 'おそらく晴れ'; + + @override + String get weatherModeThunderstorm => '雷雨'; + + @override + String get homeViewOnMap => '地図で見る'; + + @override + String get reportFilterIntensityInfoLegacyTitle => '旧制(2020 年より前)'; @override String get typhoonLabelSpeed => 'これまでの移動速度'; @override - String get typhoonLabelPressure => '中心気圧'; + String mapAppOpenFailed(String app) { + return '$app を開けませんでした'; + } @override - String get typhoonLabelWind => '中心付近の最大風速'; + String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA レシピ)'; @override - String get typhoonLabelGust => '最大瞬間風速'; + String get meshtasticReceived => 'Received'; + + @override + String get weatherRankingExtremeLow => '今日の最低'; + + @override + String get mapLayerSatelliteB10 => 'ひまわり 下層水蒸気(B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => 'おそらく雲'; + + @override + String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(水域なし)'; + + @override + String get shelterCategoryLabel => '対象災害'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => '突風'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => '避難所の災害種別'; + + @override + String get moreServerStatus => 'サーバー状態'; + + @override + String get notifySectionWeather => '天気'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => '地震'; + + @override + String get changelogBodyEmpty => 'このリリースの説明はありません。'; + + @override + String get radarGlobalOutline => '国境線'; + + @override + String get notifyEew => '緊急地震速報'; + + @override + String get regionNationwide => '全国'; + + @override + String get moreNotifyLog => 'DPIP 通知送信履歴'; + + @override + String get regionCurrent => '現在地'; + + @override + String get dpmFilterSectionRestroom => '施設の種類'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => '雪'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => 'デバッグ情報'; + + @override + String get mapLayerSatelliteB14 => 'ひまわり 長波長赤外線(B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => '稲妻'; + + @override + String get homeForecastEmpty => '予報データがありません'; + + @override + String get sponsorOneTime => '一回限りの支援'; + + @override + String get mapLayerSatelliteBtdSplit => 'ひまわり スプリットウィンドウ'; + + @override + String get onboardingPermBackground => 'バックグラウンドの位置情報'; + + @override + String get aedEmergencyPhone => '緊急連絡先'; + + @override + String get dpmOpenInMaps => '地図アプリで開く'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + '生命に関わる緊急地震速報を、消音モードやおやすみモードでも鳴らせるようにします。'; + + @override + String get mapLayerSatelliteTransparentWarm => '晴れ(暖域) = 透明、地図が透ける'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => '24時間予報'; + + @override + String get typhoonLegendWarningAreas => '警報区域'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; + } + + @override + String get notifyOptLocalIntensity1 => '所在地の震度1以上'; + + @override + String get mapTimelinePast => '過去'; + + @override + String get restroomTypeFemale => '女性用トイレ'; + + @override + String get reportListToday => '今日'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => '読み込み中…'; + + @override + String get typhoonIntensityModerate => '並の台風'; + + @override + String get typhoonWind => '風速'; + + @override + String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + + @override + String get rainInterval3h => '3時間'; + + @override + String get reportListSearch => '検索'; + + @override + String get mapLayerCategorySatellite => '衛星'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => '場所'; + + @override + String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; + + @override + String get typhoonIntensityTd => '熱帯低気圧'; + + @override + String get reportFilterDate => '日付'; + + @override + String get sponsorRestoreUnavailable => 'ストアに接続できません。しばらくしてからもう一度お試しください。'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => '登録地域がありません'; + + @override + String get onboardingPermBatteryDesc => + 'DPIP がバックグラウンドで動作し続けられるようにして、警報の遅延や取りこぼしを防ぎます。'; + + @override + String get mapNavDisaster => '防災'; + + @override + String get radarScanRangeSubtitle => '4基のレーダーが実際に観測する範囲を示します。'; + + @override + String get aedHoursSunday => '日曜の開館時間'; + + @override + String get reportDetailOriginTime => '発震時刻'; + + @override + String get trendNoData => 'トレンドデータがありません'; + + @override + String get onboardingPermLocation => '位置情報'; + + @override + String get moreDiscord => 'Discord コミュニティ'; + + @override + String get mapNavPressure => '気圧'; + + @override + String get mapLayerSatelliteB13 => 'ひまわり 赤外線(B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => 'リリースノートはまだありません'; + + @override + String get reportFilterDateStartNote => '開始日:当日 00:00(台北時間)'; + + @override + String get eewTitle => '緊急地震速報'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '$count/$max 件選択中'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'ひまわり 二酸化硫黄/雲相'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => '本曇り'; + + @override + String get reportDetailDepth => '震源の深さ'; + + @override + String get typhoonOverlayWarningTooltip => '台風警報対象の県を強調'; + + @override + String get reportFilterDatePick => '日付を選択'; + + @override + String get onboardingSkipStay => '戻って許可'; + + @override + String get commonFetchFailed => 'データを取得できませんでした。しばらくしてから再度お試しください。'; + + @override + String get shelterOutdoorLabel => '屋外収容'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => 'レーダー'; + + @override + String get mapLayerSatelliteCloudClear => '晴れ'; + + @override + String eewSummary(String magnitude, String depth) { + return 'M$magnitude・深さ $depth km'; + } + + @override + String get locationBannerPermission => '位置情報の許可がオフです。所在地に合わせた警報を配信できません。'; + + @override + String get typhoonOverlayWeatherNoneTooltip => 'レーダー/赤外線なし'; + + @override + String get radarCountyOutlineHint => 'エコーの上に描画'; + + @override + String get windForecastCountyOutlineHint => '風場の上に描画'; + + @override + String get homeRainTrendTitle => '今後1時間の雨'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => '台風'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => '男女共用トイレ'; + + @override + String get restroomGradeGood => '優良'; + + @override + String get notifyTsunami => '津波情報'; + + @override + String get navData => 'データ'; + + @override + String get mapLayerSatelliteBtdWvirw => 'ひまわり オーバーシューティングトップ'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => 'この端末では通話できません'; + + @override + String get reportFilterAny => '指定なし'; + + @override + String get weatherRankingMergeTo => '統合'; + + @override + String get notifyIntensity => '震度速報'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => '累積期間'; + + @override + String get reportDetailLocalFelt => '局地的な有感地震'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => '許可'; + + @override + String get weatherModeRain => '雨'; + + @override + String get shelterVulnerableOkLabel => '要配慮者向け収容'; + + @override + String get stationSheetEmpty => '観測点をタップして値を表示'; + + @override + String get typhoonLegendProbability => '接近確率'; + + @override + String get reportFilterMagnitude => 'マグニチュード'; + + @override + String get skyTimeMorning => '午前'; + + @override + String get experimentalFeatures => '実験的機能'; + + @override + String get onboardingTermsBody => + 'DPIP をご利用になる前に、以下の注意事項を必ずお読みください:\n\n• すべての情報は、台湾中央気象署(CWA)が発表する内容を優先してください。\n\n• ネットワーク、サーバー、アプリ、上流のデータソースの状態によっては、情報を受信できない場合があります。可能な限り回避に努めますが、決して発生しないことを保証するものではありません。\n\n• 強い揺れが、通知より先にあなたの所在地へ到達する場合があります。\n\n• 緊急地震速報は高速に計算された結果であり、大きな誤差を含む可能性があります。この点を理解したうえで、慎重にご利用ください。\n\n• 公的機関に認められていない行為には法的リスクが伴う可能性があります。関連する規定を必ずお守りください。\n\nまた、地域に応じた警報を提供するため、本サービスは、どの警報をあなたに送信するかを判断する目的にのみ、あなたのおおよその位置情報とプッシュ識別子を、フォアグラウンドおよびバックグラウンドで収集・アップロードします。\n\n下部の「同意して続行」をタップすることで、上記を読み、理解し、同意したものとみなされます。'; + + @override + String get reportFilterTitle => '絞り込み'; + + @override + String get onboardingPermCritical => '重大な通知'; + + @override + String trendCumulativeTotal(String total) { + return '累計 $total mm'; + } + + @override + String get languageName => '日本語'; + + @override + String get reportListEmptyFiltered => '条件に一致する地震報告はありません'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => '台風'; + + @override + String get weatherModeSand => '砂じん'; + + @override + String get typhoonSatelliteTitle => '衛星'; + + @override + String get notifyReport => '地震報告'; + + @override + String get mapAppCoordinatesCopied => '座標をコピーしました'; + + @override + String get skyTimeNight => '夜'; + + @override + String get sponsorRecommended => 'おすすめ'; + + @override + String get mapLayerSatelliteB15 => 'ひまわり 長波長赤外線(B15)'; + + @override + String get weatherRankingWind => '風速'; + + @override + String get feedStale => 'データが最新でない可能性があります'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · 風力$level'; + } + + @override + String get navHome => 'ホーム'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂温度'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => 'オープンソースライセンス'; + + @override + String get weatherRankingLowest => '最低'; + + @override + String get reportFilterSortDepth => '深さ'; + + @override + String mapTimelineDataTime(String time) { + return 'データ時刻 $time'; + } + + @override + String get radarScanRange => '走査範囲を表示'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return '較差 $value°C'; + } + + @override + String get weatherRankingExtremeHigh => '今日の最高'; + + @override + String get changelogVersionDetails => 'リリース詳細'; + + @override + String get sponsorPrivacy => 'プライバシーポリシー'; + + @override + String get reportDetailLocalIntensity => '現在地の震度'; + + @override + String get mapLayerSatelliteNaturalcolor => 'ひまわり ナチュラルカラー'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; + + @override + String shelterCapacityValue(int n) { + return '$n 人'; + } + + @override + String lightningLegendCc(int minutes) { + return '雲間 · $minutes 分以内'; + } + + @override + String get meshtasticSendHint => 'Message to broadcast'; + + @override + String monitorDelay(String value) { + return '遅延 $value s'; + } + + @override + String get dpmNo => 'いいえ'; + + @override + String get mapLayerSatelliteB08 => 'ひまわり 上層水蒸気(B08)'; + + @override + String get meshtasticReconnecting => 'Reconnecting…'; + + @override + String get radarTownOutlineSubtitle => 'レーダーエコーの下でも市町村境界が見えるようにします。'; + + @override + String get typhoonOverlayWeatherSatelliteTooltip => '通報時刻に最も近い赤外線'; + + @override + String get radarScanRangeHint => '枠外の空白は未観測'; + + @override + String typhoonPickerTd(String no) { + return '熱帯低気圧 TD $no'; + } + + @override + String get mapLayerSatelliteWatervapor => 'ひまわり 水蒸気'; + + @override + String get regionAddButton => '地域を追加'; + + @override + String get displaySettings => '表示'; + + @override + String get restroomGradePoor => '不合格'; + + @override + String get restroomCategoryTourist => '観光地・景勝地'; + + @override + String get locationBannerServiceOff => '位置情報サービスがオフです。所在地に合わせた警報を配信できません。'; + + @override + String get mapLayerStyleTooltip => '色調'; + + @override + String lightningLegendCg(int minutes) { + return '対地 · $minutes 分以内'; + } + + @override + String get skyTimeAuto => '自動'; + + @override + String get appLogs => 'アプリログ'; + + @override + String get feedConnecting => '接続中…'; + + @override + String get notifyBannerDisabled => '通知がオフです — 災害警報を受け取れません。'; + + @override + String get weatherHumidity => '湿度'; + + @override + String typhoonValueMs(String n) { + return '毎秒 $n m'; + } + + @override + String homeForecastHumidity(String value) { + return '湿度 $value%'; + } + + @override + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; + + @override + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; + + @override + String get restroomCategoryTransport => '交通'; + + @override + String get reportFilterLocationHint => '例:花蓮、海域'; + + @override + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; + + @override + String get meshtasticBattery => 'Battery'; + + @override + String get meshtasticDistance => '距離'; + + @override + String get meshtasticSnrTrend => '信号トレンド (SNR)'; + + @override + String get meshtasticBatteryTrend => 'バッテリー推移'; + + @override + String get typhoonOverlayMenuTooltip => '台風オーバーレイ設定'; + + @override + String get mapLayerSatelliteBtdOzone => 'ひまわり 対流圏界面'; + + @override + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } + + @override + String get notifySectionEarthquake => '地震'; + + @override + String get mapLayerDisasterMap => '防災マップ'; + + @override + String get weatherModeFog => '霧'; + + @override + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } + + @override + String get mapLayerStyleGrayTooltip => '気象庁の赤外画像の慣例:温度が低いほど白'; + + @override + String get moreAnnouncements => 'お知らせ'; + + @override + String get mapLayerSatelliteTransparentNoData => 'データなし(陸上) = 透明'; + + @override + String get restroomCategoryGovernment => '行政サービス施設'; + + @override + String get typhoonLegendCurrent => '現在中心'; + + @override + String get aedAddress => '住所'; + + @override + String get mapLayerAed => 'AED'; + + @override + String get changelogTypePrerelease => 'ベータ'; + + @override + String get reportFilterIntensityInfoModernBody => + '震度は 0–4、5弱、5強、6弱、6強、7。フィルタは新制に準拠し、それ以前の地震はリストで旧制表記になります。'; + + @override + String get typhoonOverlayWeatherNone => 'なし'; + + @override + String get mapLayerStyleGray => 'グレースケール(JMA)'; + + @override + String get weatherModeAuto => '自動'; + + @override + String get typhoonLabelProbCircle => '70%確率円'; + + @override + String get notifyOptAll => 'すべて受信'; + + @override + String get displayTheme => 'テーマ'; + + @override + String get mapLayerSatelliteB07 => 'ひまわり 短波長赤外(B07)'; + + @override + String get typhoonLabelDirection => 'これまでの進行方向'; + + @override + String get regionManageTitle => '登録地域'; + + @override + String get typhoonLegendCone => '予報円'; + + @override + String get moreCwaEew => '中央気象署 緊急地震速報'; + + @override + String get onboardingPermsTitle => '権限の許可'; + + @override + String get mapLayerStyleJma => '雲頂強調(JMA)'; + + @override + String get rainInterval10m => '10分'; + + @override + String weatherRankingAnalysisLow(String value) { + return '最低 $value'; + } + + @override + String get meshtasticConnectAnyway => 'Connect anyway'; + + @override + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => 'ひまわり 近赤外(B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => '低反射率・夜間 = 透明、地図が透ける'; + + @override + String chartHourLabel(int hour) { + return '$hour時'; + } + + @override + String get mapLayerShelter => '避難所'; + + @override + String get typhoonOverlayProbabilityTooltip => '接近確率を表示(予報円を隠す)'; + + @override + String get mapLayerSatelliteNdwi => 'ひまわり NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => '避難所を表示'; + + @override + String get mapNavHumidity => '湿度'; + + @override + String get reportDetailSortByIntensity => '震度順に並べ替え'; + + @override + String get homeRainTrendNoData => 'データなし'; + + @override + String get mapLayerCategoryRadar => 'レーダー'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => 'ひまわり エアマス'; + + @override + String get typhoonTrackDetail => '経路詳細'; + + @override + String get dataSectionWeather => '気象'; + + @override + String get aedHoursWeekday => '平日の開館時間'; + + @override + String get homeActiveEventsTitle => '発生中の事象'; + + @override + String weatherRankingAnalysisHigh(String value) { + return '最高 $value'; + } + + @override + String get faq => 'よくある質問'; @override - String get typhoonLabelGaleAvg => '強風域の平均半径'; + String get typhoonHistoryLive => '最新'; @override - String get typhoonLabelStormAvg => '暴風域の平均半径'; + String eewSerial(int serial) { + return '第 $serial 報'; + } @override - String get typhoonLabelProbCircle => '70%確率円'; + String get reportFilterSort => '並び替え'; @override - String typhoonForecastLead(String hours) { - return '予報 +$hours 時間'; - } + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; @override - String get typhoonLabelNw => '北西'; + String get dataEarthquakeSubtitle => '地震報告'; @override - String get typhoonLabelNe => '北東'; + String get typhoonNoActive => '発生中の台風なし'; @override - String get typhoonLabelSw => '南西'; + String get mapLayerSatelliteB11 => 'ひまわり 二酸化硫黄/雲相(B11)'; @override - String get typhoonLabelSe => '南東'; + String get navEvents => 'イベント'; @override - String typhoonValueLat(String lat) { - return '北緯 $lat 度'; - } + String get onboardingTermsTitle => 'サービス利用規約'; @override - String typhoonValueLon(String lon) { - return '東経 $lon 度'; - } + String get mapTownLabels => '郷鎮名'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => '設定を保存できませんでした。もう一度お試しください。'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '毎秒 $n m'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return '資料時刻\n$time'; - } + String get notifyAnnouncement => 'お知らせ'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'DPIP へようこそ'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => '現在地を取得できません'; @override - String get mapLayerMonitor => '強震モニタ'; + String get languageSystem => 'システムの既定'; @override - String get mapLayerDisasterMap => '防災マップ'; + String get skyTimeSunset => '日の入り'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'ひまわり 黄砂'; @override - String get disasterMapOverlayMenuTooltip => '防災マップのレイヤー'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => 'レイヤー'; + String get regionEdit => '変更'; @override - String get disasterMapOverlayAedTooltip => 'AEDの位置を表示'; + String get weatherDynamicState => '天気アニメーション'; @override - String get aedAddress => '住所'; + String get mapPlaceholderDisabled => '地図(一時的に無効)'; @override - String get aedRegion => '地域'; + String get moonNow => '現在'; @override - String get aedCategory => '分類'; + String get moonSectionAppearance => '見え方'; @override - String get aedType => '種類'; + String get moonSectionRiseSet => '月の出・月の入り'; @override - String get aedPlaceDesc => '設置場所'; + String get moonSectionUpcoming => '次の月相'; @override - String get aedDescription => '備考'; + String get moonSectionCalendar => '月齢カレンダー'; @override - String get aedHoursWeekday => '平日の開館時間'; + String get moonDistance => '距離'; @override - String get aedHoursSaturday => '土曜の開館時間'; + String get moonKilometres => 'km'; @override - String get aedHoursSunday => '日曜の開館時間'; + String get moonApparentSize => '視直径'; @override - String get aedOpenRemark => '開館時間メモ'; + String get moonRise => '月の出'; @override - String get aedEmergencyPhone => '緊急連絡先'; + String get moonSet => '月の入り'; @override - String get mapLayerRestroom => 'トイレ'; + String get moonNextNewMoon => '次の新月'; @override - String get mapLayerShelter => '避難所'; + String get moonAlwaysUp => '終日地平線上'; @override - String get disasterMapOverlayRestroomTooltip => 'トイレを表示'; + String get moonNoEvent => 'この日はなし'; @override - String get disasterMapOverlayShelterTooltip => '避難所を表示'; + String get sunTitle => '太陽'; @override - String get dpmOpenInMaps => '地図アプリで開く'; + String get sunSubtitle => '日の出・薄明・二十四節気'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => '日照'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => '薄明'; @override - String mapAppDefault(String app) { - return '$app(デフォルト)'; - } + String get sunSectionLight => '光'; @override - String get mapAppCopyCoordinates => '座標をコピー'; + String get sunSectionSundial => '日時計'; @override - String get mapAppCoordinatesCopied => '座標をコピーしました'; + String get sunSectionTerms => '二十四節気'; @override - String mapAppOpenFailed(String app) { - return '$app を開けませんでした'; - } + String get sunRise => '日の出'; @override - String get mapAppCallFailed => 'この端末では通話できません'; + String get sunSet => '日の入り'; @override - String get mapOverlaySectionReference => '参照レイヤー'; + String get sunNoon => '南中'; @override - String get mapLayerCategoryEarthquake => '地震'; + String get sunDayLength => '昼の長さ'; @override - String get mapLayerCategoryTyphoon => '台風'; + String get sunTwilightCivil => '市民'; @override - String get mapLayerCategoryWeather => '気象観測'; + String get sunTwilightNautical => '航海'; @override - String get mapLayerCategorySatellite => '衛星'; + String get sunTwilightAstronomical => '天文'; @override - String get mapLayerCategoryRadar => 'レーダー'; + String get sunGoldenHourMorning => '朝のゴールデンアワー'; @override - String get mapLayerCategoryLife => '生活'; + String get sunGoldenHourEvening => '夕のゴールデンアワー'; @override - String get mapLayerCategoryForecast => '数値予報'; + String get sunBlueHour => 'ブルーアワー'; @override - String get mapOverlaySectionMap => '地図'; + String get sunEquationOfTime => '均時差'; @override - String get rainIntervalSection => '集計時間'; + String get sunMinutes => '分'; @override - String get mapTownLabels => '郷鎮名'; + String get solarTermNext => '次の節気'; @override - String get mapTownLabelsHint => '拡大すると郷鎮名を表示'; + String get planetsTitle => '惑星'; @override - String get mapTerrainRelief => '地形の立体感'; + String get planetsSubtitle => '今夜の位置と明るさ'; @override - String get mapTerrainReliefHint => 'ベースマップに地形の陰影を表示'; + String get planetsSectionTonight => '現在'; @override - String get dpmSheetEmpty => '地図上のマーカーをタップして詳細を表示'; + String get planetUp => '地平線上'; @override - String get dpmAddress => '住所'; + String get planetDown => '地平線下'; @override - String get restroomTypeLabel => '種別'; + String get planetInGlare => '太陽に近い'; @override - String get restroomCategoryLabel => '区分'; + String get planetMagnitude => '等級'; @override - String get restroomGradeLabel => '等級'; + String get planetElongation => '離角'; @override - String get restroomTypeFemale => '女性用トイレ'; + String get planetSky => '時間帯'; @override - String get restroomTypeMale => '男性用トイレ'; + String get planetEvening => '宵の明星'; @override - String get restroomTypeMixed => '男女共用トイレ'; + String get planetMorning => '明けの明星'; @override - String get restroomTypeAccessible => 'バリアフリートイレ'; + String get planetDistance => '距離'; @override - String get restroomTypeGenderNeutral => 'ジェンダーニュートラルトイレ'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => '親子トイレ'; + String get planetAltitude => '高度'; @override - String get restroomTypeUnspecified => '未設定'; + String get planetMercury => '水星'; @override - String get restroomCategoryTransport => '交通'; + String get planetVenus => '金星'; @override - String get restroomCategoryPark => '公園'; + String get planetMars => '火星'; @override - String get restroomCategoryCommercial => '商業・営業施設'; + String get planetJupiter => '木星'; @override - String get restroomCategoryReligious => '宗教・礼拝施設'; + String get planetSaturn => '土星'; @override - String get restroomCategoryCultural => '文化・娯楽施設'; + String get planetUranus => '天王星'; @override - String get restroomCategoryGovernment => '行政サービス施設'; + String get planetNeptune => '海王星'; @override - String get restroomCategoryWelfare => '社会福祉施設・集会所'; + String get solarTermVernalEquinox => '春分'; @override - String get restroomCategoryTourist => '観光地・景勝地'; + String get solarTermPureBrightness => '清明'; @override - String get restroomCategoryLeisure => 'レジャー・娯楽施設'; + String get solarTermGrainRain => '穀雨'; @override - String get restroomCategoryOther => 'その他'; + String get solarTermStartOfSummer => '立夏'; @override - String get restroomGradeExcellent => '最上級'; + String get solarTermGrainFull => '小満'; @override - String get restroomGradeGood => '優良'; + String get solarTermGrainInEar => '芒種'; @override - String get restroomGradeAverage => '普通'; + String get solarTermSummerSolstice => '夏至'; @override - String get restroomGradePoor => '不合格'; + String get solarTermMinorHeat => '小暑'; @override - String get shelterAddressLabel => '住所'; + String get solarTermMajorHeat => '大暑'; @override - String get shelterCapacityLabel => '収容人数'; + String get solarTermStartOfAutumn => '立秋'; @override - String shelterCapacityValue(int n) { - return '$n 人'; - } + String get solarTermEndOfHeat => '処暑'; @override - String get shelterCategoryLabel => '対象災害'; + String get solarTermWhiteDew => '白露'; @override - String get shelterIndoorLabel => '屋内収容'; + String get solarTermAutumnalEquinox => '秋分'; @override - String get shelterOutdoorLabel => '屋外収容'; + String get solarTermColdDew => '寒露'; @override - String get shelterVulnerableOkLabel => '要配慮者向け収容'; + String get solarTermFrostDescent => '霜降'; @override - String get dpmYes => 'はい'; + String get solarTermStartOfWinter => '立冬'; @override - String get dpmNo => 'いいえ'; + String get solarTermMinorSnow => '小雪'; @override - String get stationSheetEmpty => '観測点をタップして値を表示'; + String get solarTermMajorSnow => '大雪'; @override - String monitorDelay(String value) { - return '遅延 $value s'; - } + String get solarTermWinterSolstice => '冬至'; @override - String get monitorWaiting => 'データ待機中…'; + String get solarTermMinorCold => '小寒'; @override - String mapLegendUnit(String unit) { - return '単位:$unit'; - } + String get solarTermMajorCold => '大寒'; @override - String get typhoonLegendPast => '実況経路'; + String get solarTermStartOfSpring => '立春'; @override - String get typhoonIntensityTd => '熱帯低気圧'; + String get solarTermRainWater => '雨水'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => '啓蟄'; @override - String typhoonPickerTd(String no) { - return '熱帯低気圧 TD $no'; - } + String get tonightTitle => '今夜'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => '何が見えるか、いつ見えるか'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => '観測ウィンドウ'; @override - String get typhoonIntensityMild => '弱い台風'; + String get tonightAstronomicalNight => '天文薄明終了'; @override - String get typhoonIntensityModerate => '並の台風'; + String get tonightNeverDark => '完全に暗くならない'; @override - String get typhoonIntensityIntense => '強い台風'; + String get tonightDarkWindow => '暗夜の時間帯'; @override - String get typhoonLegendForecast => '予報経路'; + String get tonightMoonAllNight => '月が一晩中出ている'; @override - String get typhoonLegendForecastPoint => '予報点'; + String get tonightDarkTotal => '暗夜合計'; @override - String get typhoonLegendCurrent => '現在中心'; + String get tonightMoonlight => '月明かり'; @override - String get typhoonLegendCone => '予報円'; + String get tonightSectionShowers => '流星群'; @override - String get mapLegendExpand => '凡例'; + String get tonightRadiantDown => '放射点が昇らない'; @override - String get mapLegendCollapse => '凡例を閉じる'; + String get tonightPerHour => '個/時'; @override - String get mapMyLocation => '現在地'; + String get tonightSectionSatellites => '衛星の通過'; @override - String get mapResetNorth => '北を上にする'; + String get tonightSectionTargets => '今見られる天体'; @override - String get typhoonLegendCircle15 => '強風域(30kt)'; + String get showerQuadrantids => 'しぶんぎ座'; @override - String get typhoonLegendCircleAvg => '平均円'; + String get showerLyrids => 'こと座'; @override - String get typhoonLegendCircle25 => '暴風域(50kt)'; + String get showerEtaAquariids => 'みずがめ座η'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => 'みずがめ座δ'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => 'ペルセウス座'; @override - String get typhoonLegendProbability => '接近確率'; + String get showerOrionids => 'オリオン座'; @override - String get typhoonLegendWarningAreas => '警報区域'; + String get showerSouthernTaurids => 'おうし座南'; @override - String get typhoonOverlayMenuTooltip => '台風オーバーレイ設定'; + String get showerLeonids => 'しし座'; @override - String get typhoonOverlaySectionStorm => '暴風域'; + String get showerGeminids => 'ふたご座'; @override - String get typhoonOverlaySectionExtra => 'オーバーレイ'; + String get showerUrsids => 'こぐま座'; @override - String get typhoonOverlayStormBandSubtitle => '平均円付き'; + String get deepSkyOpenCluster => '散開星団'; @override - String get typhoonOverlayProbabilityHint => '予報円を隠します'; + String get deepSkyGlobularCluster => '球状星団'; @override - String get typhoonOverlayProbabilityTooltip => '接近確率を表示(予報円を隠す)'; + String get deepSkySpiralGalaxy => '渦巻銀河'; @override - String get typhoonOverlayWarningTooltip => '台風警報対象の県を強調'; + String get deepSkyEllipticalGalaxy => '楕円銀河'; @override - String get typhoonOverlayStormL7Tooltip => '強風域 + 平均円(紫)'; + String get deepSkyIrregularGalaxy => '不規則銀河'; @override - String get typhoonOverlayStormL10Tooltip => '暴風域 + 平均円(黄)'; + String get deepSkyPlanetaryNebula => '惑星状星雲'; @override - String get typhoonOverlaySectionWeather => '天気下敷き'; + String get deepSkySupernovaRemnant => '超新星残骸'; @override - String get typhoonOverlayWeatherNone => 'なし'; + String get deepSkyEmissionNebula => '散光星雲'; @override - String get typhoonOverlayWeatherHint => '通報時刻に合わせる'; + String get deepSkyReflectionNebula => '反射星雲'; @override - String get typhoonOverlayWeatherNoneTooltip => 'レーダー/赤外線なし'; + String get deepSkyAsterism => 'アステリズム'; @override - String get typhoonOverlayWeatherRadarTooltip => '通報時刻に最も近いレーダー'; + String get almanacTitle => '暦'; @override - String get typhoonOverlayWeatherSatelliteTooltip => '通報時刻に最も近い赤外線'; + String get almanacSubtitle => '旧暦と今後の日食・月食'; @override - String get typhoonWarningTitle => '台風警報'; + String get almanacSectionToday => '今日'; @override - String typhoonWarningAreas(String areas) { - return '対象地域:$areas'; - } + String get almanacGregorian => '西暦'; @override - String get typhoonTrackDetail => '経路詳細'; + String get almanacLunar => '旧暦'; @override - String get typhoonHistoryTitle => '資料時刻'; + String get almanacYear => '歳次'; @override - String get typhoonHistoryLive => '最新'; + String get almanacMonthLength => '月の大小'; @override - String get typhoonSatelliteTitle => '衛星'; + String get almanacLongMonth => '30日'; @override - String get typhoonOverlayForecastCallouts => '予報点の情報'; + String get almanacShortMonth => '29日'; @override - String get typhoonOverlayForecastCalloutsTooltip => '拡大時に予報点の詳細カードを表示'; + String get almanacLeapPrefix => '閏'; @override - String get dpmFilterSectionRestroom => '施設の種類'; + String get almanacSectionLunarEclipses => '月食'; @override - String get dpmFilterSectionRestroomType => 'トイレの種類'; + String get almanacSectionSolarEclipses => '日食'; @override - String get dpmFilterSectionShelter => '避難所の災害種別'; + String get almanacNoSolarEclipse => '範囲内になし'; @override - String get dpmDisasterFlood => '洪水'; + String get eclipseTotal => '皆既'; @override - String get dpmDisasterEarthquake => '震災'; + String get eclipsePartial => '部分'; @override - String get dpmDisasterLandslide => '土石流'; + String get eclipseAnnular => '金環'; @override - String get dpmDisasterTsunami => '津波'; + String get eclipsePenumbral => '半影'; @override - String get dpmDisasterSlope => '斜面災害'; + String get zodiacRat => '子'; @override - String get dpmDisasterNuclear => '原子力事故'; + String get zodiacOx => '丑'; @override - String get skyTime => '空の時刻'; + String get zodiacTiger => '寅'; @override - String get skyTimeAuto => '自動'; + String get zodiacRabbit => '卯'; @override - String get skyTimeDawn => '夜明け前'; + String get zodiacDragon => '辰'; @override - String get skyTimeSunrise => '日の出'; + String get zodiacSnake => '巳'; @override - String get skyTimeMorning => '午前'; + String get zodiacHorse => '午'; @override - String get skyTimeNoon => '正午'; + String get zodiacGoat => '未'; @override - String get skyTimeAfternoon => '午後'; + String get zodiacMonkey => '申'; @override - String get skyTimeGolden => 'ゴールデンアワー'; + String get zodiacRooster => '酉'; @override - String get skyTimeSunset => '日の入り'; + String get zodiacDog => '戌'; @override - String get skyTimeDusk => '薄暮'; + String get zodiacPig => '亥'; @override - String get skyTimeNight => '夜'; + String get tideTitle => '潮汐'; @override - String get weatherModeCloudy => '曇り'; + String get tideSubtitle => '大潮・小潮と月の引力'; @override - String get weatherModeOvercast => '本曇り'; + String get tideDisclaimer => '天文起潮力のみで、港湾の潮汐表ではありません。潮位は気象庁の公表値をご覧ください。'; @override - String get weatherModeSnow => '雪'; + String get tideSectionNow => '現在'; @override - String get weatherModeSand => '砂じん'; + String get tidePhase => '周期'; @override - String get radarScanRange => '走査範囲を表示'; + String get tideSpring => '大潮'; @override - String get radarScanRangeSubtitle => '4基のレーダーが実際に観測する範囲を示します。'; + String get tideNeap => '小潮'; @override - String get radarScanRangeHint => '枠外の空白は未観測'; + String get tideMiddling => '中潮'; @override - String get radarOverlayMenuTooltip => 'レーダーレイヤー設定'; + String get tideLunarDistanceFactor => '月の引力'; @override - String get radarCountyOutline => '県市境界'; + String get tideEquilibrium => '平衡潮位'; @override - String get radarGlobalOutline => '国境線'; + String get tideMetres => 'm'; @override - String get radarGlobalOutlineHint => '各国の国境外枠'; + String get tidePerigeanSpring => '次の近地点大潮'; @override - String get radarCountyOutlineHint => 'エコーの上に描画'; + String get tideSectionTurningPoints => '転換点'; @override - String get radarCountyOutlineSubtitle => 'レーダーエコーの下でも県市境界が見えるようにします。'; + String get tideHigh => '高'; @override - String get radarTownOutline => '市町村境界'; + String get tideLow => '低'; @override - String get radarTownOutlineHint => 'より細かい区分'; + String get skyChartTitle => '星図'; @override - String get radarTownOutlineSubtitle => 'レーダーエコーの下でも市町村境界が見えるようにします。'; + String get skyChartSubtitle => '頭上の肉眼で見える空'; @override - String get qpesumsOverlayMenuTooltip => '定量降水予報レイヤー設定'; + String get skyChartNorth => '北'; @override - String get windForecastOverlayMenuTooltip => '風予報レイヤー設定'; + String get skyChartEast => '東'; @override - String get windForecastCountyOutlineHint => '風場の上に描画'; + String get skyChartSouth => '南'; @override - String get windForecastGlobalOutlineHint => '各国の国境外枠'; + String get skyChartWest => '西'; @override - String get windForecastTownOutlineHint => 'より細かいメッシュ'; + String tonightElementAge(int days) { + return '軌道要素 $days 日前'; + } @override - String eewSerial(int serial) { - return '第 $serial 報'; + String almanacLunarDate(String leap, int month, int day) { + return '$leap$month 月 $day 日'; } @override - String get eewMaxIntensity => '最大震度'; + String get tonightNoShowers => '流星群なし'; @override - String get eewLocalIntensity => '現在地の推定'; + String get tonightNoPasses => '48 時間以内に可視通過なし'; @override - String get eewSWave => 'S波'; + String get tonightSatellitesUnavailable => '軌道データを読み込めません'; @override - String get eewArrived => '到達'; + String get tonightNoTargets => '十分な高度の天体なし'; @override - String eewCountdown(int seconds) { - return 'あと $seconds 秒'; - } + String get skyChartUnavailable => '星表を読み込めません'; } diff --git a/lib/l10n/gen/app_localizations_ko.dart b/lib/l10n/gen/app_localizations_ko.dart index dbbcdfc88..10bb3fb09 100644 --- a/lib/l10n/gen/app_localizations_ko.dart +++ b/lib/l10n/gen/app_localizations_ko.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,508 +10,498 @@ class AppLocalizationsKo extends AppLocalizations { AppLocalizationsKo([String locale = 'ko']) : super(locale); @override - String get languageName => '한국어'; + String typhoonValueLat(String lat) { + return '$lat°N'; + } @override - String get navHome => '홈'; + String get onboardingSkipBody => + '위치 및 알림 권한이 없으면 DPIP가 주변의 지진과 재난을 실시간으로 알려드릴 수 없습니다. 나중에 설정에서 권한을 허용할 수 있습니다.'; @override - String get navEvents => '이벤트'; + String get rainInterval24h => '24시간'; @override - String get navMap => '지도'; + String homeRainTrendHeavyStopping(int minutes) { + return '$minutes분 후에 강한 비가 그칠 것으로 예상돼요'; + } @override - String get navData => '자료'; + String get mapTimelineObserved => '관측'; @override - String get navEarthquake => '지진'; + String get regionSelectTitle => '지역 선택'; @override - String get dataSectionSeismic => '지진'; + String get skyTimeNoon => '정오'; @override - String get dataEarthquakeSubtitle => '지진 보고서'; + String get radarCountyOutlineSubtitle => '레이더 에코 아래에서도 경계가 보이도록 합니다.'; @override - String get dataSectionWeather => '기상'; + String get dpmFilterSectionRestroomType => '화장실 유형'; @override - String get dataWeatherRankingSubtitle => '실시간 관측 순위'; + String get mapLayerSatelliteB03 => '히마와리 가시 적색(B03)'; @override - String get weatherRankingTitle => '관측 순위'; + String get reportFilterIntensity => '진도'; @override - String weatherRankingMeta(String time, int count) { - return '자료 시각: $time\n관측점 $count'; - } + String get mapLayerLightning => '번개'; @override - String get weatherRankingEmpty => '정렬할 관측이 없습니다'; + String get restroomTypeMale => '남자 화장실'; @override - String get weatherRankingBy => '정렬'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => '최고'; + String get reportDetailSortByCounty => '지역순 정렬'; @override - String get weatherRankingLowest => '최저'; + String get homeRainTrendScattered => '약한 비가 올 수 있어요'; @override - String get weatherRankingMergeTo => '병합'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => '향진'; + String get weatherRankingTempExtremes => '기온 극값'; @override - String get weatherRankingMergeCounty => '현시'; + String get themeLight => '라이트'; @override - String get weatherRankingWind => '풍속'; + String get mapTerrainReliefHint => '기본 지도에 지형 음영 표시'; @override - String get weatherRankingGust => '돌풍'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => '기온 극값'; + String get moreSectionRegion => '지역'; @override - String get weatherRankingExtremeHigh => '오늘 최고'; + String get dpmDisasterEarthquake => '지진'; @override - String get weatherRankingExtremeLow => '오늘 최저'; + String get mapLayerSatellite => '히마와리 적외(B13)'; @override - String get weatherRankingExtremeRange => '일교차'; + String get aedHoursSaturday => '토요일 운영시간'; @override - String weatherRankingRecordedAt(String time) { - return '기록 시각 $time'; - } + String get dpmDisasterSlope => '사면 재해'; @override - String weatherRankingAnalysisCurrent(String value) { - return '현재 $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return '최고 $value'; - } + String get notifySectionEew => '지진 조기경보'; @override - String weatherRankingAnalysisLow(String value) { - return '최저 $value'; - } + String get mapResetNorth => '북쪽으로 되돌리기'; @override - String weatherRankingAnalysisRange(String value) { - return '일교차 $value°C'; - } + String get rainInterval2d => '2일'; @override - String get reportListEmpty => '지진 보고서가 없습니다'; + String get mapTownLabelsHint => '확대하면 읍면동 이름 표시'; @override - String get reportListEmptyFiltered => '조건에 맞는 지진 보고서가 없습니다'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => '지진해일 경보만'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => '히마와리 야간 안개'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => '고급'; @override - String get reportListLocalFelt => '소규모 유감'; + String get weatherRankingExtremeRange => '일교차'; @override - String get reportListToday => '오늘'; + String get notifySettingsMenu => '알림 설정'; @override - String get reportListYesterday => '어제'; + String get typhoonHistoryTitle => '자료 시각'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app (기본)'; } @override - String get reportListEnd => '마지막입니다'; + String get trendRange24h => '24시간'; @override - String get reportFilterTitle => '필터'; + String get mapLayerStyleJmaTooltip => '그레이스케일 바탕에 −40 °C 이하를 채색, 운정 고도 강조'; @override - String get reportFilterSort => '정렬'; + String weatherRankingRecordedAt(String time) { + return '기록 시각 $time'; + } @override - String get reportFilterSortTime => '시간'; + String get mapLayerRain => '강수량'; @override - String get reportFilterSortIntensity => '진도'; + String get mapLayerQpesums => '1시간 강수 예보'; @override - String get reportFilterSortMagnitude => '규모'; + String get mapOverlaySectionMap => '지도'; @override - String get reportFilterSortDepth => '깊이'; + String get mapTerrainRelief => '지형 입체감'; @override - String get reportFilterOrderDesc => '내림차순'; + String get eewMaxIntensity => '최대 진도'; @override - String get reportFilterOrderAsc => '오름차순'; + String get mapLegendCollapse => '범례 숨기기'; @override - String get reportFilterIntensity => '진도'; + String get changelogTitle => '변경 로그'; @override - String get reportFilterIntensityInfoTitle => '진도 신제·구제'; + String get reportFilterOrderDesc => '내림차순'; @override - String get reportFilterIntensityInfoIntro => - '기상서는 2020년 1월 1일(타이베이 시간)부터 신제 진도를 사용합니다.'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyTitle => '구제(2020년 이전)'; + String get reportFilterIntensityInfoTitle => '진도 신제·구제'; @override - String get reportFilterIntensityInfoLegacyBody => - '진도는 0–7만 있으며 5약/5강/6약/6강 구분이 없습니다.'; + String get mapLayerTyphoon => '태풍'; @override - String get reportFilterIntensityInfoModernTitle => '신제(2020년 이후)'; + String get radarOverlayMenuTooltip => '레이더 레이어 옵션'; @override - String get reportFilterIntensityInfoModernBody => - '진도는 0–4, 5약, 5강, 6약, 6강, 7입니다. 필터는 신제를 따르며, 이전 지진은 목록에서 구제 표기로 표시됩니다.'; + String get mapMyLocation => '내 위치'; @override - String get reportFilterMagnitude => '규모'; + String get meshtasticNodes => 'Nodes'; @override - String get reportFilterDepth => '깊이'; + String get meshtasticSend => 'Send'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get typhoonOverlayStormL7Tooltip => + 'Level-7 wind field + average circle (purple)'; @override - String get reportFilterDate => '날짜'; + String get aedType => '유형'; @override - String get reportFilterDatePick => '날짜 선택'; + String get termsOfService => '서비스 약관'; @override - String get reportFilterDateStartNote => '시작일: 당일 00:00(타이베이)'; + String get typhoonLegendCircle25 => '폭풍권 (10급)'; @override - String get reportFilterDateEndNote => '종료일: 당일 24:00(타이베이)'; + String get sponsorTitle => 'DPIP 후원하기'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; + String get mapNavSatellite => '위성'; + + @override + String homeRainTrendUpdated(String time) { + return '업데이트 $time'; } @override - String get reportFilterLocation => '위치'; + String get onboardingNext => '다음'; @override - String get reportFilterLocationHint => '예: 화롄, 해역'; + String get weatherRankingMergeTown => '향진'; @override - String get reportFilterAny => '전체'; + String get mapLayerMonitor => '실시간 지진 모니터'; @override - String get reportFilterApply => '적용'; + String get moreYoutube => 'YouTube'; @override - String get reportFilterReset => '초기화'; + String get sponsorSubscriptions => '구독'; @override - String get reportListSearch => '조회'; + String typhoonValueLon(String lon) { + return '$lon°E'; + } @override - String get reportDetailTitle => '지진 보고서'; + String get skyTime => '하늘 시각'; @override - String reportDetailNumbered(String number) { - return '번호 $number 유의미 유감지진'; - } + String get weatherModeCloudy => '구름 많음'; @override - String get reportDetailLocalFelt => '국지적 유감지진'; + String get skyTimeDusk => '땅거미'; @override - String get reportDetailInfo => '상세 정보'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailOriginTime => '발생 시각'; + String get reportFilterDateEndNote => '종료일: 당일 24:00(타이베이)'; @override - String get reportDetailEpicenter => '진앙 좌표'; + String get reportFilterSortMagnitude => '규모'; @override - String get reportDetailMagnitude => '지진 규모'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailDepth => '진원 깊이'; + String get mapLayerCategoryEarthquake => '지진'; @override - String get reportDetailAreaIntensity => '지역별 진도'; + String get mapLayerSatelliteB12 => '히마와리 오존(B12)'; @override - String get reportDetailLocalIntensity => '내 위치의 진도'; + String get typhoonLegendPast => '실황 경로'; @override - String get reportDetailLocalIntensityUnavailable => '진도 정보 없음'; + String get restroomCategoryOther => '기타'; @override - String get reportDetailSortByIntensity => '진도순 정렬'; + String homeForecastHighLow(String high, String low) { + return '최고 $high° · 최저 $low°'; + } @override - String get reportDetailSortByCounty => '지역순 정렬'; + String get locationBannerFix => '설정 열기'; @override - String get reportDetailImage => '지진 보고서 이미지'; + String get mapLegendExpand => '범례'; @override - String get reportDetailImageUnavailable => '보고서 이미지가 아직 없습니다'; + String get eewNone => '현재 지진 조기경보가 없습니다'; @override - String get reportDetailOpenReport => '보고서 페이지'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get reportDetailReplay => '다시 보기'; + String get notifyOptTsunamiAll => '지진해일 주의보 및 경보'; @override - String get navMore => '더보기'; + String get meshtasticLayerOptions => 'Node options'; @override - String get appLogs => '앱 로그'; + String get onboardingAgreeContinue => '동의하고 계속'; @override - String get changelogTitle => '변경 로그'; + String get commonRetry => '다시 시도'; @override - String get changelogEmpty => '아직 릴리스 노트가 없습니다'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogTypePrerelease => '베타'; + String reportDetailNumbered(String number) { + return '번호 $number 유의미 유감지진'; + } @override - String get changelogTypeStable => '정식'; + String get typhoonOverlayStormBandSubtitle => 'With average circle'; @override - String get changelogCurrentVersion => '현재'; + String get disasterMapOverlayRestroomTooltip => '공중화장실 표시'; @override - String get changelogVersionDetails => '릴리스 상세'; + String get weatherRankingTitle => '관측 순위'; @override - String get changelogBodyEmpty => '이 릴리스에 대한 설명이 없습니다.'; + String get homeRainTrendHeavySustained => '앞으로 1시간 동안 강한 비가 이어질 거예요'; @override - String get mapPlaceholderDisabled => '지도 (일시 사용 중지)'; + String get notifySectionTsunami => '지진해일'; @override - String get moreSectionRegion => '지역'; + String get restroomCategoryPark => '공원'; @override - String get moreSectionNotify => '알림'; + String get moreLinkOpenFailed => '링크를 열 수 없습니다'; @override - String get moreSectionDisplay => '표시'; + String get themeDark => '다크'; @override - String get regionManageTitle => '저장한 지역'; + String get sponsorRestore => '구매 복원'; @override - String get regionAddButton => '지역 추가'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String get regionEmpty => '저장된 지역이 없습니다'; + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String get regionSelectTitle => '지역 선택'; + String get meshtasticTraffic => 'Traffic'; @override - String regionSelectCount(int count, int max) { - return '$count/$max 선택됨'; - } + String get mapLayerStyleBdTooltip => 'Dvorak BD 커브——열대저기압 강도 분석용 계단 그레이스케일'; @override - String regionSelectFull(int max) { - return '최대 $max개 지역까지 저장할 수 있습니다'; - } + String get disasterMapOverlayAedTooltip => 'AED 위치 표시'; @override - String get regionEdit => '수정'; + String get mapLayerHumidity => '습도'; @override - String get moreSectionAdvanced => '고급'; + String get mapLayerSatelliteTransparentNight => '야간 = 투명,배경 지도 표시'; @override - String get moreDeveloper => '디버그 정보'; + String get meshtasticScanning => 'Scanning…'; @override - String get experimentalFeatures => '실험적 기능'; + String regionSelectFull(int max) { + return '최대 $max개 지역까지 저장할 수 있습니다'; + } @override - String get moreSectionLinks => '링크'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreCwaEew => '중앙기상청(CWA) 지진 조기경보'; + String get navMore => '더보기'; @override - String get moreTremReport => 'TREM 탐지 보고'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreServerStatus => '서버 상태'; + String get disasterMapOverlaySectionLayers => '레이어'; @override - String get moreAnnouncements => '공지사항'; + String get mapLayerSatelliteB05 => '히마와리 근적외(B05)'; @override - String get moreDiscord => 'Discord 커뮤니티'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get moreNotifyLog => 'DPIP 알림 발송 기록'; + String get typhoonLabelNe => 'NE'; @override - String get moreLinkOpenFailed => '링크를 열 수 없습니다'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherDynamicState => '날씨 애니메이션'; + String get reportListEmpty => '지진 보고서가 없습니다'; @override - String get weatherDynamicStateSubtitle => '홈 배경 날씨를 재정의합니다'; + String get reportListEnd => '마지막입니다'; @override - String get weatherModeAuto => '자동'; + String get mapLayerSatelliteTruecolor => '히마와리 트루컬러'; @override - String get weatherModeClear => '맑음'; + String get typhoonOverlaySectionExtra => 'Overlays'; @override - String get weatherModeRain => '비'; + String get eewSWave => 'S파'; @override - String get weatherModeFog => '안개'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get weatherModeThunderstorm => '뇌우'; + String get restroomCategoryCultural => '문화·여가 시설'; @override - String get commonLoading => '불러오는 중…'; + String get typhoonLabelWind => 'Max. sustained wind near centre'; @override - String get commonRetry => '다시 시도'; + String get radarGlobalOutlineHint => '각국 국경선'; @override - String get commonError => '문제가 발생했습니다'; + String get notifyEvacuation => '재난 정보'; @override - String get commonFetchFailed => '데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.'; + String get typhoonLegendCircle15 => '강풍권 (7급)'; @override - String get commonEmpty => '표시할 내용이 없습니다'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedConnecting => '연결 중…'; + String get homeRainTrendLightSustained => '앞으로 1시간 동안 약한 비가 이어질 거예요'; @override - String get feedStale => '데이터가 오래되었을 수 있습니다'; + String get commonError => '문제가 발생했습니다'; @override - String get feedOffline => '연결이 끊어졌습니다'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String get eewTitle => '지진 조기경보'; + String get meshtasticPower => 'Power'; @override - String get eewNone => '현재 지진 조기경보가 없습니다'; + String get mapTimelineNow => '현재'; @override - String eewSummary(String magnitude, String depth) { - return '규모 $magnitude · 깊이 $depth km'; + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => '전국'; + String get reportDetailOpenReport => '보고서 페이지'; @override - String get regionCurrent => '현재 위치'; + String get trendRange7d => '7일'; @override - String get regionCurrentUnavailable => '현재 위치를 가져올 수 없습니다'; + String typhoonWarningAreas(String areas) { + return '대상 지역: $areas'; + } @override - String get weatherPrecipitation => '강수량'; + String get rainIntervalSection => '집계 시간'; @override - String get weatherHumidity => '습도'; + String get notifyTitle => '알림'; @override - String weatherDataTime(String station, String time) { - return '$station · 데이터 시간 $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => '지도에서 보기'; + String get restroomCategoryLabel => '구분'; @override - String get homeForecastTitle => '24시간 예보'; + String get sponsorRestoring => '구매를 복원하는 중…'; @override - String homeForecastHighLow(String high, String low) { - return '최고 $high° · 최저 $low°'; - } + String get sponsorIntro => + 'DPIP는 실시간 재난 예방 정보를 제공하는 데 전념하며, 광고나 다른 수익 모델이 없습니다. 여러분의 후원은 서버 운영과 지속적인 개발에 도움이 됩니다.'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => '주소'; @override - String homeForecastFeelsLike(String temp) { - return '체감 $temp°'; - } + String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; @override - String homeForecastHumidity(String value) { - return '습도 $value%'; - } + String get restroomCategoryCommercial => '상업·영업 시설'; @override - String homeForecastWind(String direction, String level) { - return '$direction · 풍력 $level'; - } + String get aedRegion => '지역'; @override - String get homeForecastUnavailable => '지역을 선택하면 예보를 볼 수 있습니다'; + String homeRainTrendLightStopping(int minutes) { + return '$minutes분 후에 비가 그칠 것으로 예상돼요'; + } @override - String get homeForecastEmpty => '예보 데이터가 없습니다'; + String get reportDetailInfo => '상세 정보'; @override - String get homeActiveEventsTitle => '발효 중 이벤트'; + String get mapNavWind => '풍향'; @override - String get homeActiveEventsEmpty => '발효 중인 이벤트가 없습니다'; + String get windForecastOverlayMenuTooltip => '바람 예보 레이어 옵션'; @override - String get homeRainTrendTitle => '향후 1시간 강수'; + String get dataWeatherRankingSubtitle => '실시간 관측 순위'; @override String homeRainTrendMinute(int minute) { @@ -518,1292 +509,2104 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return '업데이트 $time'; - } + String get rainInterval6h => '6시간'; @override - String get homeRainTrendNoData => '데이터 없음'; + String get restroomTypeUnspecified => '미설정'; @override - String get homeRainTrendScattered => '약한 비가 올 수 있어요'; + String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; @override - String get homeRainTrendLightSustained => '앞으로 1시간 동안 약한 비가 이어질 거예요'; + String get mapLayerSatelliteGlobalOutline => '국경선'; @override - String homeRainTrendLightStopping(int minutes) { - return '$minutes분 후에 비가 그칠 것으로 예상돼요'; - } + String get mapNavTemperature => '온도'; @override - String get homeRainTrendHeavySustained => '앞으로 1시간 동안 강한 비가 이어질 거예요'; + String get typhoonLegendForecastPoint => '예보 지점'; @override - String homeRainTrendHeavyStopping(int minutes) { - return '$minutes분 후에 강한 비가 그칠 것으로 예상돼요'; - } + String get reportListYesterday => '어제'; @override - String get mapLayers => '레이어'; + String get moreSectionLinks => '링크'; @override - String get mapLayerOrderTitle => '레이어 순서'; + String get feedOffline => '연결이 끊어졌습니다'; @override - String get mapLayerOrderReset => '기본 순서로 재설정'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => '레이더 합성 에코'; + String get moreSectionDisplay => '표시'; @override - String get mapLayerSatellite => '히마와리 적외(B13)'; + String get rainInterval3d => '3일'; @override - String get mapLayerSatelliteB01 => '히마와리 가시 청색(B01)'; + String get defaultMapLayerSubtitle => + '지도 탭을 열 때 표시할 레이어입니다. 하단 탐색 아이콘과 라벨도 함께 바뀝니다.'; @override - String get mapLayerSatelliteB02 => '히마와리 가시 녹색(B02)'; + String get aedDescription => '비고'; @override - String get mapLayerSatelliteB03 => '히마와리 가시 적색(B03)'; + String get typhoonOverlayWeatherRadarTooltip => + 'Radar echo closest to the typhoon bulletin time'; @override - String get mapLayerSatelliteB04 => '히마와리 근적외(B04)'; + String get onboardingPermLocationDesc => '현재 위치에 맞춰 경보를 전달합니다.'; @override - String get mapLayerSatelliteB05 => '히마와리 근적외(B05)'; + String get mapLayerSatelliteB16 => '히마와리 이산화탄소(B16)'; @override - String get mapLayerSatelliteB06 => '히마와리 근적외(B06)'; + String get homeActiveEventsEmpty => '발효 중인 이벤트가 없습니다'; @override - String get mapLayerSatelliteB07 => '히마와리 단파 적외(B07)'; + String get typhoonLabelPosition => 'Centre location'; @override - String get mapLayerSatelliteB08 => '히마와리 상층 수증기(B08)'; + String get weatherRankingBy => '정렬'; @override - String get mapLayerSatelliteB09 => '히마와리 중층 수증기(B09)'; + String get typhoonIntensityMild => 'Mild typhoon'; @override - String get mapLayerSatelliteB10 => '히마와리 하층 수증기(B10)'; + String get windForecastGlobalOutlineHint => '각국 국경선'; @override - String get mapLayerSatelliteB11 => '히마와리 이산화황/구름상(B11)'; + String get rainInterval1h => '1시간'; @override - String get mapLayerSatelliteB12 => '히마와리 오존(B12)'; + String get eewLocalIntensity => '현재 위치 예상'; @override - String get mapLayerSatelliteB13 => '히마와리 적외(B13)'; + String get mapLayerRadar => '레이더 합성 에코'; @override - String get mapLayerSatelliteB14 => '히마와리 장파 적외(B14)'; + String get restroomCategoryReligious => '종교·의례 시설'; @override - String get mapLayerSatelliteB15 => '히마와리 장파 적외(B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => '히마와리 이산화탄소(B16)'; + String get mapLayerSatelliteCloudCloudy => '구름'; @override - String get mapLayerSatelliteTruecolor => '히마와리 트루컬러'; + String get skyTimeSunrise => '일출'; @override - String get mapLayerSatelliteNaturalcolor => '히마와리 내추럴컬러'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => '히마와리 화산재'; + String get onboardingPermNotifyDesc => '지진, 날씨, 재해가 발생하는 즉시 경보를 전달합니다.'; @override - String get mapLayerSatelliteDust => '히마와리 황사'; + String get radarTownOutline => '읍·면·동 경계'; @override - String get mapLayerSatelliteAirmass => '히마와리 에어매스'; + String get mapLayerStyleSection => '색상 스타일'; @override - String get mapLayerSatelliteNightmicrophysics => '히마와리 야간 미세물리'; + String get disasterMapOverlayMenuTooltip => '방재 지도 레이어'; @override - String get mapLayerSatelliteWatervapor => '히마와리 수증기'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => '히마와리 스플릿 윈도우'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => '히마와리 야간 안개'; + String get typhoonLabelSw => 'SW'; @override - String get mapLayerSatelliteBtdWvirw => '히마와리 오버슈팅 탑'; + String typhoonForecastLead(String hours) { + return 'Forecast +$hours h'; + } @override - String get mapLayerSatelliteBtdSo2 => '히마와리 이산화황/구름상'; + String get dpmDisasterTsunami => '쓰나미'; @override - String get mapLayerSatelliteBtdCo2 => '히마와리 권운/운고'; + String get changelogTypeStable => '정식'; @override - String get mapLayerSatelliteBtdOzone => '히마와리 대류권계면'; + String get mapLayerSatelliteTransparentClear => '맑음 = 투명,배경 지도 표시'; @override - String get mapLayerSatelliteCloudtop => '히마와리 운정 온도'; + String get mapOverlaySectionReference => '참조 레이어'; @override - String get mapLayerSatelliteCloudmask => '히마와리 구름 마스크'; + String get mapLayerSatelliteB02 => '히마와리 가시 녹색(B02)'; @override - String get mapLayerSatelliteSst => '히마와리 해수면 온도'; + String get reportListLocalFelt => '소규모 유감'; @override - String get mapLayerSatelliteNdvi => '히마와리 NDVI'; + String get weatherRankingEmpty => '정렬할 관측이 없습니다'; @override - String get mapLayerSatelliteNdwi => '히마와리 NDWI'; + String get notifySectionOther => '기타'; @override - String get mapLayerSatelliteMndwi => '히마와리 MNDWI'; + String weatherRankingMeta(String time, int count) { + return '자료 시각: $time\n관측점 $count'; + } @override - String get mapLayerSatelliteGlobalOutline => '국경선'; + String get onboardingTermsAgree => '서비스 약관을 읽었으며 이에 동의합니다'; @override - String get mapLayerSatelliteRgbComposite => 'RGB 합성(JMA 레시피)'; + String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 투명(식생 없음)'; @override - String get mapLayerSatelliteCloudClear => '맑음'; + String get notifyOptLocalIntensity4 => '현재 위치 진도 4 이상'; @override - String get mapLayerSatelliteCloudProbablyClear => '아마 맑음'; + String get eewArrived => '도달'; @override - String get mapLayerSatelliteCloudProbablyCloudy => '아마 구름'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => '구름'; + String get mapLayerCategoryLife => '생활'; @override - String get mapLayerSatelliteTransparentWarm => '맑음(고온부) = 투명,배경 지도 표시'; + String get reportFilterSortIntensity => '진도'; @override - String get mapLayerSatelliteTransparentReflectance => - '낮은 반사율/야간 = 투명,배경 지도 표시'; + String get typhoonMotion => '이동'; @override - String get mapLayerSatelliteTransparentZero => '차이 0 = 투명(신호 없음)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => '야간 = 투명,배경 지도 표시'; + String get typhoonIntensityIntense => 'Intense typhoon'; @override - String get mapLayerSatelliteTransparentNoData => '데이터 없음(육지) = 투명'; + String get mapLayerOrderTitle => '레이어 순서'; @override - String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 투명(식생 없음)'; + String get dpmYes => '예'; @override - String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 투명(수역 없음)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => '맑음 = 투명,배경 지도 표시'; + String get reportDetailLocalIntensityUnavailable => '진도 정보 없음'; @override - String get mapLayerStyleSection => '색상 스타일'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => '색상 스타일'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => '그레이스케일(JMA)'; + String get reportFilterDepth => '깊이'; @override - String get mapLayerStyleGrayTooltip => '기상청 적외 영상 관례:온도가 낮을수록 흰색'; + String get onboardingScrollHint => '계속하려면 아래로 스크롤하세요'; @override - String get mapLayerStyleJma => '운정 강조(JMA)'; + String get mapNavQpesums => '예보'; @override - String get mapLayerStyleJmaTooltip => '그레이스케일 바탕에 −40 °C 이하를 채색, 운정 고도 강조'; + String get navMap => '지도'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => '기상 특보'; @override - String get mapLayerStyleBdTooltip => 'Dvorak BD 커브——열대저기압 강도 분석용 계단 그레이스케일'; + String get reportFilterReset => '초기화'; @override - String get mapLayerQpesums => '1시간 강수 예보'; + String get mapLayerSatelliteMndwi => '히마와리 MNDWI'; @override - String get mapLayerLightning => '번개'; + String get typhoonOverlaySectionStorm => 'Storm wind'; @override - String lightningLegendCg(int minutes) { - return '대지로 · $minutes분 이내'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return '구름 사이 · $minutes분 이내'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => '현재'; + String get weatherDynamicStateSubtitle => '홈 배경 날씨를 재정의합니다'; @override - String get mapTimelinePast => '과거'; + String get reportFilterIntensityInfoModernTitle => '신제(2020년 이후)'; @override - String get mapTimelineFuture => '미래'; + String typhoonDataTime(String time) { + return 'Data time\n$time'; + } @override - String get mapTimelineObserved => '관측'; + String get restroomTypeAccessible => '장애인 화장실'; @override - String get mapTimelineForecast => '예보'; + String get moreSectionAbout => '정보'; @override - String mapTimelineDataTime(String time) { - return '데이터 시간 $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => '알림 설정'; + String get onboardingIntroBody => + 'DPIP는 여러분과 함께하는 방재 파트너입니다. 지진 조기경보, 지진 보고, 날씨, 각종 재해 정보를 통합하여 중요한 순간에 실시간으로 알려드립니다.\n\n• 지진: 지진 조기경보, 진도 속보, 상세 지진 보고\n• 날씨: 뇌우 실시간 메시지, 기상 특보\n• 지진해일 및 재난 정보\n\n다음으로, 서비스 약관을 확인하고 DPIP가 실시간으로 여러분을 보호할 수 있도록 몇 가지 권한을 허용해 주시기 바랍니다.'; @override - String get notifyTitle => '알림'; + String get shelterCapacityLabel => '수용 인원'; @override - String get notifyUnavailable => '푸시 알림이 아직 준비되지 않았습니다. 잠시 후 다시 시도해 주세요.'; + String get reportDetailImage => '지진 보고서 이미지'; @override - String get notifySetFailed => '설정을 저장하지 못했습니다. 다시 시도해 주세요.'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => '지진 조기경보'; + String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; @override - String get notifySectionEarthquake => '지진'; + String get onboardingPermNotify => '알림'; @override - String get notifySectionWeather => '날씨'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => '지진해일'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => '기타'; + String get defaultMapLayerSettings => '지도 기본 레이어'; @override - String get notifyEew => '긴급 지진 경보'; + String get moreSectionNotify => '알림'; @override - String get notifyMonitor => '강진 감시기'; + String get notifyUnavailable => '푸시 알림이 아직 준비되지 않았습니다. 잠시 후 다시 시도해 주세요.'; @override - String get notifyReport => '지진 보고'; + String get mapLayerOrderReset => '기본 순서로 재설정'; @override - String get notifyIntensity => '진도 속보'; + String get dpmAddress => '주소'; @override - String get notifyThunderstorm => '뇌우 알림'; + String get weatherRankingMergeCounty => '현시'; @override - String get notifyAdvisory => '기상 특보'; + String get moreSectionApp => '앱 다운로드'; @override - String get notifyEvacuation => '재난 정보'; + String get reportFilterIntensityInfoLegacyBody => + '진도는 0–7만 있으며 5약/5강/6약/6강 구분이 없습니다.'; @override - String get notifyTsunami => '지진해일 정보'; + String get mapLayerSatelliteSst => '히마와리 해수면 온도'; @override - String get notifyAnnouncement => '공지사항'; + String get qpesumsOverlayMenuTooltip => '정량 강수 예보 레이어 옵션'; @override - String get notifyOptOff => '끄기'; + String get mapTimelineFuture => '미래'; @override - String get notifyOptAll => '전체 수신'; + String get typhoonLegendCircleAvg => 'Average circle'; @override - String get notifyOptLocalIntensity4 => '현재 위치 진도 4 이상'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => '현재 위치 진도 1 이상'; + String get typhoonLabelSe => 'SE'; @override - String get notifyOptWeatherLocal => '현재 위치만'; + String get radarTownOutlineHint => '더 세밀한 구획'; @override - String get notifyOptTsunamiWarning => '지진해일 경보만'; + String eewCountdown(int seconds) { + return '$seconds초'; + } @override - String get notifyOptTsunamiAll => '지진해일 주의보 및 경보'; + String get typhoonLabelGust => 'Peak gust'; @override - String get onboardingNext => '다음'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => '이전'; + String get sponsorTerms => '이용약관'; @override - String get onboardingScrollHint => '계속하려면 아래로 스크롤하세요'; + String get restroomTypeGenderNeutral => '성중립 화장실'; @override - String get onboardingIntroTitle => 'DPIP에 오신 것을 환영합니다'; + String get notifyThunderstorm => '뇌우 알림'; @override - String get onboardingIntroBody => - 'DPIP는 여러분과 함께하는 방재 파트너입니다. 지진 조기경보, 지진 보고, 날씨, 각종 재해 정보를 통합하여 중요한 순간에 실시간으로 알려드립니다.\n\n• 지진: 지진 조기경보, 진도 속보, 상세 지진 보고\n• 날씨: 뇌우 실시간 메시지, 기상 특보\n• 지진해일 및 재난 정보\n\n다음으로, 서비스 약관을 확인하고 DPIP가 실시간으로 여러분을 보호할 수 있도록 몇 가지 권한을 허용해 주시기 바랍니다.'; + String get skyTimeGolden => '골든아워'; @override - String get onboardingTermsTitle => '서비스 약관'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'DPIP를 사용하기 전에 다음 유의 사항을 반드시 읽어 주세요:\n\n• 모든 정보는 중앙기상청(CWA)에서 발표한 내용을 기준으로 합니다.\n\n• 네트워크, 서버, 애플리케이션, 상위 데이터 출처의 상태에 따라 정보를 받지 못할 수 있습니다. 이러한 상황을 방지하기 위해 최선을 다하고 있으나, 절대 발생하지 않는다고 보장할 수는 없습니다.\n\n• 강한 흔들림이 알림보다 먼저 귀하의 위치에 도달할 수 있습니다.\n\n• 지진 조기경보는 빠르게 계산된 결과로 상당한 오차가 있을 수 있으므로, 이를 이해하고 신중하게 사용하시기 바랍니다.\n\n• 당국이 승인하지 않은 모든 행위는 법적 위험을 수반할 수 있으니, 관련 규정을 반드시 준수해 주세요.\n\n또한 지역 맞춤형 경보를 제공하기 위해, 본 서비스는 귀하에게 어떤 경보를 보낼지 결정하기 위한 목적으로만 포그라운드 및 백그라운드에서 귀하의 대략적인 위치와 푸시 식별자를 수집하여 업로드합니다.\n\n하단의 “동의하고 계속”을 누르면 위 사항을 읽고 이해했으며 이에 동의함을 확인하는 것입니다.'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => '서비스 약관을 읽었으며 이에 동의합니다'; + String weatherRankingAnalysisCurrent(String value) { + return '현재 $value°C'; + } @override - String get onboardingAgreeContinue => '동의하고 계속'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => '권한 허용'; + String get homeForecastUnavailable => '지역을 선택하면 예보를 볼 수 있습니다'; @override - String get onboardingPermsBody => - '재해가 발생하는 즉시 알려드릴 수 있도록 다음 권한을 허용해 주세요. 시스템 설정에서 언제든지 변경할 수 있습니다.'; + String get mapLayers => '레이어'; @override - String get onboardingPermNotify => '알림'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => '지진, 날씨, 재해가 발생하는 즉시 경보를 전달합니다.'; + String get languageSettings => '언어'; @override - String get onboardingPermCritical => '중요 알림'; + String get dpmDisasterNuclear => '핵 사고'; @override - String get onboardingPermCriticalDesc => - '생명을 위협하는 지진 경보가 무음 모드나 방해 금지 모드에서도 소리를 낼 수 있도록 합니다.'; + String get language => '언어'; @override - String get onboardingPermLocation => '위치'; + String homeForecastFeelsLike(String temp) { + return '체감 $temp°'; + } @override - String get onboardingPermLocationDesc => '현재 위치에 맞춰 경보를 전달합니다.'; + String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; @override - String get onboardingPermBackground => '백그라운드 위치'; + String get skyTimeDawn => '여명'; @override - String get onboardingPermBackgroundDesc => - '“항상 허용”을 선택하면 앱이 종료된 상태에서도 위치 맞춤 경보를 받을 수 있습니다.'; + String get skyTimeAfternoon => '오후'; @override - String get onboardingPermBattery => '배터리 최적화 제외'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'DPIP가 백그라운드에서 계속 실행되어 경보가 지연되거나 누락되지 않도록 허용합니다.'; + String get typhoonWarningTitle => '태풍 경보'; @override - String get onboardingGrant => '허용'; + String get moreSourceCode => '소스 코드'; @override - String get onboardingGranted => '허용됨'; + String get mapLayerCategoryWeather => '기상 관측'; @override - String get onboardingStart => '시작하기'; + String get mapLayerSatelliteB09 => '히마와리 중층 수증기(B09)'; @override - String get language => '언어'; + String get windForecastTownOutlineHint => '더 촘촘한 망'; @override - String get languageSettings => '언어'; + String get mapLayerSatelliteCloudmask => '히마와리 구름 마스크'; @override - String get languageSystem => '시스템 기본값'; + String get mapAppCopyCoordinates => '좌표 복사'; @override - String get locationBannerServiceOff => '위치 서비스가 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.'; + String get reportFilterIntensityInfoIntro => + '기상서는 2020년 1월 1일(타이베이 시간)부터 신제 진도를 사용합니다.'; @override - String get locationBannerPermission => '위치 권한이 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.'; + String get mapNavEarthquake => '지진'; @override - String get locationBannerFix => '설정 열기'; + String get typhoonGust => '순간최대풍속'; @override - String get notifyBannerDisabled => '알림이 꺼져 있어 재난 경보를 받을 수 없습니다.'; + String get restroomGradeAverage => '보통'; @override - String get onboardingSkipTitle => '권한이 허용되지 않았습니다'; + String get mapLayerSatelliteBtdCo2 => '히마와리 권운/운고'; @override - String get onboardingSkipBody => - '위치 및 알림 권한이 없으면 DPIP가 주변의 지진과 재난을 실시간으로 알려드릴 수 없습니다. 나중에 설정에서 권한을 허용할 수 있습니다.'; + String get onboardingPermBackgroundDesc => + '“항상 허용”을 선택하면 앱이 종료된 상태에서도 위치 맞춤 경보를 받을 수 있습니다.'; @override - String get onboardingSkipStay => '돌아가기'; + String get mapTimelineForecast => '예보'; @override - String get onboardingSkipLeave => '그래도 건너뛰기'; + String get restroomTypeLabel => '유형'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => '지진'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => + 'Level-10 wind field + average circle (yellow)'; @override - String get moreSourceCode => '소스 코드'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => '앱 다운로드'; + String get reportDetailTitle => '지진 보고서'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'TREM 탐지 보고'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · 데이터 시간 $time'; + } @override - String get displaySettings => '화면'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => '지도 기본 레이어'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - '지도 탭을 열 때 표시할 레이어입니다. 하단 탐색 아이콘과 라벨도 함께 바뀝니다.'; + String get radarCountyOutline => '시·군 경계'; @override - String get mapNavRadar => '레이더'; + String get onboardingGranted => '허용됨'; @override - String get mapNavQpesums => '예보'; + String get commonClose => '닫기'; @override - String get mapNavSatellite => '위성'; + String get restroomGradeLabel => '등급'; @override - String get mapNavLightning => '번개'; + String get rainIntervalNow => '오늘'; @override - String get mapNavTyphoon => '태풍'; + String get changelogCurrentVersion => '현재'; @override - String get mapNavEarthquake => '지진'; + String get typhoonLabelPressure => 'Central pressure'; @override - String get mapNavTemperature => '온도'; + String get typhoonOverlayForecastCalloutsTooltip => + 'Show forecast-point detail cards when zoomed in'; @override - String get mapNavHumidity => '습도'; + String get aedOpenRemark => '운영시간 비고'; @override - String get mapNavPressure => '기압'; + String get onboardingPermsBody => + '재해가 발생하는 즉시 알려드릴 수 있도록 다음 권한을 허용해 주세요. 시스템 설정에서 언제든지 변경할 수 있습니다.'; @override - String get mapNavWind => '풍향'; + String get typhoonOverlaySectionWeather => 'Weather underlay'; @override - String get mapNavRain => '강우'; + String get notifyOptWeatherLocal => '현재 위치만'; @override - String get mapNavDisaster => '방재'; + String get mapNavRain => '강우'; @override - String get displayTheme => '테마'; + String get moonDays => 'days'; @override - String get themeSystem => '시스템'; + String mapLegendUnit(String unit) { + return '단위: $unit'; + } @override - String get themeLight => '라이트'; + String get weatherModeClear => '맑음'; @override - String get themeDark => '다크'; + String get meshtasticRadio => 'Radio'; @override - String get moreSectionAbout => '정보'; + String get commonEmpty => '표시할 내용이 없습니다'; @override - String get termsOfService => '서비스 약관'; + String get mapLayerSatelliteB01 => '히마와리 가시 청색(B01)'; @override - String get faq => '자주 묻는 질문'; + String get meshtasticExternalPower => 'External power'; @override - String get openSourceLicenses => '오픈소스 라이선스'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get sponsorTitle => 'DPIP 후원하기'; + String get reportFilterOrderAsc => '오름차순'; @override - String get sponsorIntro => - 'DPIP는 실시간 재난 예방 정보를 제공하는 데 전념하며, 광고나 다른 수익 모델이 없습니다. 여러분의 후원은 서버 운영과 지속적인 개발에 도움이 됩니다.'; + String get reportFilterApply => '적용'; @override - String get sponsorSubscriptions => '구독'; + String get reportDetailImageUnavailable => '보고서 이미지가 아직 없습니다'; @override - String get sponsorRecommended => '추천'; + String get weatherRankingHighest => '최고'; @override - String get sponsorOneTime => '일회성 후원'; + String get reportDetailReplay => '다시 보기'; @override - String sponsorPerMonth(String price) { - return '$price / 월'; - } + String get mapLayerRestroom => '공중화장실'; @override - String get sponsorRestore => '구매 복원'; + String get restroomCategoryWelfare => '사회복지 기관·집회 시설'; @override - String get sponsorTerms => '이용약관'; + String get restroomGradeExcellent => '최우수'; @override - String get sponsorPrivacy => '개인정보 처리방침'; + String get meshtasticLastSent => 'Last sent'; @override - String get sponsorRestoring => '구매를 복원하는 중…'; + String get meshtasticName => 'Name'; @override - String get sponsorRestoreUnavailable => '스토어에 연결할 수 없습니다. 나중에 다시 시도해 주세요.'; + String get meshtasticScan => 'Scan'; @override - String get commonClose => '닫기'; + String get mapLayerCategoryForecast => '수치 예보'; @override - String get mapLayerTemperature => '기온'; + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; @override - String get trendRange24h => '24시간'; + String get themeSystem => '시스템'; @override - String get trendRange7d => '7일'; + String get mapLayerSatelliteNdvi => '히마와리 NDVI'; @override - String get trendNoData => '추세 데이터 없음'; + String get typhoonLegendForecast => '예보 경로'; @override - String trendCumulativeTotal(String total) { - return '누적 $total mm'; + String typhoonValueHpa(String n) { + return '$n hPa'; } @override - String chartHourLabel(int hour) { - return '$hour시'; - } + String get weatherPrecipitation => '강수량'; @override - String get mapLayerHumidity => '습도'; + String get moonNextFullMoon => 'Next full moon'; @override - String get mapLayerPressure => '기압'; + String get dpmSheetEmpty => '지도에서 마커를 눌러 상세 보기'; @override - String get mapLayerWind => '바람'; + String get onboardingSkipLeave => '그래도 건너뛰기'; @override - String get mapLayerRain => '강수량'; + String get onboardingBack => '이전'; @override - String get rainIntervalMenu => '누적 구간'; + String get aedPlaceDesc => '설치 위치'; @override - String get rainIntervalNow => '오늘'; + String get onboardingSkipTitle => '권한이 허용되지 않았습니다'; @override - String get rainInterval10m => '10분'; + String get restroomTypeFamily => '가족 화장실'; @override - String get rainInterval1h => '1시간'; + String typhoonValueKm(String n) { + return '$n km'; + } @override - String get rainInterval3h => '3시간'; + String get typhoonPressure => '기압'; @override - String get rainInterval6h => '6시간'; + String get onboardingPermBattery => '배터리 최적화 제외'; + + @override + String get typhoonLabelNw => 'NW'; + + @override + String get dpmDisasterFlood => '홍수'; + + @override + String get moonPhaseWaxingCrescent => 'Waxing crescent'; + + @override + String get restroomCategoryLeisure => '휴양·오락 시설'; + + @override + String get mapLayerTemperature => '기온'; + + @override + String get aedCategory => '분류'; + + @override + String get meshtasticChannels => 'Channels'; + + @override + String get monitorWaiting => '데이터 대기 중…'; + + @override + String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + + @override + String get reportDetailEpicenter => '진앙 좌표'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => '바람'; + + @override + String get reportDetailMagnitude => '지진 규모'; + + @override + String get reportDetailAreaIntensity => '지역별 진도'; @override String get rainInterval12h => '12시간'; @override - String get rainInterval24h => '24시간'; + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } @override - String get rainInterval2d => '2일'; + String get dpmDisasterLandslide => '산사태'; @override - String get rainInterval3d => '3일'; + String get notifyMonitor => '강진 감시기'; @override - String get mapLayerTyphoon => '태풍'; + String get onboardingStart => '시작하기'; @override - String get typhoonNoActive => '활성 태풍 없음'; + String sponsorPerMonth(String price) { + return '$price / 월'; + } @override - String get typhoonWind => '풍속'; + String get mapLayerPressure => '기압'; @override - String get typhoonGust => '순간최대풍속'; + String get mapLayerSatelliteB04 => '히마와리 근적외(B04)'; @override - String get typhoonPressure => '기압'; + String get mapLayerSatelliteTransparentZero => '차이 0 = 투명(신호 없음)'; @override - String get typhoonMotion => '이동'; + String get shelterIndoorLabel => '실내 수용'; @override - String get typhoonLabelPosition => 'Centre location'; + String get notifyOptOff => '끄기'; @override - String get typhoonLabelDirection => 'Past movement direction'; + String get reportFilterSortTime => '시간'; + + @override + String get mapLayerSatelliteCloudProbablyClear => '아마 맑음'; + + @override + String get weatherModeThunderstorm => '뇌우'; + + @override + String get homeViewOnMap => '지도에서 보기'; + + @override + String get reportFilterIntensityInfoLegacyTitle => '구제(2020년 이전)'; @override String get typhoonLabelSpeed => 'Past movement speed'; @override - String get typhoonLabelPressure => 'Central pressure'; + String mapAppOpenFailed(String app) { + return '$app을(를) 열 수 없습니다'; + } @override - String get typhoonLabelWind => 'Max. sustained wind near centre'; + String get mapLayerSatelliteRgbComposite => 'RGB 합성(JMA 레시피)'; @override - String get typhoonLabelGust => 'Peak gust'; + String get meshtasticReceived => 'Received'; @override - String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; + String get weatherRankingExtremeLow => '오늘 최저'; @override - String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; + String get mapLayerSatelliteB10 => '히마와리 하층 수증기(B10)'; @override - String get typhoonLabelProbCircle => '70% probability circle'; + String get mapLayerSatelliteCloudProbablyCloudy => '아마 구름'; + + @override + String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 투명(수역 없음)'; + + @override + String get shelterCategoryLabel => '적용 재해'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => '돌풍'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => '대피소 재해 유형'; + + @override + String get moreServerStatus => '서버 상태'; + + @override + String get notifySectionWeather => '날씨'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => '지진'; + + @override + String get changelogBodyEmpty => '이 릴리스에 대한 설명이 없습니다.'; + + @override + String get radarGlobalOutline => '국경'; + + @override + String get notifyEew => '긴급 지진 경보'; + + @override + String get regionNationwide => '전국'; + + @override + String get moreNotifyLog => 'DPIP 알림 발송 기록'; + + @override + String get regionCurrent => '현재 위치'; + + @override + String get dpmFilterSectionRestroom => '시설 유형'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => '눈'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => '디버그 정보'; + + @override + String get mapLayerSatelliteB14 => '히마와리 장파 적외(B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => '번개'; + + @override + String get homeForecastEmpty => '예보 데이터가 없습니다'; + + @override + String get sponsorOneTime => '일회성 후원'; + + @override + String get mapLayerSatelliteBtdSplit => '히마와리 스플릿 윈도우'; + + @override + String get onboardingPermBackground => '백그라운드 위치'; + + @override + String get aedEmergencyPhone => '비상 연락처'; + + @override + String get dpmOpenInMaps => '지도에서 열기'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + '생명을 위협하는 지진 경보가 무음 모드나 방해 금지 모드에서도 소리를 낼 수 있도록 합니다.'; + + @override + String get mapLayerSatelliteTransparentWarm => '맑음(고온부) = 투명,배경 지도 표시'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => '24시간 예보'; + + @override + String get typhoonLegendWarningAreas => '경보 지역'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; + } + + @override + String get notifyOptLocalIntensity1 => '현재 위치 진도 1 이상'; + + @override + String get mapTimelinePast => '과거'; + + @override + String get restroomTypeFemale => '여자 화장실'; + + @override + String get reportListToday => '오늘'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => '불러오는 중…'; + + @override + String get typhoonIntensityModerate => 'Moderate typhoon'; + + @override + String get typhoonWind => '풍속'; + + @override + String get mapLayerSatelliteAsh => '히마와리 화산재'; + + @override + String get rainInterval3h => '3시간'; + + @override + String get reportListSearch => '조회'; + + @override + String get mapLayerCategorySatellite => '위성'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => '위치'; + + @override + String get mapLayerSatelliteNightmicrophysics => '히마와리 야간 미세물리'; + + @override + String get typhoonIntensityTd => 'Tropical depression'; + + @override + String get reportFilterDate => '날짜'; + + @override + String get sponsorRestoreUnavailable => '스토어에 연결할 수 없습니다. 나중에 다시 시도해 주세요.'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => '저장된 지역이 없습니다'; + + @override + String get onboardingPermBatteryDesc => + 'DPIP가 백그라운드에서 계속 실행되어 경보가 지연되거나 누락되지 않도록 허용합니다.'; + + @override + String get mapNavDisaster => '방재'; + + @override + String get radarScanRangeSubtitle => '레이더 4기가 실제로 관측하는 범위를 표시합니다.'; + + @override + String get aedHoursSunday => '일요일 운영시간'; + + @override + String get reportDetailOriginTime => '발생 시각'; + + @override + String get trendNoData => '추세 데이터 없음'; + + @override + String get onboardingPermLocation => '위치'; + + @override + String get moreDiscord => 'Discord 커뮤니티'; + + @override + String get mapNavPressure => '기압'; + + @override + String get mapLayerSatelliteB13 => '히마와리 적외(B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => '아직 릴리스 노트가 없습니다'; + + @override + String get reportFilterDateStartNote => '시작일: 당일 00:00(타이베이)'; + + @override + String get eewTitle => '지진 조기경보'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '$count/$max 선택됨'; + } + + @override + String get mapLayerSatelliteBtdSo2 => '히마와리 이산화황/구름상'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => '흐림'; + + @override + String get reportDetailDepth => '진원 깊이'; + + @override + String get typhoonOverlayWarningTooltip => + 'Highlight counties under a typhoon warning'; + + @override + String get reportFilterDatePick => '날짜 선택'; + + @override + String get onboardingSkipStay => '돌아가기'; + + @override + String get commonFetchFailed => '데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.'; + + @override + String get shelterOutdoorLabel => '실외 수용'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => '레이더'; + + @override + String get mapLayerSatelliteCloudClear => '맑음'; + + @override + String eewSummary(String magnitude, String depth) { + return '규모 $magnitude · 깊이 $depth km'; + } + + @override + String get locationBannerPermission => '위치 권한이 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.'; + + @override + String get typhoonOverlayWeatherNoneTooltip => + 'No radar or infrared underlay'; + + @override + String get radarCountyOutlineHint => '에코 위에 표시'; + + @override + String get windForecastCountyOutlineHint => '바람장 위에 표시'; + + @override + String get homeRainTrendTitle => '향후 1시간 강수'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => '태풍'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => '남녀 공용 화장실'; + + @override + String get restroomGradeGood => '우수'; + + @override + String get notifyTsunami => '지진해일 정보'; + + @override + String get navData => '자료'; + + @override + String get mapLayerSatelliteBtdWvirw => '히마와리 오버슈팅 탑'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => '이 기기에서는 전화를 걸 수 없습니다'; + + @override + String get reportFilterAny => '전체'; + + @override + String get weatherRankingMergeTo => '병합'; + + @override + String get notifyIntensity => '진도 속보'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => '누적 구간'; + + @override + String get reportDetailLocalFelt => '국지적 유감지진'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => '허용'; + + @override + String get weatherModeRain => '비'; + + @override + String get shelterVulnerableOkLabel => '취약계층 수용 가능'; + + @override + String get stationSheetEmpty => '관측소를 눌러 관측값 보기'; + + @override + String get typhoonLegendProbability => '내습 확률'; + + @override + String get reportFilterMagnitude => '규모'; + + @override + String get skyTimeMorning => '오전'; + + @override + String get experimentalFeatures => '실험적 기능'; + + @override + String get onboardingTermsBody => + 'DPIP를 사용하기 전에 다음 유의 사항을 반드시 읽어 주세요:\n\n• 모든 정보는 중앙기상청(CWA)에서 발표한 내용을 기준으로 합니다.\n\n• 네트워크, 서버, 애플리케이션, 상위 데이터 출처의 상태에 따라 정보를 받지 못할 수 있습니다. 이러한 상황을 방지하기 위해 최선을 다하고 있으나, 절대 발생하지 않는다고 보장할 수는 없습니다.\n\n• 강한 흔들림이 알림보다 먼저 귀하의 위치에 도달할 수 있습니다.\n\n• 지진 조기경보는 빠르게 계산된 결과로 상당한 오차가 있을 수 있으므로, 이를 이해하고 신중하게 사용하시기 바랍니다.\n\n• 당국이 승인하지 않은 모든 행위는 법적 위험을 수반할 수 있으니, 관련 규정을 반드시 준수해 주세요.\n\n또한 지역 맞춤형 경보를 제공하기 위해, 본 서비스는 귀하에게 어떤 경보를 보낼지 결정하기 위한 목적으로만 포그라운드 및 백그라운드에서 귀하의 대략적인 위치와 푸시 식별자를 수집하여 업로드합니다.\n\n하단의 “동의하고 계속”을 누르면 위 사항을 읽고 이해했으며 이에 동의함을 확인하는 것입니다.'; + + @override + String get reportFilterTitle => '필터'; + + @override + String get onboardingPermCritical => '중요 알림'; + + @override + String trendCumulativeTotal(String total) { + return '누적 $total mm'; + } + + @override + String get languageName => '한국어'; + + @override + String get reportListEmptyFiltered => '조건에 맞는 지진 보고서가 없습니다'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => '태풍'; + + @override + String get weatherModeSand => '황사'; + + @override + String get typhoonSatelliteTitle => '위성'; + + @override + String get notifyReport => '지진 보고'; + + @override + String get mapAppCoordinatesCopied => '좌표가 복사되었습니다'; + + @override + String get skyTimeNight => '밤'; + + @override + String get sponsorRecommended => '추천'; + + @override + String get mapLayerSatelliteB15 => '히마와리 장파 적외(B15)'; + + @override + String get weatherRankingWind => '풍속'; + + @override + String get feedStale => '데이터가 오래되었을 수 있습니다'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · 풍력 $level'; + } + + @override + String get navHome => '홈'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => '히마와리 운정 온도'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => '오픈소스 라이선스'; + + @override + String get weatherRankingLowest => '최저'; + + @override + String get reportFilterSortDepth => '깊이'; + + @override + String mapTimelineDataTime(String time) { + return '데이터 시간 $time'; + } + + @override + String get radarScanRange => '스캔 범위 표시'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return '일교차 $value°C'; + } + + @override + String get weatherRankingExtremeHigh => '오늘 최고'; + + @override + String get changelogVersionDetails => '릴리스 상세'; + + @override + String get sponsorPrivacy => '개인정보 처리방침'; + + @override + String get reportDetailLocalIntensity => '내 위치의 진도'; + + @override + String get mapLayerSatelliteNaturalcolor => '히마와리 내추럴컬러'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; + + @override + String shelterCapacityValue(int n) { + return '$n 명'; + } + + @override + String lightningLegendCc(int minutes) { + return '구름 사이 · $minutes분 이내'; + } + + @override + String get meshtasticSendHint => 'Message to broadcast'; + + @override + String monitorDelay(String value) { + return '지연 $value s'; + } + + @override + String get dpmNo => '아니요'; + + @override + String get mapLayerSatelliteB08 => '히마와리 상층 수증기(B08)'; + + @override + String get meshtasticReconnecting => 'Reconnecting…'; + + @override + String get radarTownOutlineSubtitle => '레이더 에코 아래에서도 읍·면·동 경계가 보이도록 합니다.'; + + @override + String get typhoonOverlayWeatherSatelliteTooltip => + 'Infrared closest to the typhoon bulletin time'; + + @override + String get radarScanRangeHint => '범위 밖 공백은 미관측'; + + @override + String typhoonPickerTd(String no) { + return 'Tropical depression TD $no'; + } + + @override + String get mapLayerSatelliteWatervapor => '히마와리 수증기'; + + @override + String get regionAddButton => '지역 추가'; + + @override + String get displaySettings => '화면'; + + @override + String get restroomGradePoor => '불합격'; + + @override + String get restroomCategoryTourist => '관광 지역·경치 구역'; + + @override + String get locationBannerServiceOff => '위치 서비스가 꺼져 있어 지역 맞춤 경보를 받을 수 없습니다.'; + + @override + String get mapLayerStyleTooltip => '색상 스타일'; + + @override + String lightningLegendCg(int minutes) { + return '대지로 · $minutes분 이내'; + } + + @override + String get skyTimeAuto => '자동'; + + @override + String get appLogs => '앱 로그'; + + @override + String get feedConnecting => '연결 중…'; + + @override + String get notifyBannerDisabled => '알림이 꺼져 있어 재난 경보를 받을 수 없습니다.'; + + @override + String get weatherHumidity => '습도'; + + @override + String typhoonValueMs(String n) { + return '$n m/s'; + } + + @override + String homeForecastHumidity(String value) { + return '습도 $value%'; + } + + @override + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; + + @override + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; + + @override + String get restroomCategoryTransport => '교통'; + + @override + String get reportFilterLocationHint => '예: 화롄, 해역'; + + @override + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; + + @override + String get meshtasticBattery => 'Battery'; + + @override + String get meshtasticDistance => '거리'; + + @override + String get meshtasticSnrTrend => '신호 추이 (SNR)'; + + @override + String get meshtasticBatteryTrend => '배터리 추이'; + + @override + String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + + @override + String get mapLayerSatelliteBtdOzone => '히마와리 대류권계면'; + + @override + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } + + @override + String get notifySectionEarthquake => '지진'; + + @override + String get mapLayerDisasterMap => '방재 지도'; + + @override + String get weatherModeFog => '안개'; + + @override + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } + + @override + String get mapLayerStyleGrayTooltip => '기상청 적외 영상 관례:온도가 낮을수록 흰색'; + + @override + String get moreAnnouncements => '공지사항'; + + @override + String get mapLayerSatelliteTransparentNoData => '데이터 없음(육지) = 투명'; + + @override + String get restroomCategoryGovernment => '민원 업무 시설'; + + @override + String get typhoonLegendCurrent => '현재 중심'; + + @override + String get aedAddress => '주소'; + + @override + String get mapLayerAed => 'AED'; + + @override + String get changelogTypePrerelease => '베타'; + + @override + String get reportFilterIntensityInfoModernBody => + '진도는 0–4, 5약, 5강, 6약, 6강, 7입니다. 필터는 신제를 따르며, 이전 지진은 목록에서 구제 표기로 표시됩니다.'; + + @override + String get typhoonOverlayWeatherNone => 'None'; + + @override + String get mapLayerStyleGray => '그레이스케일(JMA)'; + + @override + String get weatherModeAuto => '자동'; + + @override + String get typhoonLabelProbCircle => '70% probability circle'; + + @override + String get notifyOptAll => '전체 수신'; + + @override + String get displayTheme => '테마'; + + @override + String get mapLayerSatelliteB07 => '히마와리 단파 적외(B07)'; + + @override + String get typhoonLabelDirection => 'Past movement direction'; + + @override + String get regionManageTitle => '저장한 지역'; + + @override + String get typhoonLegendCone => '예보 원추'; + + @override + String get moreCwaEew => '중앙기상청(CWA) 지진 조기경보'; + + @override + String get onboardingPermsTitle => '권한 허용'; + + @override + String get mapLayerStyleJma => '운정 강조(JMA)'; + + @override + String get rainInterval10m => '10분'; + + @override + String weatherRankingAnalysisLow(String value) { + return '최저 $value'; + } + + @override + String get meshtasticConnectAnyway => 'Connect anyway'; + + @override + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => '히마와리 근적외(B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => + '낮은 반사율/야간 = 투명,배경 지도 표시'; + + @override + String chartHourLabel(int hour) { + return '$hour시'; + } + + @override + String get mapLayerShelter => '대피소'; + + @override + String get typhoonOverlayProbabilityTooltip => + 'Show strike probability (hides the forecast cone)'; + + @override + String get mapLayerSatelliteNdwi => '히마와리 NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => '대피소 표시'; + + @override + String get mapNavHumidity => '습도'; + + @override + String get reportDetailSortByIntensity => '진도순 정렬'; + + @override + String get homeRainTrendNoData => '데이터 없음'; + + @override + String get mapLayerCategoryRadar => '레이더'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => '히마와리 에어매스'; + + @override + String get typhoonTrackDetail => '경로 상세'; + + @override + String get dataSectionWeather => '기상'; + + @override + String get aedHoursWeekday => '평일 운영시간'; + + @override + String get homeActiveEventsTitle => '발효 중 이벤트'; + + @override + String weatherRankingAnalysisHigh(String value) { + return '최고 $value'; + } + + @override + String get faq => '자주 묻는 질문'; + + @override + String get typhoonHistoryLive => '실시간'; + + @override + String eewSerial(int serial) { + return '제 $serial 보'; + } + + @override + String get reportFilterSort => '정렬'; @override - String typhoonForecastLead(String hours) { - return 'Forecast +$hours h'; - } + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; @override - String get typhoonLabelNw => 'NW'; + String get dataEarthquakeSubtitle => '지진 보고서'; @override - String get typhoonLabelNe => 'NE'; + String get typhoonNoActive => '활성 태풍 없음'; @override - String get typhoonLabelSw => 'SW'; + String get mapLayerSatelliteB11 => '히마와리 이산화황/구름상(B11)'; @override - String get typhoonLabelSe => 'SE'; + String get navEvents => '이벤트'; @override - String typhoonValueLat(String lat) { - return '$lat°N'; - } + String get onboardingTermsTitle => '서비스 약관'; @override - String typhoonValueLon(String lon) { - return '$lon°E'; - } + String get mapTownLabels => '읍면동 이름'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => '설정을 저장하지 못했습니다. 다시 시도해 주세요.'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '$n m/s'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return 'Data time\n$time'; - } + String get notifyAnnouncement => '공지사항'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'DPIP에 오신 것을 환영합니다'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => '현재 위치를 가져올 수 없습니다'; @override - String get mapLayerMonitor => '실시간 지진 모니터'; + String get languageSystem => '시스템 기본값'; @override - String get mapLayerDisasterMap => '방재 지도'; + String get skyTimeSunset => '일몰'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => '히마와리 황사'; @override - String get disasterMapOverlayMenuTooltip => '방재 지도 레이어'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => '레이어'; + String get regionEdit => '수정'; @override - String get disasterMapOverlayAedTooltip => 'AED 위치 표시'; + String get weatherDynamicState => '날씨 애니메이션'; @override - String get aedAddress => '주소'; + String get mapPlaceholderDisabled => '지도 (일시 사용 중지)'; @override - String get aedRegion => '지역'; + String get moonNow => '지금'; @override - String get aedCategory => '분류'; + String get moonSectionAppearance => '겉모습'; @override - String get aedType => '유형'; + String get moonSectionRiseSet => '월출·월몰'; @override - String get aedPlaceDesc => '설치 위치'; + String get moonSectionUpcoming => '다음 위상'; @override - String get aedDescription => '비고'; + String get moonSectionCalendar => '달력'; @override - String get aedHoursWeekday => '평일 운영시간'; + String get moonDistance => '거리'; @override - String get aedHoursSaturday => '토요일 운영시간'; + String get moonKilometres => 'km'; @override - String get aedHoursSunday => '일요일 운영시간'; + String get moonApparentSize => '시직경'; @override - String get aedOpenRemark => '운영시간 비고'; + String get moonRise => '월출'; @override - String get aedEmergencyPhone => '비상 연락처'; + String get moonSet => '월몰'; @override - String get mapLayerRestroom => '공중화장실'; + String get moonNextNewMoon => '다음 삭'; @override - String get mapLayerShelter => '대피소'; + String get moonAlwaysUp => '종일 지평선 위'; @override - String get disasterMapOverlayRestroomTooltip => '공중화장실 표시'; + String get moonNoEvent => '해당 없음'; @override - String get disasterMapOverlayShelterTooltip => '대피소 표시'; + String get sunTitle => '태양'; @override - String get dpmOpenInMaps => '지도에서 열기'; + String get sunSubtitle => '일출·박명·절기'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => '일조'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => '박명'; @override - String mapAppDefault(String app) { - return '$app (기본)'; - } + String get sunSectionLight => '빛'; @override - String get mapAppCopyCoordinates => '좌표 복사'; + String get sunSectionSundial => '해시계'; @override - String get mapAppCoordinatesCopied => '좌표가 복사되었습니다'; + String get sunSectionTerms => '절기'; @override - String mapAppOpenFailed(String app) { - return '$app을(를) 열 수 없습니다'; - } + String get sunRise => '일출'; @override - String get mapAppCallFailed => '이 기기에서는 전화를 걸 수 없습니다'; + String get sunSet => '일몰'; @override - String get mapOverlaySectionReference => '참조 레이어'; + String get sunNoon => '남중'; @override - String get mapLayerCategoryEarthquake => '지진'; + String get sunDayLength => '낮 길이'; @override - String get mapLayerCategoryTyphoon => '태풍'; + String get sunTwilightCivil => '시민'; @override - String get mapLayerCategoryWeather => '기상 관측'; + String get sunTwilightNautical => '항해'; @override - String get mapLayerCategorySatellite => '위성'; + String get sunTwilightAstronomical => '천문'; @override - String get mapLayerCategoryRadar => '레이더'; + String get sunGoldenHourMorning => '아침 골든아워'; @override - String get mapLayerCategoryLife => '생활'; + String get sunGoldenHourEvening => '저녁 골든아워'; @override - String get mapLayerCategoryForecast => '수치 예보'; + String get sunBlueHour => '블루아워'; @override - String get mapOverlaySectionMap => '지도'; + String get sunEquationOfTime => '균시차'; @override - String get rainIntervalSection => '집계 시간'; + String get sunMinutes => '분'; @override - String get mapTownLabels => '읍면동 이름'; + String get solarTermNext => '다음 절기'; @override - String get mapTownLabelsHint => '확대하면 읍면동 이름 표시'; + String get planetsTitle => '행성'; @override - String get mapTerrainRelief => '지형 입체감'; + String get planetsSubtitle => '오늘 밤 위치와 밝기'; @override - String get mapTerrainReliefHint => '기본 지도에 지형 음영 표시'; + String get planetsSectionTonight => '현재'; @override - String get dpmSheetEmpty => '지도에서 마커를 눌러 상세 보기'; + String get planetUp => '지평선 위'; @override - String get dpmAddress => '주소'; + String get planetDown => '지평선 아래'; @override - String get restroomTypeLabel => '유형'; + String get planetInGlare => '태양에 근접'; @override - String get restroomCategoryLabel => '구분'; + String get planetMagnitude => '등급'; @override - String get restroomGradeLabel => '등급'; + String get planetElongation => '이각'; @override - String get restroomTypeFemale => '여자 화장실'; + String get planetSky => '시간대'; @override - String get restroomTypeMale => '남자 화장실'; + String get planetEvening => '초저녁'; @override - String get restroomTypeMixed => '남녀 공용 화장실'; + String get planetMorning => '새벽'; @override - String get restroomTypeAccessible => '장애인 화장실'; + String get planetDistance => '거리'; @override - String get restroomTypeGenderNeutral => '성중립 화장실'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => '가족 화장실'; + String get planetAltitude => '고도'; @override - String get restroomTypeUnspecified => '미설정'; + String get planetMercury => '수성'; @override - String get restroomCategoryTransport => '교통'; + String get planetVenus => '금성'; @override - String get restroomCategoryPark => '공원'; + String get planetMars => '화성'; @override - String get restroomCategoryCommercial => '상업·영업 시설'; + String get planetJupiter => '목성'; @override - String get restroomCategoryReligious => '종교·의례 시설'; + String get planetSaturn => '토성'; @override - String get restroomCategoryCultural => '문화·여가 시설'; + String get planetUranus => '천왕성'; @override - String get restroomCategoryGovernment => '민원 업무 시설'; + String get planetNeptune => '해왕성'; @override - String get restroomCategoryWelfare => '사회복지 기관·집회 시설'; + String get solarTermVernalEquinox => '춘분'; @override - String get restroomCategoryTourist => '관광 지역·경치 구역'; + String get solarTermPureBrightness => '청명'; @override - String get restroomCategoryLeisure => '휴양·오락 시설'; + String get solarTermGrainRain => '곡우'; @override - String get restroomCategoryOther => '기타'; + String get solarTermStartOfSummer => '입하'; @override - String get restroomGradeExcellent => '최우수'; + String get solarTermGrainFull => '소만'; @override - String get restroomGradeGood => '우수'; + String get solarTermGrainInEar => '망종'; @override - String get restroomGradeAverage => '보통'; + String get solarTermSummerSolstice => '하지'; @override - String get restroomGradePoor => '불합격'; + String get solarTermMinorHeat => '소서'; @override - String get shelterAddressLabel => '주소'; + String get solarTermMajorHeat => '대서'; @override - String get shelterCapacityLabel => '수용 인원'; + String get solarTermStartOfAutumn => '입추'; @override - String shelterCapacityValue(int n) { - return '$n 명'; - } + String get solarTermEndOfHeat => '처서'; @override - String get shelterCategoryLabel => '적용 재해'; + String get solarTermWhiteDew => '백로'; @override - String get shelterIndoorLabel => '실내 수용'; + String get solarTermAutumnalEquinox => '추분'; @override - String get shelterOutdoorLabel => '실외 수용'; + String get solarTermColdDew => '한로'; @override - String get shelterVulnerableOkLabel => '취약계층 수용 가능'; + String get solarTermFrostDescent => '상강'; @override - String get dpmYes => '예'; + String get solarTermStartOfWinter => '입동'; @override - String get dpmNo => '아니요'; + String get solarTermMinorSnow => '소설'; @override - String get stationSheetEmpty => '관측소를 눌러 관측값 보기'; + String get solarTermMajorSnow => '대설'; @override - String monitorDelay(String value) { - return '지연 $value s'; - } + String get solarTermWinterSolstice => '동지'; @override - String get monitorWaiting => '데이터 대기 중…'; + String get solarTermMinorCold => '소한'; @override - String mapLegendUnit(String unit) { - return '단위: $unit'; - } + String get solarTermMajorCold => '대한'; @override - String get typhoonLegendPast => '실황 경로'; + String get solarTermStartOfSpring => '입춘'; @override - String get typhoonIntensityTd => 'Tropical depression'; + String get solarTermRainWater => '우수'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => '경칩'; @override - String typhoonPickerTd(String no) { - return 'Tropical depression TD $no'; - } + String get tonightTitle => '오늘 밤'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => '무엇을 언제 볼 수 있는가'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => '관측 가능 시간'; @override - String get typhoonIntensityMild => 'Mild typhoon'; + String get tonightAstronomicalNight => '천문박명 종료'; @override - String get typhoonIntensityModerate => 'Moderate typhoon'; + String get tonightNeverDark => '완전히 어두워지지 않음'; @override - String get typhoonIntensityIntense => 'Intense typhoon'; + String get tonightDarkWindow => '암흑 시간대'; @override - String get typhoonLegendForecast => '예보 경로'; + String get tonightMoonAllNight => '달이 밤새 떠 있음'; @override - String get typhoonLegendForecastPoint => '예보 지점'; + String get tonightDarkTotal => '총 암흑 시간'; @override - String get typhoonLegendCurrent => '현재 중심'; + String get tonightMoonlight => '달빛'; @override - String get typhoonLegendCone => '예보 원추'; + String get tonightSectionShowers => '유성우'; @override - String get mapLegendExpand => '범례'; + String get tonightRadiantDown => '복사점이 뜨지 않음'; @override - String get mapLegendCollapse => '범례 숨기기'; + String get tonightPerHour => '개/시'; @override - String get mapMyLocation => '내 위치'; + String get tonightSectionSatellites => '위성 통과'; @override - String get mapResetNorth => '북쪽으로 되돌리기'; + String get tonightSectionTargets => '지금 볼 수 있는 천체'; @override - String get typhoonLegendCircle15 => '강풍권 (7급)'; + String get showerQuadrantids => '사분의자리'; @override - String get typhoonLegendCircleAvg => 'Average circle'; + String get showerLyrids => '거문고자리'; @override - String get typhoonLegendCircle25 => '폭풍권 (10급)'; + String get showerEtaAquariids => '물병자리 에타'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => '물병자리 델타'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => '페르세우스자리'; @override - String get typhoonLegendProbability => '내습 확률'; + String get showerOrionids => '오리온자리'; @override - String get typhoonLegendWarningAreas => '경보 지역'; + String get showerSouthernTaurids => '황소자리 남'; @override - String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + String get showerLeonids => '사자자리'; @override - String get typhoonOverlaySectionStorm => 'Storm wind'; + String get showerGeminids => '쌍둥이자리'; @override - String get typhoonOverlaySectionExtra => 'Overlays'; + String get showerUrsids => '작은곰자리'; @override - String get typhoonOverlayStormBandSubtitle => 'With average circle'; + String get deepSkyOpenCluster => '산개성단'; @override - String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; + String get deepSkyGlobularCluster => '구상성단'; @override - String get typhoonOverlayProbabilityTooltip => - 'Show strike probability (hides the forecast cone)'; + String get deepSkySpiralGalaxy => '나선은하'; @override - String get typhoonOverlayWarningTooltip => - 'Highlight counties under a typhoon warning'; + String get deepSkyEllipticalGalaxy => '타원은하'; @override - String get typhoonOverlayStormL7Tooltip => - 'Level-7 wind field + average circle (purple)'; + String get deepSkyIrregularGalaxy => '불규칙은하'; @override - String get typhoonOverlayStormL10Tooltip => - 'Level-10 wind field + average circle (yellow)'; + String get deepSkyPlanetaryNebula => '행성상성운'; @override - String get typhoonOverlaySectionWeather => 'Weather underlay'; + String get deepSkySupernovaRemnant => '초신성 잔해'; @override - String get typhoonOverlayWeatherNone => 'None'; + String get deepSkyEmissionNebula => '발광성운'; @override - String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; + String get deepSkyReflectionNebula => '반사성운'; @override - String get typhoonOverlayWeatherNoneTooltip => - 'No radar or infrared underlay'; + String get deepSkyAsterism => '성군'; @override - String get typhoonOverlayWeatherRadarTooltip => - 'Radar echo closest to the typhoon bulletin time'; + String get almanacTitle => '역법'; @override - String get typhoonOverlayWeatherSatelliteTooltip => - 'Infrared closest to the typhoon bulletin time'; + String get almanacSubtitle => '음력과 앞으로의 일식·월식'; @override - String get typhoonWarningTitle => '태풍 경보'; + String get almanacSectionToday => '오늘'; @override - String typhoonWarningAreas(String areas) { - return '대상 지역: $areas'; - } + String get almanacGregorian => '양력'; @override - String get typhoonTrackDetail => '경로 상세'; + String get almanacLunar => '음력'; @override - String get typhoonHistoryTitle => '자료 시각'; + String get almanacYear => '세차'; @override - String get typhoonHistoryLive => '실시간'; + String get almanacMonthLength => '월 대소'; @override - String get typhoonSatelliteTitle => '위성'; + String get almanacLongMonth => '30일'; @override - String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + String get almanacShortMonth => '29일'; @override - String get typhoonOverlayForecastCalloutsTooltip => - 'Show forecast-point detail cards when zoomed in'; + String get almanacLeapPrefix => '윤'; @override - String get dpmFilterSectionRestroom => '시설 유형'; + String get almanacSectionLunarEclipses => '월식'; @override - String get dpmFilterSectionRestroomType => '화장실 유형'; + String get almanacSectionSolarEclipses => '일식'; @override - String get dpmFilterSectionShelter => '대피소 재해 유형'; + String get almanacNoSolarEclipse => '범위 내 없음'; @override - String get dpmDisasterFlood => '홍수'; + String get eclipseTotal => '개기'; @override - String get dpmDisasterEarthquake => '지진'; + String get eclipsePartial => '부분'; @override - String get dpmDisasterLandslide => '산사태'; + String get eclipseAnnular => '금환'; @override - String get dpmDisasterTsunami => '쓰나미'; + String get eclipsePenumbral => '반영'; @override - String get dpmDisasterSlope => '사면 재해'; + String get zodiacRat => '쥐'; @override - String get dpmDisasterNuclear => '핵 사고'; + String get zodiacOx => '소'; @override - String get skyTime => '하늘 시각'; + String get zodiacTiger => '호랑이'; @override - String get skyTimeAuto => '자동'; + String get zodiacRabbit => '토끼'; @override - String get skyTimeDawn => '여명'; + String get zodiacDragon => '용'; @override - String get skyTimeSunrise => '일출'; + String get zodiacSnake => '뱀'; @override - String get skyTimeMorning => '오전'; + String get zodiacHorse => '말'; @override - String get skyTimeNoon => '정오'; + String get zodiacGoat => '양'; @override - String get skyTimeAfternoon => '오후'; + String get zodiacMonkey => '원숭이'; @override - String get skyTimeGolden => '골든아워'; + String get zodiacRooster => '닭'; @override - String get skyTimeSunset => '일몰'; + String get zodiacDog => '개'; @override - String get skyTimeDusk => '땅거미'; + String get zodiacPig => '돼지'; @override - String get skyTimeNight => '밤'; + String get tideTitle => '조석'; @override - String get weatherModeCloudy => '구름 많음'; + String get tideSubtitle => '사리·조금과 달의 인력'; @override - String get weatherModeOvercast => '흐림'; + String get tideDisclaimer => '천문 기조력만이며 항만 조석표가 아닙니다. 수위는 기상청 발표를 참고하세요.'; @override - String get weatherModeSnow => '눈'; + String get tideSectionNow => '현재'; @override - String get weatherModeSand => '황사'; + String get tidePhase => '주기'; @override - String get radarScanRange => '스캔 범위 표시'; + String get tideSpring => '사리'; @override - String get radarScanRangeSubtitle => '레이더 4기가 실제로 관측하는 범위를 표시합니다.'; + String get tideNeap => '조금'; @override - String get radarScanRangeHint => '범위 밖 공백은 미관측'; + String get tideMiddling => '중조'; @override - String get radarOverlayMenuTooltip => '레이더 레이어 옵션'; + String get tideLunarDistanceFactor => '달의 인력'; @override - String get radarCountyOutline => '시·군 경계'; + String get tideEquilibrium => '평형 조위'; @override - String get radarGlobalOutline => '국경'; + String get tideMetres => 'm'; @override - String get radarGlobalOutlineHint => '각국 국경선'; + String get tidePerigeanSpring => '다음 근지점 사리'; @override - String get radarCountyOutlineHint => '에코 위에 표시'; + String get tideSectionTurningPoints => '전환점'; @override - String get radarCountyOutlineSubtitle => '레이더 에코 아래에서도 경계가 보이도록 합니다.'; + String get tideHigh => '고'; @override - String get radarTownOutline => '읍·면·동 경계'; + String get tideLow => '저'; @override - String get radarTownOutlineHint => '더 세밀한 구획'; + String get skyChartTitle => '성도'; @override - String get radarTownOutlineSubtitle => '레이더 에코 아래에서도 읍·면·동 경계가 보이도록 합니다.'; + String get skyChartSubtitle => '머리 위 맨눈으로 보이는 하늘'; @override - String get qpesumsOverlayMenuTooltip => '정량 강수 예보 레이어 옵션'; + String get skyChartNorth => '북'; @override - String get windForecastOverlayMenuTooltip => '바람 예보 레이어 옵션'; + String get skyChartEast => '동'; @override - String get windForecastCountyOutlineHint => '바람장 위에 표시'; + String get skyChartSouth => '남'; @override - String get windForecastGlobalOutlineHint => '각국 국경선'; + String get skyChartWest => '서'; @override - String get windForecastTownOutlineHint => '더 촘촘한 망'; + String tonightElementAge(int days) { + return '궤도 요소 $days일 전'; + } @override - String eewSerial(int serial) { - return '제 $serial 보'; + String almanacLunarDate(String leap, int month, int day) { + return '$leap$month월 $day일'; } @override - String get eewMaxIntensity => '최대 진도'; + String get tonightNoShowers => '진행 중인 유성우 없음'; @override - String get eewLocalIntensity => '현재 위치 예상'; + String get tonightNoPasses => '48시간 내 가시 통과 없음'; @override - String get eewSWave => 'S파'; + String get tonightSatellitesUnavailable => '궤도 데이터를 읽을 수 없음'; @override - String get eewArrived => '도달'; + String get tonightNoTargets => '충분히 높은 천체 없음'; @override - String eewCountdown(int seconds) { - return '$seconds초'; - } + String get skyChartUnavailable => '성표를 읽을 수 없음'; } diff --git a/lib/l10n/gen/app_localizations_th.dart b/lib/l10n/gen/app_localizations_th.dart index 98d76557e..8ba879469 100644 --- a/lib/l10n/gen/app_localizations_th.dart +++ b/lib/l10n/gen/app_localizations_th.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,509 +10,503 @@ class AppLocalizationsTh extends AppLocalizations { AppLocalizationsTh([String locale = 'th']) : super(locale); @override - String get languageName => 'ไทย'; + String typhoonValueLat(String lat) { + return '$lat°N'; + } @override - String get navHome => 'หน้าแรก'; + String get onboardingSkipBody => + 'หากไม่อนุญาตตำแหน่งและการแจ้งเตือน DPIP จะไม่สามารถแจ้งเตือนแผ่นดินไหวและภัยพิบัติใกล้คุณแบบเรียลไทม์ได้ คุณยังสามารถเปิดใช้ภายหลังได้ในการตั้งค่า'; @override - String get navEvents => 'เหตุการณ์'; + String get rainInterval24h => '24 ชม.'; @override - String get navMap => 'แผนที่'; + String homeRainTrendHeavyStopping(int minutes) { + return 'คาดว่าฝนตกหนักจะหยุดในอีก $minutes นาที'; + } @override - String get navData => 'ข้อมูล'; + String get mapTimelineObserved => 'เวลาตรวจวัด'; @override - String get navEarthquake => 'แผ่นดินไหว'; + String get regionSelectTitle => 'เลือกพื้นที่'; @override - String get dataSectionSeismic => 'แผ่นดินไหว'; + String get skyTimeNoon => 'เที่ยงวัน'; @override - String get dataEarthquakeSubtitle => 'รายงานแผ่นดินไหว'; + String get radarCountyOutlineSubtitle => + 'ทำให้เส้นแบ่งเขตยังอ่านออกใต้ภาพเอคโคเรดาร์'; @override - String get dataSectionWeather => 'อากาศ'; + String get dpmFilterSectionRestroomType => 'ประเภทห้องน้ำ'; @override - String get dataWeatherRankingSubtitle => 'อันดับสถานีแบบเรียลไทม์'; + String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; @override - String get weatherRankingTitle => 'อันดับการสังเกต'; + String get reportFilterIntensity => 'ความเข้ม'; @override - String weatherRankingMeta(String time, int count) { - return 'เวลาข้อมูล: $time\n$count สถานี'; - } + String get mapLayerLightning => 'ฟ้าผ่า'; @override - String get weatherRankingEmpty => 'ไม่มีข้อมูลให้จัดอันดับ'; + String get restroomTypeMale => 'ห้องน้ำชาย'; @override - String get weatherRankingBy => 'เรียง'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => 'สูงสุด'; + String get reportDetailSortByCounty => 'เรียงตามพื้นที่'; @override - String get weatherRankingLowest => 'ต่ำสุด'; + String get homeRainTrendScattered => 'อาจมีฝนตกประปราย'; @override - String get weatherRankingMergeTo => 'รวม'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => 'ตำบล'; + String get weatherRankingTempExtremes => 'ค่าสุดขั้วอุณหภูมิ'; @override - String get weatherRankingMergeCounty => 'อำเภอ/เมือง'; + String get themeLight => 'สว่าง'; @override - String get weatherRankingWind => 'ความเร็วลม'; + String get mapTerrainReliefHint => 'แสดงความนูนของภูมิประเทศบนแผนที่ฐาน'; @override - String get weatherRankingGust => 'ลมกระโชก'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => 'ค่าสุดขั้วอุณหภูมิ'; + String get moreSectionRegion => 'พื้นที่'; @override - String get weatherRankingExtremeHigh => 'สูงสุดวันนี้'; + String get dpmDisasterEarthquake => 'แผ่นดินไหว'; @override - String get weatherRankingExtremeLow => 'ต่ำสุดวันนี้'; + String get mapLayerSatellite => 'Himawari Infrared (B13)'; @override - String get weatherRankingExtremeRange => 'ช่วงวัน'; + String get aedHoursSaturday => 'เวลาวันเสาร์'; @override - String weatherRankingRecordedAt(String time) { - return 'บันทึกเมื่อ $time'; - } + String get dpmDisasterSlope => 'ภัยพิบัติลาดชัน'; @override - String weatherRankingAnalysisCurrent(String value) { - return 'ปัจจุบัน $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return 'สูง $value'; - } + String get notifySectionEew => 'การเตือนแผ่นดินไหวล่วงหน้า'; @override - String weatherRankingAnalysisLow(String value) { - return 'ต่ำ $value'; - } + String get mapResetNorth => 'กลับไปทางเหนือ'; @override - String weatherRankingAnalysisRange(String value) { - return 'ช่วง $value°C'; - } + String get rainInterval2d => '2 วัน'; @override - String get reportListEmpty => 'ไม่มีรายงานแผ่นดินไหว'; + String get mapTownLabelsHint => 'แสดงชื่อตำบลเมื่อขยายแผนที่'; @override - String get reportListEmptyFiltered => 'ไม่มีรายงานที่ตรงกับเงื่อนไข'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => 'เฉพาะการเตือนภัยสึนามิ'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => 'ขั้นสูง'; @override - String get reportListLocalFelt => 'รู้สึกในพื้นที่'; + String get weatherRankingExtremeRange => 'ช่วงวัน'; @override - String get reportListToday => 'วันนี้'; + String get notifySettingsMenu => 'การตั้งค่าการแจ้งเตือน'; @override - String get reportListYesterday => 'เมื่อวาน'; + String get typhoonHistoryTitle => 'เวลาข้อมูล'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app (ค่าเริ่มต้น)'; } @override - String get reportListEnd => 'สิ้นสุดรายการ'; + String get trendRange24h => '24 ชม.'; @override - String get reportFilterTitle => 'ตัวกรอง'; + String get mapLayerStyleJmaTooltip => + 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; @override - String get reportFilterSort => 'เรียงลำดับ'; + String weatherRankingRecordedAt(String time) { + return 'บันทึกเมื่อ $time'; + } @override - String get reportFilterSortTime => 'เวลา'; + String get mapLayerRain => 'ปริมาณฝน'; @override - String get reportFilterSortIntensity => 'ความเข้ม'; + String get mapLayerQpesums => 'พยากรณ์ฝน 1 ชั่วโมงข้างหน้า'; @override - String get reportFilterSortMagnitude => 'ขนาด'; + String get mapOverlaySectionMap => 'แผนที่'; @override - String get reportFilterSortDepth => 'ความลึก'; + String get mapTerrainRelief => 'ความนูนของภูมิประเทศ'; @override - String get reportFilterOrderDesc => 'มาก→น้อย'; + String get eewMaxIntensity => 'ความรุนแรงสูงสุด'; @override - String get reportFilterOrderAsc => 'น้อย→มาก'; + String get mapLegendCollapse => 'ซ่อนคำอธิบาย'; @override - String get reportFilterIntensity => 'ความเข้ม'; + String get changelogTitle => 'บันทึกการอัปเดต'; @override - String get reportFilterIntensityInfoTitle => 'มาตรวัดความรุนแรงแบบใหม่/เก่า'; + String get reportFilterOrderDesc => 'มาก→น้อย'; @override - String get reportFilterIntensityInfoIntro => - 'CWA เปลี่ยนมาตรวัดเมื่อ 1 ม.ค. 2020 (เวลาไทเป)'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyTitle => 'แบบเก่า (ก่อน 2020)'; + String get reportFilterIntensityInfoTitle => 'มาตรวัดความรุนแรงแบบใหม่/เก่า'; @override - String get reportFilterIntensityInfoLegacyBody => - 'มีระดับ 0–7 เท่านั้น ไม่แยก 5−/5+/6−/6+'; + String get mapLayerTyphoon => 'ไต้ฝุ่น'; @override - String get reportFilterIntensityInfoModernTitle => 'แบบใหม่ (ตั้งแต่ 2020)'; + String get radarOverlayMenuTooltip => 'ตัวเลือกชั้นเรดาร์'; @override - String get reportFilterIntensityInfoModernBody => - 'ระดับ 0–4, 5−, 5+, 6−, 6+, 7 แถบตัวกรองใช้แบบใหม่ เหตุการณ์เก่าในรายการยังแสดงป้ายแบบเก่า'; + String get mapMyLocation => 'ตำแหน่งของฉัน'; @override - String get reportFilterMagnitude => 'ขนาด'; + String get meshtasticNodes => 'Nodes'; @override - String get reportFilterDepth => 'ความลึก'; + String get meshtasticSend => 'Send'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get typhoonOverlayStormL7Tooltip => + 'Level-7 wind field + average circle (purple)'; @override - String get reportFilterDate => 'วันที่'; + String get aedType => 'ประเภท'; @override - String get reportFilterDatePick => 'เลือกวันที่'; + String get termsOfService => 'ข้อกำหนดในการให้บริการ'; @override - String get reportFilterDateStartNote => 'วันเริ่ม: 00:00 ของวันนั้น(ไทเป)'; + String get typhoonLegendCircle25 => 'วงพายุ (รุนแรง)'; @override - String get reportFilterDateEndNote => 'วันสิ้นสุด: 24:00 ของวันนั้น(ไทเป)'; + String get sponsorTitle => 'สนับสนุน DPIP'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get mapNavSatellite => 'ดาวเทียม'; @override - String get reportFilterLocation => 'สถานที่'; + String homeRainTrendUpdated(String time) { + return 'อัปเดต $time'; + } @override - String get reportFilterLocationHint => 'เช่น ฮวาเหลียน'; + String get onboardingNext => 'ถัดไป'; @override - String get reportFilterAny => 'ทั้งหมด'; + String get weatherRankingMergeTown => 'ตำบล'; @override - String get reportFilterApply => 'ใช้'; + String get mapLayerMonitor => 'เครื่องตรวจแผ่นดินไหว'; @override - String get reportFilterReset => 'รีเซ็ต'; + String get moreYoutube => 'YouTube'; @override - String get reportListSearch => 'ค้นหา'; + String get sponsorSubscriptions => 'แบบสมัครสมาชิก'; @override - String get reportDetailTitle => 'รายงานแผ่นดินไหว'; + String typhoonValueLon(String lon) { + return '$lon°E'; + } @override - String reportDetailNumbered(String number) { - return 'แผ่นดินไหวรู้สึกได้อย่างมีนัยสำคัญ หมายเลข $number'; - } + String get skyTime => 'เวลาท้องฟ้า'; @override - String get reportDetailLocalFelt => 'แผ่นดินไหวรู้สึกได้เฉพาะพื้นที่'; + String get weatherModeCloudy => 'มีเมฆมาก'; @override - String get reportDetailInfo => 'รายละเอียด'; + String get skyTimeDusk => 'สนธยา'; @override - String get reportDetailOriginTime => 'เวลาเกิดเหตุ'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailEpicenter => 'พิกัดศูนย์กลาง'; + String get reportFilterDateEndNote => 'วันสิ้นสุด: 24:00 ของวันนั้น(ไทเป)'; @override - String get reportDetailMagnitude => 'ขนาดแผ่นดินไหว'; + String get reportFilterSortMagnitude => 'ขนาด'; @override - String get reportDetailDepth => 'ความลึกจุดศูนย์กลาง'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailAreaIntensity => 'ความเข้มแยกตามพื้นที่'; + String get mapLayerCategoryEarthquake => 'แผ่นดินไหว'; @override - String get reportDetailLocalIntensity => 'ความเข้มที่ตำแหน่งของคุณ'; + String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; @override - String get reportDetailLocalIntensityUnavailable => 'ไม่มีข้อมูลความเข้ม'; + String get typhoonLegendPast => 'เส้นทางจริง'; @override - String get reportDetailSortByIntensity => 'เรียงตามความเข้ม'; + String get restroomCategoryOther => 'อื่น ๆ'; @override - String get reportDetailSortByCounty => 'เรียงตามพื้นที่'; + String homeForecastHighLow(String high, String low) { + return 'สูง $high° · ต่ำ $low°'; + } @override - String get reportDetailImage => 'ภาพรายงานแผ่นดินไหว'; + String get locationBannerFix => 'เปิดการตั้งค่า'; @override - String get reportDetailImageUnavailable => 'ยังไม่มีภาพรายงาน'; + String get mapLegendExpand => 'คำอธิบาย'; @override - String get reportDetailOpenReport => 'หน้ารายงาน'; + String get eewNone => 'ขณะนี้ไม่มีการเตือนแผ่นดินไหวล่วงหน้า'; @override - String get reportDetailReplay => 'เล่นย้อนหลัง'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get navMore => 'เพิ่มเติม'; + String get notifyOptTsunamiAll => 'ข่าวสารและการเตือนภัยสึนามิ'; @override - String get appLogs => 'บันทึกแอป'; + String get meshtasticLayerOptions => 'Node options'; @override - String get changelogTitle => 'บันทึกการอัปเดต'; + String get onboardingAgreeContinue => 'ยอมรับและดำเนินการต่อ'; @override - String get changelogEmpty => 'ยังไม่มีบันทึกการเผยแพร่'; + String get commonRetry => 'ลองอีกครั้ง'; @override - String get changelogTypePrerelease => 'เบต้า'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogTypeStable => 'ทางการ'; + String reportDetailNumbered(String number) { + return 'แผ่นดินไหวรู้สึกได้อย่างมีนัยสำคัญ หมายเลข $number'; + } @override - String get changelogCurrentVersion => 'ปัจจุบัน'; + String get typhoonOverlayStormBandSubtitle => 'With average circle'; @override - String get changelogVersionDetails => 'รายละเอียดเวอร์ชัน'; + String get disasterMapOverlayRestroomTooltip => 'แสดงห้องน้ำสาธารณะ'; @override - String get changelogBodyEmpty => 'ไม่มีคำอธิบายสำหรับรุ่นนี้'; + String get weatherRankingTitle => 'อันดับการสังเกต'; @override - String get mapPlaceholderDisabled => 'แผนที่ (ปิดใช้งานชั่วคราว)'; + String get homeRainTrendHeavySustained => 'ฝนตกหนักต่อเนื่องตลอดชั่วโมงหน้า'; @override - String get moreSectionRegion => 'พื้นที่'; + String get notifySectionTsunami => 'สึนามิ'; @override - String get moreSectionNotify => 'การแจ้งเตือน'; + String get restroomCategoryPark => 'สวนสาธารณะ'; @override - String get moreSectionDisplay => 'การแสดงผล'; + String get moreLinkOpenFailed => 'ไม่สามารถเปิดลิงก์ได้'; @override - String get regionManageTitle => 'พื้นที่ที่ใช้บ่อย'; + String get themeDark => 'มืด'; @override - String get regionAddButton => 'เพิ่มพื้นที่'; + String get sponsorRestore => 'กู้คืนการซื้อ'; @override - String get regionEmpty => 'ยังไม่มีพื้นที่ที่บันทึกไว้'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String get regionSelectTitle => 'เลือกพื้นที่'; + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String regionSelectCount(int count, int max) { - return 'เลือกแล้ว $count/$max'; - } + String get meshtasticTraffic => 'Traffic'; @override - String regionSelectFull(int max) { - return 'บันทึกได้สูงสุด $max พื้นที่'; - } + String get mapLayerStyleBdTooltip => + 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; @override - String get regionEdit => 'แก้ไข'; + String get disasterMapOverlayAedTooltip => 'แสดงตำแหน่ง AED'; @override - String get moreSectionAdvanced => 'ขั้นสูง'; + String get mapLayerHumidity => 'ความชื้น'; @override - String get moreDeveloper => 'ข้อมูลดีบัก'; + String get mapLayerSatelliteTransparentNight => + 'Night = transparent, the basemap shows'; @override - String get experimentalFeatures => 'ฟีเจอร์ทดลอง'; + String get meshtasticScanning => 'Scanning…'; @override - String get moreSectionLinks => 'ลิงก์ที่เกี่ยวข้อง'; + String regionSelectFull(int max) { + return 'บันทึกได้สูงสุด $max พื้นที่'; + } @override - String get moreCwaEew => - 'การเตือนแผ่นดินไหวล่วงหน้าของกรมอุตุนิยมวิทยากลาง (CWA)'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreTremReport => 'รายงานการตรวจจับ TREM'; + String get navMore => 'เพิ่มเติม'; @override - String get moreServerStatus => 'สถานะเซิร์ฟเวอร์'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreAnnouncements => 'ประกาศ'; + String get disasterMapOverlaySectionLayers => 'ชั้น'; @override - String get moreDiscord => 'ชุมชน Discord'; + String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; @override - String get moreNotifyLog => 'บันทึกการส่งการแจ้งเตือนของ DPIP'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get moreLinkOpenFailed => 'ไม่สามารถเปิดลิงก์ได้'; + String get typhoonLabelNe => 'NE'; @override - String get weatherDynamicState => 'แอนิเมชันสภาพอากาศ'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherDynamicStateSubtitle => 'แทนที่สภาพอากาศพื้นหลังหน้าแรก'; + String get reportListEmpty => 'ไม่มีรายงานแผ่นดินไหว'; @override - String get weatherModeAuto => 'อัตโนมัติ'; + String get reportListEnd => 'สิ้นสุดรายการ'; @override - String get weatherModeClear => 'ท้องฟ้าแจ่มใส'; + String get mapLayerSatelliteTruecolor => 'Himawari True Color'; @override - String get weatherModeRain => 'ฝนตก'; + String get typhoonOverlaySectionExtra => 'Overlays'; @override - String get weatherModeFog => 'หมอกหนา'; + String get eewSWave => 'คลื่น S'; @override - String get weatherModeThunderstorm => 'พายุฝนฟ้าคะนอง'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get commonLoading => 'กำลังโหลด…'; + String get restroomCategoryCultural => 'สถานที่ทางวัฒนธรรม'; @override - String get commonRetry => 'ลองอีกครั้ง'; + String get typhoonLabelWind => 'Max. sustained wind near centre'; @override - String get commonError => 'เกิดข้อผิดพลาด'; + String get radarGlobalOutlineHint => 'กรอบนอกของทุกประเทศ'; @override - String get commonFetchFailed => 'ไม่สามารถโหลดข้อมูลได้ โปรดลองอีกครั้ง'; + String get notifyEvacuation => 'ข้อมูลภัยพิบัติ'; @override - String get commonEmpty => 'ไม่มีข้อมูล'; + String get typhoonLegendCircle15 => 'วงพายุ (แรง)'; @override - String get feedConnecting => 'กำลังเชื่อมต่อ…'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedStale => 'ข้อมูลอาจล้าสมัย'; + String get homeRainTrendLightSustained => + 'ฝนตกเล็กน้อยต่อเนื่องตลอดชั่วโมงหน้า'; @override - String get feedOffline => 'การเชื่อมต่อขาดหาย'; + String get commonError => 'เกิดข้อผิดพลาด'; @override - String get eewTitle => 'การเตือนแผ่นดินไหวล่วงหน้า'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String get eewNone => 'ขณะนี้ไม่มีการเตือนแผ่นดินไหวล่วงหน้า'; + String get meshtasticPower => 'Power'; @override - String eewSummary(String magnitude, String depth) { - return 'ขนาด $magnitude · ความลึก $depth กม.'; + String get mapTimelineNow => 'ตอนนี้'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => 'ทั่วประเทศ'; + String get reportDetailOpenReport => 'หน้ารายงาน'; @override - String get regionCurrent => 'ตำแหน่งปัจจุบัน'; + String get trendRange7d => '7 วัน'; @override - String get regionCurrentUnavailable => 'ไม่สามารถระบุตำแหน่งปัจจุบันได้'; + String typhoonWarningAreas(String areas) { + return 'พื้นที่: $areas'; + } @override - String get weatherPrecipitation => 'ปริมาณน้ำฝน'; + String get rainIntervalSection => 'ช่วงเวลา'; @override - String get weatherHumidity => 'ความชื้น'; + String get notifyTitle => 'การแจ้งเตือน'; @override - String weatherDataTime(String station, String time) { - return '$station · เวลาข้อมูล $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => 'ดูบนแผนที่'; + String get restroomCategoryLabel => 'หมวดหมู่'; @override - String get homeForecastTitle => 'พยากรณ์ 24 ชั่วโมง'; + String get sponsorRestoring => 'กำลังกู้คืนการซื้อ…'; @override - String homeForecastHighLow(String high, String low) { - return 'สูง $high° · ต่ำ $low°'; - } + String get sponsorIntro => + 'DPIP มุ่งมั่นให้ข้อมูลการป้องกันภัยพิบัติแบบเรียลไทม์ โดยไม่มีโฆษณาหรือรูปแบบหารายได้อื่น การสนับสนุนของคุณช่วยให้เรารักษาเซิร์ฟเวอร์และพัฒนาต่อไปได้'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => 'ที่อยู่'; @override - String homeForecastFeelsLike(String temp) { - return 'รู้สึกเหมือน $temp°'; - } + String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; @override - String homeForecastHumidity(String value) { - return 'ความชื้น $value%'; - } + String get restroomCategoryCommercial => 'สถานประกอบการพาณิชย์'; @override - String homeForecastWind(String direction, String level) { - return '$direction · แรง $level'; - } + String get aedRegion => 'พื้นที่'; @override - String get homeForecastUnavailable => 'เลือกพื้นที่เพื่อดูพยากรณ์'; + String homeRainTrendLightStopping(int minutes) { + return 'คาดว่าฝนจะหยุดในอีก $minutes นาที'; + } @override - String get homeForecastEmpty => 'ไม่มีข้อมูลพยากรณ์'; + String get reportDetailInfo => 'รายละเอียด'; @override - String get homeActiveEventsTitle => 'เหตุการณ์ที่ยังมีผล'; + String get mapNavWind => 'ทิศลม'; @override - String get homeActiveEventsEmpty => 'ไม่มีเหตุการณ์ที่ยังมีผล'; + String get windForecastOverlayMenuTooltip => 'ตัวเลือกชั้นพยากรณ์ลม'; @override - String get homeRainTrendTitle => 'ฝนชั่วโมงถัดไป'; + String get dataWeatherRankingSubtitle => 'อันดับสถานีแบบเรียลไทม์'; @override String homeRainTrendMinute(int minute) { @@ -519,1313 +514,2122 @@ class AppLocalizationsTh extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return 'อัปเดต $time'; - } + String get rainInterval6h => '6 ชม.'; @override - String get homeRainTrendNoData => 'ไม่มีข้อมูล'; + String get restroomTypeUnspecified => 'ไม่ระบุ'; @override - String get homeRainTrendScattered => 'อาจมีฝนตกประปราย'; + String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; @override - String get homeRainTrendLightSustained => - 'ฝนตกเล็กน้อยต่อเนื่องตลอดชั่วโมงหน้า'; + String get mapLayerSatelliteGlobalOutline => 'Country border'; @override - String homeRainTrendLightStopping(int minutes) { - return 'คาดว่าฝนจะหยุดในอีก $minutes นาที'; - } + String get mapNavTemperature => 'อุณหภูมิ'; @override - String get homeRainTrendHeavySustained => 'ฝนตกหนักต่อเนื่องตลอดชั่วโมงหน้า'; + String get typhoonLegendForecastPoint => 'จุดพยากรณ์'; @override - String homeRainTrendHeavyStopping(int minutes) { - return 'คาดว่าฝนตกหนักจะหยุดในอีก $minutes นาที'; - } + String get reportListYesterday => 'เมื่อวาน'; @override - String get mapLayers => 'ชั้นข้อมูล'; + String get moreSectionLinks => 'ลิงก์ที่เกี่ยวข้อง'; @override - String get mapLayerOrderTitle => 'จัดเรียงเลเยอร์'; + String get feedOffline => 'การเชื่อมต่อขาดหาย'; @override - String get mapLayerOrderReset => 'รีเซ็ตลำดับ'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => 'เรดาร์สะท้อนสังเคราะห์'; + String get moreSectionDisplay => 'การแสดงผล'; @override - String get mapLayerSatellite => 'Himawari Infrared (B13)'; + String get rainInterval3d => '3 วัน'; @override - String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; + String get defaultMapLayerSubtitle => + 'แท็บแผนที่จะเปิดชั้นนี้ ไอคอนและป้ายนำทางด้านล่างจะเปลี่ยนตาม'; @override - String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; + String get aedDescription => 'หมายเหตุ'; @override - String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; + String get typhoonOverlayWeatherRadarTooltip => + 'Radar echo closest to the typhoon bulletin time'; @override - String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + String get onboardingPermLocationDesc => 'ส่งการเตือนภัยตามตำแหน่งที่คุณอยู่'; @override - String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; + String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; @override - String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + String get homeActiveEventsEmpty => 'ไม่มีเหตุการณ์ที่ยังมีผล'; @override - String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + String get typhoonLabelPosition => 'Centre location'; @override - String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; + String get weatherRankingBy => 'เรียง'; @override - String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; + String get typhoonIntensityMild => 'Mild typhoon'; @override - String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + String get windForecastGlobalOutlineHint => 'กรอบนอกของทุกประเทศ'; @override - String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; + String get rainInterval1h => '1 ชม.'; @override - String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; + String get eewLocalIntensity => 'ประมาณ ณ ตำแหน่ง'; @override - String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + String get mapLayerRadar => 'เรดาร์สะท้อนสังเคราะห์'; @override - String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + String get restroomCategoryReligious => 'สถานที่ทางศาสนา'; @override - String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; + String get mapLayerSatelliteCloudCloudy => 'Cloudy'; @override - String get mapLayerSatelliteTruecolor => 'Himawari True Color'; + String get skyTimeSunrise => 'พระอาทิตย์ขึ้น'; @override - String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => 'Himawari Ash'; + String get onboardingPermNotifyDesc => + 'ส่งการเตือนแผ่นดินไหว สภาพอากาศ และภัยพิบัติทันทีที่เกิดขึ้น'; @override - String get mapLayerSatelliteDust => 'Himawari Dust'; + String get radarTownOutline => 'เส้นแบ่งเขตอำเภอ'; @override - String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + String get mapLayerStyleSection => 'Colour style'; @override - String get mapLayerSatelliteNightmicrophysics => - 'Himawari Night Microphysics'; + String get disasterMapOverlayMenuTooltip => 'ชั้นแผนที่ป้องกันภัย'; @override - String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; + String get typhoonLabelSw => 'SW'; @override - String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + String typhoonForecastLead(String hours) { + return 'Forecast +$hours h'; + } @override - String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + String get dpmDisasterTsunami => 'สึนามิ'; @override - String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; + String get changelogTypeStable => 'ทางการ'; @override - String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; + String get mapLayerSatelliteTransparentClear => + 'Clear sky = transparent, the basemap shows'; @override - String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + String get mapOverlaySectionReference => 'เลเยอร์อ้างอิง'; @override - String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; + String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; @override - String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; + String get reportListLocalFelt => 'รู้สึกในพื้นที่'; @override - String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + String get weatherRankingEmpty => 'ไม่มีข้อมูลให้จัดอันดับ'; @override - String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + String get notifySectionOther => 'อื่น ๆ'; @override - String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; + String weatherRankingMeta(String time, int count) { + return 'เวลาข้อมูล: $time\n$count สถานี'; + } @override - String get mapLayerSatelliteGlobalOutline => 'Country border'; + String get onboardingTermsAgree => + 'ฉันได้อ่านและยอมรับข้อกำหนดการให้บริการแล้ว'; @override - String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + String get mapLayerSatelliteTransparentNoVegetation => + 'Below 0.1 = transparent (no vegetation)'; @override - String get mapLayerSatelliteCloudClear => 'Clear'; + String get notifyOptLocalIntensity4 => 'ความรุนแรงในพื้นที่ระดับ 4 ขึ้นไป'; @override - String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + String get eewArrived => 'มาถึงแล้ว'; @override - String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => 'Cloudy'; + String get mapLayerCategoryLife => 'ชีวิตประจำวัน'; @override - String get mapLayerSatelliteTransparentWarm => - 'Clear sky (warm end) = transparent, the basemap shows'; + String get reportFilterSortIntensity => 'ความเข้ม'; @override - String get mapLayerSatelliteTransparentReflectance => - 'Low reflectance / night = transparent, the basemap shows'; + String get typhoonMotion => 'เคลื่อนที่'; @override - String get mapLayerSatelliteTransparentZero => - 'Zero difference = transparent (no signal)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => - 'Night = transparent, the basemap shows'; + String get typhoonIntensityIntense => 'Intense typhoon'; @override - String get mapLayerSatelliteTransparentNoData => - 'No data (land) = transparent'; + String get mapLayerOrderTitle => 'จัดเรียงเลเยอร์'; @override - String get mapLayerSatelliteTransparentNoVegetation => - 'Below 0.1 = transparent (no vegetation)'; + String get dpmYes => 'ใช่'; @override - String get mapLayerSatelliteTransparentNoWater => - '≤ 0 = transparent (no water)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => - 'Clear sky = transparent, the basemap shows'; + String get reportDetailLocalIntensityUnavailable => 'ไม่มีข้อมูลความเข้ม'; @override - String get mapLayerStyleSection => 'Colour style'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => 'Colour style'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => 'Grayscale (JMA)'; + String get reportFilterDepth => 'ความลึก'; @override - String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; + String get onboardingScrollHint => 'เลื่อนลงเพื่อดำเนินการต่อ'; @override - String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + String get mapNavQpesums => 'พยากรณ์'; @override - String get mapLayerStyleJmaTooltip => - 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; + String get navMap => 'แผนที่'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => 'การแจ้งเตือนและประกาศสภาพอากาศ'; @override - String get mapLayerStyleBdTooltip => - 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; + String get reportFilterReset => 'รีเซ็ต'; @override - String get mapLayerQpesums => 'พยากรณ์ฝน 1 ชั่วโมงข้างหน้า'; + String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; @override - String get mapLayerLightning => 'ฟ้าผ่า'; + String get typhoonOverlaySectionStorm => 'Storm wind'; @override - String lightningLegendCg(int minutes) { - return 'เมฆสู่พื้น · $minutes นาที'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return 'เมฆสู่เมฆ · $minutes นาที'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => 'ตอนนี้'; + String get weatherDynamicStateSubtitle => 'แทนที่สภาพอากาศพื้นหลังหน้าแรก'; @override - String get mapTimelinePast => 'อดีต'; + String get reportFilterIntensityInfoModernTitle => 'แบบใหม่ (ตั้งแต่ 2020)'; @override - String get mapTimelineFuture => 'อนาคต'; + String typhoonDataTime(String time) { + return 'Data time\n$time'; + } @override - String get mapTimelineObserved => 'เวลาตรวจวัด'; + String get restroomTypeAccessible => 'ห้องน้ำคนพิการ'; @override - String get mapTimelineForecast => 'พยากรณ์'; + String get moreSectionAbout => 'เกี่ยวกับ'; @override - String mapTimelineDataTime(String time) { - return 'เวลาข้อมูล $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => 'การตั้งค่าการแจ้งเตือน'; + String get onboardingIntroBody => + 'DPIP คือเพื่อนคู่ใจด้านการป้องกันภัยพิบัติของคุณ รวมการเตือนแผ่นดินไหวล่วงหน้า รายงานแผ่นดินไหว สภาพอากาศ และข้อมูลภัยพิบัติต่าง ๆ ไว้ในที่เดียว และแจ้งเตือนคุณในช่วงเวลาสำคัญ\n\n• แผ่นดินไหว: การเตือนล่วงหน้า รายงานความรุนแรง และรายงานฉบับสมบูรณ์\n• สภาพอากาศ: ข้อความพายุฝนฟ้าคะนองแบบเรียลไทม์ และการแจ้งเตือนสภาพอากาศ\n• ข้อมูลสึนามิและภัยพิบัติ\n\nต่อไป เราจะขอให้คุณอ่านข้อกำหนดการให้บริการ และอนุญาตสิทธิ์บางอย่างเพื่อให้ DPIP สามารถปกป้องคุณได้แบบเรียลไทม์'; @override - String get notifyTitle => 'การแจ้งเตือน'; + String get shelterCapacityLabel => 'ความจุ'; @override - String get notifyUnavailable => - 'การแจ้งเตือนแบบพุชยังไม่พร้อม — โปรดลองอีกครั้งในภายหลัง'; + String get reportDetailImage => 'ภาพรายงานแผ่นดินไหว'; @override - String get notifySetFailed => 'ไม่สามารถบันทึกการตั้งค่าได้ โปรดลองอีกครั้ง'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => 'การเตือนแผ่นดินไหวล่วงหน้า'; + String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; @override - String get notifySectionEarthquake => 'แผ่นดินไหว'; + String get onboardingPermNotify => 'การแจ้งเตือน'; @override - String get notifySectionWeather => 'สภาพอากาศ'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => 'สึนามิ'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => 'อื่น ๆ'; + String get defaultMapLayerSettings => 'ชั้นแผนที่เริ่มต้น'; @override - String get notifyEew => 'การเตือนแผ่นดินไหวฉุกเฉิน'; + String get moreSectionNotify => 'การแจ้งเตือน'; @override - String get notifyMonitor => 'เครื่องเฝ้าระวังการสั่นสะเทือนรุนแรง'; + String get notifyUnavailable => + 'การแจ้งเตือนแบบพุชยังไม่พร้อม — โปรดลองอีกครั้งในภายหลัง'; @override - String get notifyReport => 'รายงานแผ่นดินไหว'; + String get mapLayerOrderReset => 'รีเซ็ตลำดับ'; @override - String get notifyIntensity => 'รายงานความรุนแรงแผ่นดินไหว'; + String get dpmAddress => 'ที่อยู่'; @override - String get notifyThunderstorm => 'การแจ้งเตือนพายุฝนฟ้าคะนอง'; + String get weatherRankingMergeCounty => 'อำเภอ/เมือง'; @override - String get notifyAdvisory => 'การแจ้งเตือนและประกาศสภาพอากาศ'; + String get moreSectionApp => 'ดาวน์โหลดแอป'; @override - String get notifyEvacuation => 'ข้อมูลภัยพิบัติ'; + String get reportFilterIntensityInfoLegacyBody => + 'มีระดับ 0–7 เท่านั้น ไม่แยก 5−/5+/6−/6+'; @override - String get notifyTsunami => 'ข้อมูลสึนามิ'; + String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; @override - String get notifyAnnouncement => 'ประกาศ'; + String get qpesumsOverlayMenuTooltip => 'ตัวเลือกชั้นพยากรณ์น้ำฝน'; @override - String get notifyOptOff => 'ปิด'; + String get mapTimelineFuture => 'อนาคต'; @override - String get notifyOptAll => 'รับทั้งหมด'; + String get typhoonLegendCircleAvg => 'Average circle'; @override - String get notifyOptLocalIntensity4 => 'ความรุนแรงในพื้นที่ระดับ 4 ขึ้นไป'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => 'ความรุนแรงในพื้นที่ระดับ 1 ขึ้นไป'; + String get typhoonLabelSe => 'SE'; @override - String get notifyOptWeatherLocal => 'เฉพาะตำแหน่งปัจจุบัน'; + String get radarTownOutlineHint => 'เส้นแบ่งย่อยกว่า'; @override - String get notifyOptTsunamiWarning => 'เฉพาะการเตือนภัยสึนามิ'; + String eewCountdown(int seconds) { + return '$seconds วินาที'; + } @override - String get notifyOptTsunamiAll => 'ข่าวสารและการเตือนภัยสึนามิ'; + String get typhoonLabelGust => 'Peak gust'; @override - String get onboardingNext => 'ถัดไป'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => 'ย้อนกลับ'; + String get sponsorTerms => 'ข้อกำหนดการใช้งาน'; @override - String get onboardingScrollHint => 'เลื่อนลงเพื่อดำเนินการต่อ'; + String get restroomTypeGenderNeutral => 'ห้องน้ำเป็นกลางทางเพศ'; @override - String get onboardingIntroTitle => 'ยินดีต้อนรับสู่ DPIP'; + String get notifyThunderstorm => 'การแจ้งเตือนพายุฝนฟ้าคะนอง'; @override - String get onboardingIntroBody => - 'DPIP คือเพื่อนคู่ใจด้านการป้องกันภัยพิบัติของคุณ รวมการเตือนแผ่นดินไหวล่วงหน้า รายงานแผ่นดินไหว สภาพอากาศ และข้อมูลภัยพิบัติต่าง ๆ ไว้ในที่เดียว และแจ้งเตือนคุณในช่วงเวลาสำคัญ\n\n• แผ่นดินไหว: การเตือนล่วงหน้า รายงานความรุนแรง และรายงานฉบับสมบูรณ์\n• สภาพอากาศ: ข้อความพายุฝนฟ้าคะนองแบบเรียลไทม์ และการแจ้งเตือนสภาพอากาศ\n• ข้อมูลสึนามิและภัยพิบัติ\n\nต่อไป เราจะขอให้คุณอ่านข้อกำหนดการให้บริการ และอนุญาตสิทธิ์บางอย่างเพื่อให้ DPIP สามารถปกป้องคุณได้แบบเรียลไทม์'; + String get skyTimeGolden => 'ช่วงเวลาทอง'; @override - String get onboardingTermsTitle => 'ข้อกำหนดการให้บริการ'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'โปรดอ่านข้อควรทราบต่อไปนี้ก่อนใช้งาน DPIP:\n\n• ข้อมูลทั้งหมดควรยึดตามเนื้อหาที่เผยแพร่โดยกรมอุตุนิยมวิทยากลาง (CWA) เป็นหลัก\n\n• ขึ้นอยู่กับสภาพเครือข่าย เซิร์ฟเวอร์ แอปพลิเคชัน และแหล่งข้อมูลต้นทาง อาจมีความเป็นไปได้ที่จะไม่ได้รับข้อมูล เราพยายามอย่างเต็มที่เพื่อหลีกเลี่ยงกรณีเช่นนี้ แต่ไม่สามารถรับประกันได้ว่าจะไม่เกิดขึ้น\n\n• การสั่นสะเทือนอย่างรุนแรงอาจมาถึงตำแหน่งของคุณก่อนการแจ้งเตือน\n\n• การเตือนแผ่นดินไหวล่วงหน้าเป็นผลจากการคำนวณอย่างรวดเร็ว ซึ่งอาจมีความคลาดเคลื่อนสูง โปรดทำความเข้าใจและใช้งานด้วยความระมัดระวัง\n\n• พฤติกรรมใด ๆ ที่ไม่ได้รับการรับรองจากหน่วยงานราชการอาจมีความเสี่ยงทางกฎหมาย โปรดปฏิบัติตามระเบียบที่เกี่ยวข้องทั้งหมด\n\nนอกจากนี้ เพื่อให้บริการการเตือนภัยเฉพาะพื้นที่ บริการนี้จะเก็บรวบรวมและอัปโหลดตำแหน่งโดยประมาณและตัวระบุการแจ้งเตือนแบบพุชของคุณ — ทั้งขณะทำงานเบื้องหน้าและเบื้องหลัง — เพื่อใช้ตัดสินว่าจะส่งการเตือนใดให้คุณเท่านั้น\n\nการแตะ \"ยอมรับและดำเนินการต่อ\" ถือว่าคุณได้อ่าน เข้าใจ และยอมรับข้อความข้างต้นแล้ว'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => - 'ฉันได้อ่านและยอมรับข้อกำหนดการให้บริการแล้ว'; + String weatherRankingAnalysisCurrent(String value) { + return 'ปัจจุบัน $value°C'; + } @override - String get onboardingAgreeContinue => 'ยอมรับและดำเนินการต่อ'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => 'การอนุญาตสิทธิ์'; + String get homeForecastUnavailable => 'เลือกพื้นที่เพื่อดูพยากรณ์'; @override - String get onboardingPermsBody => - 'เพื่อให้ DPIP แจ้งเตือนคุณได้ในทันทีที่เกิดภัยพิบัติ โปรดอนุญาตสิทธิ์ต่อไปนี้ คุณสามารถเปลี่ยนแปลงได้ทุกเมื่อในการตั้งค่าระบบ'; + String get mapLayers => 'ชั้นข้อมูล'; @override - String get onboardingPermNotify => 'การแจ้งเตือน'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => - 'ส่งการเตือนแผ่นดินไหว สภาพอากาศ และภัยพิบัติทันทีที่เกิดขึ้น'; + String get languageSettings => 'ภาษา'; @override - String get onboardingPermCritical => 'การแจ้งเตือนสำคัญ'; + String get dpmDisasterNuclear => 'อุบัติเหตุนิวเคลียร์'; @override - String get onboardingPermCriticalDesc => - 'ให้การเตือนแผ่นดินไหวที่เป็นอันตรายถึงชีวิตส่งเสียงได้ แม้อยู่ในโหมดเงียบหรือโหมดห้ามรบกวน'; + String get language => 'ภาษา'; @override - String get onboardingPermLocation => 'ตำแหน่งที่ตั้ง'; + String homeForecastFeelsLike(String temp) { + return 'รู้สึกเหมือน $temp°'; + } @override - String get onboardingPermLocationDesc => 'ส่งการเตือนภัยตามตำแหน่งที่คุณอยู่'; + String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; @override - String get onboardingPermBackground => 'ตำแหน่งที่ตั้งเบื้องหลัง'; + String get skyTimeDawn => 'รุ่งอรุณ'; @override - String get onboardingPermBackgroundDesc => - 'อนุญาต \"ทุกครั้ง\" เพื่อให้การเตือนภัยยังส่งถึงคุณได้แม้ปิดแอป'; + String get skyTimeAfternoon => 'ตอนบ่าย'; @override - String get onboardingPermBattery => 'ยกเว้นการประหยัดแบตเตอรี่'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'อนุญาตให้ DPIP ทำงานเบื้องหลังอย่างต่อเนื่อง เพื่อไม่ให้การเตือนภัยล่าช้าหรือพลาดไป'; + String get typhoonWarningTitle => 'ประกาศเตือนไต้ฝุ่น'; @override - String get onboardingGrant => 'อนุญาต'; + String get moreSourceCode => 'ซอร์สโค้ด'; @override - String get onboardingGranted => 'อนุญาตแล้ว'; + String get mapLayerCategoryWeather => 'การสังเกตสภาพอากาศ'; @override - String get onboardingStart => 'เริ่มใช้งาน'; + String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; @override - String get language => 'ภาษา'; + String get windForecastTownOutlineHint => 'ตาข่ายที่ละเอียดกว่า'; @override - String get languageSettings => 'ภาษา'; + String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; @override - String get languageSystem => 'ค่าเริ่มต้นของระบบ'; + String get mapAppCopyCoordinates => 'คัดลอกพิกัด'; @override - String get locationBannerServiceOff => - 'บริการระบุตำแหน่งถูกปิด — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้'; + String get reportFilterIntensityInfoIntro => + 'CWA เปลี่ยนมาตรวัดเมื่อ 1 ม.ค. 2020 (เวลาไทเป)'; @override - String get locationBannerPermission => - 'ยังไม่ได้อนุญาตสิทธิ์ตำแหน่งที่ตั้ง — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้'; + String get mapNavEarthquake => 'แผ่นดินไหว'; @override - String get locationBannerFix => 'เปิดการตั้งค่า'; + String get typhoonGust => 'ลมกระโชก'; @override - String get notifyBannerDisabled => - 'ปิดการแจ้งเตือนอยู่ — คุณจะไม่ได้รับการเตือนภัยพิบัติ'; + String get restroomGradeAverage => 'ปานกลาง'; @override - String get onboardingSkipTitle => 'ยังไม่ได้ให้สิทธิ์'; + String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; @override - String get onboardingSkipBody => - 'หากไม่อนุญาตตำแหน่งและการแจ้งเตือน DPIP จะไม่สามารถแจ้งเตือนแผ่นดินไหวและภัยพิบัติใกล้คุณแบบเรียลไทม์ได้ คุณยังสามารถเปิดใช้ภายหลังได้ในการตั้งค่า'; + String get onboardingPermBackgroundDesc => + 'อนุญาต \"ทุกครั้ง\" เพื่อให้การเตือนภัยยังส่งถึงคุณได้แม้ปิดแอป'; @override - String get onboardingSkipStay => 'กลับไปให้สิทธิ์'; + String get mapTimelineForecast => 'พยากรณ์'; @override - String get onboardingSkipLeave => 'ข้ามไปก่อน'; + String get restroomTypeLabel => 'ประเภท'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => 'แผ่นดินไหว'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => + 'Level-10 wind field + average circle (yellow)'; @override - String get moreSourceCode => 'ซอร์สโค้ด'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => 'ดาวน์โหลดแอป'; + String get reportDetailTitle => 'รายงานแผ่นดินไหว'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'รายงานการตรวจจับ TREM'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · เวลาข้อมูล $time'; + } @override - String get displaySettings => 'การแสดงผล'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => 'ชั้นแผนที่เริ่มต้น'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - 'แท็บแผนที่จะเปิดชั้นนี้ ไอคอนและป้ายนำทางด้านล่างจะเปลี่ยนตาม'; + String get radarCountyOutline => 'เส้นแบ่งเขตจังหวัด'; @override - String get mapNavRadar => 'เรดาร์'; + String get onboardingGranted => 'อนุญาตแล้ว'; @override - String get mapNavQpesums => 'พยากรณ์'; + String get commonClose => 'ปิด'; @override - String get mapNavSatellite => 'ดาวเทียม'; + String get restroomGradeLabel => 'ระดับ'; @override - String get mapNavLightning => 'ฟ้าผ่า'; + String get rainIntervalNow => 'วันนี้'; @override - String get mapNavTyphoon => 'ไต้ฝุ่น'; + String get changelogCurrentVersion => 'ปัจจุบัน'; @override - String get mapNavEarthquake => 'แผ่นดินไหว'; + String get typhoonLabelPressure => 'Central pressure'; @override - String get mapNavTemperature => 'อุณหภูมิ'; + String get typhoonOverlayForecastCalloutsTooltip => + 'Show forecast-point detail cards when zoomed in'; @override - String get mapNavHumidity => 'ความชื้น'; + String get aedOpenRemark => 'หมายเหตุเวลาเปิด'; @override - String get mapNavPressure => 'ความกดอากาศ'; + String get onboardingPermsBody => + 'เพื่อให้ DPIP แจ้งเตือนคุณได้ในทันทีที่เกิดภัยพิบัติ โปรดอนุญาตสิทธิ์ต่อไปนี้ คุณสามารถเปลี่ยนแปลงได้ทุกเมื่อในการตั้งค่าระบบ'; @override - String get mapNavWind => 'ทิศลม'; + String get typhoonOverlaySectionWeather => 'Weather underlay'; + + @override + String get notifyOptWeatherLocal => 'เฉพาะตำแหน่งปัจจุบัน'; @override String get mapNavRain => 'ฝน'; @override - String get mapNavDisaster => 'ป้องกันภัย'; + String get moonDays => 'days'; @override - String get displayTheme => 'ธีม'; + String mapLegendUnit(String unit) { + return 'หน่วย: $unit'; + } @override - String get themeSystem => 'ระบบ'; + String get weatherModeClear => 'ท้องฟ้าแจ่มใส'; @override - String get themeLight => 'สว่าง'; + String get meshtasticRadio => 'Radio'; @override - String get themeDark => 'มืด'; + String get commonEmpty => 'ไม่มีข้อมูล'; @override - String get moreSectionAbout => 'เกี่ยวกับ'; + String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; @override - String get termsOfService => 'ข้อกำหนดในการให้บริการ'; + String get meshtasticExternalPower => 'External power'; @override - String get faq => 'คำถามที่พบบ่อย'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get openSourceLicenses => 'ใบอนุญาตโอเพนซอร์ส'; + String get reportFilterOrderAsc => 'น้อย→มาก'; @override - String get sponsorTitle => 'สนับสนุน DPIP'; + String get reportFilterApply => 'ใช้'; @override - String get sponsorIntro => - 'DPIP มุ่งมั่นให้ข้อมูลการป้องกันภัยพิบัติแบบเรียลไทม์ โดยไม่มีโฆษณาหรือรูปแบบหารายได้อื่น การสนับสนุนของคุณช่วยให้เรารักษาเซิร์ฟเวอร์และพัฒนาต่อไปได้'; + String get reportDetailImageUnavailable => 'ยังไม่มีภาพรายงาน'; @override - String get sponsorSubscriptions => 'แบบสมัครสมาชิก'; + String get weatherRankingHighest => 'สูงสุด'; @override - String get sponsorRecommended => 'แนะนำ'; + String get reportDetailReplay => 'เล่นย้อนหลัง'; @override - String get sponsorOneTime => 'สนับสนุนครั้งเดียว'; + String get mapLayerRestroom => 'ห้องน้ำสาธารณะ'; @override - String sponsorPerMonth(String price) { - return '$price / เดือน'; - } + String get restroomCategoryWelfare => 'สถานสงเคราะห์'; @override - String get sponsorRestore => 'กู้คืนการซื้อ'; + String get restroomGradeExcellent => 'ดีเยี่ยม'; @override - String get sponsorTerms => 'ข้อกำหนดการใช้งาน'; + String get meshtasticLastSent => 'Last sent'; @override - String get sponsorPrivacy => 'นโยบายความเป็นส่วนตัว'; + String get meshtasticName => 'Name'; @override - String get sponsorRestoring => 'กำลังกู้คืนการซื้อ…'; + String get meshtasticScan => 'Scan'; + + @override + String get mapLayerCategoryForecast => 'การพยากรณ์เชิงตัวเลข'; + + @override + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; + + @override + String get themeSystem => 'ระบบ'; + + @override + String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + + @override + String get typhoonLegendForecast => 'เส้นทางพยากรณ์'; + + @override + String typhoonValueHpa(String n) { + return '$n hPa'; + } + + @override + String get weatherPrecipitation => 'ปริมาณน้ำฝน'; + + @override + String get moonNextFullMoon => 'Next full moon'; + + @override + String get dpmSheetEmpty => 'แตะเครื่องหมายบนแผนที่เพื่อดูรายละเอียด'; + + @override + String get onboardingSkipLeave => 'ข้ามไปก่อน'; + + @override + String get onboardingBack => 'ย้อนกลับ'; + + @override + String get aedPlaceDesc => 'ตำแหน่งติดตั้ง'; + + @override + String get onboardingSkipTitle => 'ยังไม่ได้ให้สิทธิ์'; + + @override + String get restroomTypeFamily => 'ห้องน้ำครอบครัว'; + + @override + String typhoonValueKm(String n) { + return '$n km'; + } + + @override + String get typhoonPressure => 'ความกดอากาศ'; + + @override + String get onboardingPermBattery => 'ยกเว้นการประหยัดแบตเตอรี่'; + + @override + String get typhoonLabelNw => 'NW'; + + @override + String get dpmDisasterFlood => 'น้ำท่วม'; + + @override + String get moonPhaseWaxingCrescent => 'Waxing crescent'; + + @override + String get restroomCategoryLeisure => 'สถานที่พักผ่อนหย่อนใจ'; + + @override + String get mapLayerTemperature => 'อุณหภูมิ'; + + @override + String get aedCategory => 'หมวดหมู่'; + + @override + String get meshtasticChannels => 'Channels'; + + @override + String get monitorWaiting => 'กำลังรอข้อมูล…'; + + @override + String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + + @override + String get reportDetailEpicenter => 'พิกัดศูนย์กลาง'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => 'ลม'; + + @override + String get reportDetailMagnitude => 'ขนาดแผ่นดินไหว'; + + @override + String get reportDetailAreaIntensity => 'ความเข้มแยกตามพื้นที่'; + + @override + String get rainInterval12h => '12 ชม.'; + + @override + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } + + @override + String get dpmDisasterLandslide => 'ดินถล่ม'; + + @override + String get notifyMonitor => 'เครื่องเฝ้าระวังการสั่นสะเทือนรุนแรง'; + + @override + String get onboardingStart => 'เริ่มใช้งาน'; + + @override + String sponsorPerMonth(String price) { + return '$price / เดือน'; + } + + @override + String get mapLayerPressure => 'ความกดอากาศ'; + + @override + String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + + @override + String get mapLayerSatelliteTransparentZero => + 'Zero difference = transparent (no signal)'; + + @override + String get shelterIndoorLabel => 'การอพยพในอาคาร'; + + @override + String get notifyOptOff => 'ปิด'; + + @override + String get reportFilterSortTime => 'เวลา'; + + @override + String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + + @override + String get weatherModeThunderstorm => 'พายุฝนฟ้าคะนอง'; + + @override + String get homeViewOnMap => 'ดูบนแผนที่'; + + @override + String get reportFilterIntensityInfoLegacyTitle => 'แบบเก่า (ก่อน 2020)'; + + @override + String get typhoonLabelSpeed => 'Past movement speed'; + + @override + String mapAppOpenFailed(String app) { + return 'ไม่สามารถเปิด $app ได้'; + } + + @override + String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + + @override + String get meshtasticReceived => 'Received'; + + @override + String get weatherRankingExtremeLow => 'ต่ำสุดวันนี้'; + + @override + String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + + @override + String get mapLayerSatelliteTransparentNoWater => + '≤ 0 = transparent (no water)'; + + @override + String get shelterCategoryLabel => 'ประเภทภัยพิบัติ'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => 'ลมกระโชก'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => 'ประเภทภัยพิบัติของศูนย์อพยพ'; + + @override + String get moreServerStatus => 'สถานะเซิร์ฟเวอร์'; + + @override + String get notifySectionWeather => 'สภาพอากาศ'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => 'แผ่นดินไหว'; + + @override + String get changelogBodyEmpty => 'ไม่มีคำอธิบายสำหรับรุ่นนี้'; + + @override + String get radarGlobalOutline => 'เส้นแบ่งเขตประเทศ'; + + @override + String get notifyEew => 'การเตือนแผ่นดินไหวฉุกเฉิน'; + + @override + String get regionNationwide => 'ทั่วประเทศ'; + + @override + String get moreNotifyLog => 'บันทึกการส่งการแจ้งเตือนของ DPIP'; + + @override + String get regionCurrent => 'ตำแหน่งปัจจุบัน'; + + @override + String get dpmFilterSectionRestroom => 'ประเภทสถานที่'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => 'หิมะตก'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => 'ข้อมูลดีบัก'; + + @override + String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => 'ฟ้าผ่า'; + + @override + String get homeForecastEmpty => 'ไม่มีข้อมูลพยากรณ์'; + + @override + String get sponsorOneTime => 'สนับสนุนครั้งเดียว'; + + @override + String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + + @override + String get onboardingPermBackground => 'ตำแหน่งที่ตั้งเบื้องหลัง'; + + @override + String get aedEmergencyPhone => 'โทรศัพท์ฉุกเฉิน'; + + @override + String get dpmOpenInMaps => 'เปิดในแผนที่'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + 'ให้การเตือนแผ่นดินไหวที่เป็นอันตรายถึงชีวิตส่งเสียงได้ แม้อยู่ในโหมดเงียบหรือโหมดห้ามรบกวน'; + + @override + String get mapLayerSatelliteTransparentWarm => + 'Clear sky (warm end) = transparent, the basemap shows'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => 'พยากรณ์ 24 ชั่วโมง'; + + @override + String get typhoonLegendWarningAreas => 'พื้นที่เตือนภัย'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; + } + + @override + String get notifyOptLocalIntensity1 => 'ความรุนแรงในพื้นที่ระดับ 1 ขึ้นไป'; + + @override + String get mapTimelinePast => 'อดีต'; + + @override + String get restroomTypeFemale => 'ห้องน้ำหญิง'; + + @override + String get reportListToday => 'วันนี้'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => 'กำลังโหลด…'; + + @override + String get typhoonIntensityModerate => 'Moderate typhoon'; + + @override + String get typhoonWind => 'ความเร็วลม'; + + @override + String get mapLayerSatelliteAsh => 'Himawari Ash'; + + @override + String get rainInterval3h => '3 ชม.'; + + @override + String get reportListSearch => 'ค้นหา'; + + @override + String get mapLayerCategorySatellite => 'ดาวเทียม'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => 'สถานที่'; + + @override + String get mapLayerSatelliteNightmicrophysics => + 'Himawari Night Microphysics'; + + @override + String get typhoonIntensityTd => 'Tropical depression'; + + @override + String get reportFilterDate => 'วันที่'; @override String get sponsorRestoreUnavailable => 'ไม่สามารถเชื่อมต่อร้านค้าได้ โปรดลองอีกครั้งภายหลัง'; @override - String get commonClose => 'ปิด'; + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => 'ยังไม่มีพื้นที่ที่บันทึกไว้'; + + @override + String get onboardingPermBatteryDesc => + 'อนุญาตให้ DPIP ทำงานเบื้องหลังอย่างต่อเนื่อง เพื่อไม่ให้การเตือนภัยล่าช้าหรือพลาดไป'; + + @override + String get mapNavDisaster => 'ป้องกันภัย'; + + @override + String get radarScanRangeSubtitle => + 'แสดงพื้นที่ที่เรดาร์ทั้งสี่ตรวจวัดได้จริง'; + + @override + String get aedHoursSunday => 'เวลาวันอาทิตย์'; + + @override + String get reportDetailOriginTime => 'เวลาเกิดเหตุ'; + + @override + String get trendNoData => 'ไม่มีข้อมูลแนวโน้ม'; + + @override + String get onboardingPermLocation => 'ตำแหน่งที่ตั้ง'; + + @override + String get moreDiscord => 'ชุมชน Discord'; + + @override + String get mapNavPressure => 'ความกดอากาศ'; + + @override + String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => 'ยังไม่มีบันทึกการเผยแพร่'; + + @override + String get reportFilterDateStartNote => 'วันเริ่ม: 00:00 ของวันนั้น(ไทเป)'; + + @override + String get eewTitle => 'การเตือนแผ่นดินไหวล่วงหน้า'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return 'เลือกแล้ว $count/$max'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => 'ฟ้าปิด'; + + @override + String get reportDetailDepth => 'ความลึกจุดศูนย์กลาง'; + + @override + String get typhoonOverlayWarningTooltip => + 'Highlight counties under a typhoon warning'; + + @override + String get reportFilterDatePick => 'เลือกวันที่'; + + @override + String get onboardingSkipStay => 'กลับไปให้สิทธิ์'; + + @override + String get commonFetchFailed => 'ไม่สามารถโหลดข้อมูลได้ โปรดลองอีกครั้ง'; + + @override + String get shelterOutdoorLabel => 'การอพยพกลางแจ้ง'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => 'เรดาร์'; + + @override + String get mapLayerSatelliteCloudClear => 'Clear'; + + @override + String eewSummary(String magnitude, String depth) { + return 'ขนาด $magnitude · ความลึก $depth กม.'; + } + + @override + String get locationBannerPermission => + 'ยังไม่ได้อนุญาตสิทธิ์ตำแหน่งที่ตั้ง — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้'; + + @override + String get typhoonOverlayWeatherNoneTooltip => + 'No radar or infrared underlay'; + + @override + String get radarCountyOutlineHint => 'วาดทับภาพเอคโค'; + + @override + String get windForecastCountyOutlineHint => 'วาดทับบนสนามลม'; + + @override + String get homeRainTrendTitle => 'ฝนชั่วโมงถัดไป'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => 'พายุไต้ฝุ่น'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => 'ห้องน้ำรวม'; + + @override + String get restroomGradeGood => 'ดี'; + + @override + String get notifyTsunami => 'ข้อมูลสึนามิ'; + + @override + String get navData => 'ข้อมูล'; + + @override + String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => 'อุปกรณ์นี้ไม่สามารถโทรออกได้'; + + @override + String get reportFilterAny => 'ทั้งหมด'; + + @override + String get weatherRankingMergeTo => 'รวม'; + + @override + String get notifyIntensity => 'รายงานความรุนแรงแผ่นดินไหว'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => 'ช่วงสะสม'; + + @override + String get reportDetailLocalFelt => 'แผ่นดินไหวรู้สึกได้เฉพาะพื้นที่'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => 'อนุญาต'; + + @override + String get weatherModeRain => 'ฝนตก'; + + @override + String get shelterVulnerableOkLabel => 'เหมาะกับผู้เปราะบาง'; + + @override + String get stationSheetEmpty => 'แตะสถานีเพื่อดูค่าที่วัดได้'; + + @override + String get typhoonLegendProbability => 'โอกาสกระทบ'; + + @override + String get reportFilterMagnitude => 'ขนาด'; + + @override + String get skyTimeMorning => 'ตอนเช้า'; + + @override + String get experimentalFeatures => 'ฟีเจอร์ทดลอง'; + + @override + String get onboardingTermsBody => + 'โปรดอ่านข้อควรทราบต่อไปนี้ก่อนใช้งาน DPIP:\n\n• ข้อมูลทั้งหมดควรยึดตามเนื้อหาที่เผยแพร่โดยกรมอุตุนิยมวิทยากลาง (CWA) เป็นหลัก\n\n• ขึ้นอยู่กับสภาพเครือข่าย เซิร์ฟเวอร์ แอปพลิเคชัน และแหล่งข้อมูลต้นทาง อาจมีความเป็นไปได้ที่จะไม่ได้รับข้อมูล เราพยายามอย่างเต็มที่เพื่อหลีกเลี่ยงกรณีเช่นนี้ แต่ไม่สามารถรับประกันได้ว่าจะไม่เกิดขึ้น\n\n• การสั่นสะเทือนอย่างรุนแรงอาจมาถึงตำแหน่งของคุณก่อนการแจ้งเตือน\n\n• การเตือนแผ่นดินไหวล่วงหน้าเป็นผลจากการคำนวณอย่างรวดเร็ว ซึ่งอาจมีความคลาดเคลื่อนสูง โปรดทำความเข้าใจและใช้งานด้วยความระมัดระวัง\n\n• พฤติกรรมใด ๆ ที่ไม่ได้รับการรับรองจากหน่วยงานราชการอาจมีความเสี่ยงทางกฎหมาย โปรดปฏิบัติตามระเบียบที่เกี่ยวข้องทั้งหมด\n\nนอกจากนี้ เพื่อให้บริการการเตือนภัยเฉพาะพื้นที่ บริการนี้จะเก็บรวบรวมและอัปโหลดตำแหน่งโดยประมาณและตัวระบุการแจ้งเตือนแบบพุชของคุณ — ทั้งขณะทำงานเบื้องหน้าและเบื้องหลัง — เพื่อใช้ตัดสินว่าจะส่งการเตือนใดให้คุณเท่านั้น\n\nการแตะ \"ยอมรับและดำเนินการต่อ\" ถือว่าคุณได้อ่าน เข้าใจ และยอมรับข้อความข้างต้นแล้ว'; + + @override + String get reportFilterTitle => 'ตัวกรอง'; + + @override + String get onboardingPermCritical => 'การแจ้งเตือนสำคัญ'; + + @override + String trendCumulativeTotal(String total) { + return 'สะสม $total มม.'; + } + + @override + String get languageName => 'ไทย'; + + @override + String get reportListEmptyFiltered => 'ไม่มีรายงานที่ตรงกับเงื่อนไข'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => 'ไต้ฝุ่น'; + + @override + String get weatherModeSand => 'ฝุ่นทราย'; + + @override + String get typhoonSatelliteTitle => 'ดาวเทียม'; + + @override + String get notifyReport => 'รายงานแผ่นดินไหว'; + + @override + String get mapAppCoordinatesCopied => 'คัดลอกพิกัดแล้ว'; + + @override + String get skyTimeNight => 'กลางคืน'; + + @override + String get sponsorRecommended => 'แนะนำ'; + + @override + String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + + @override + String get weatherRankingWind => 'ความเร็วลม'; + + @override + String get feedStale => 'ข้อมูลอาจล้าสมัย'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · แรง $level'; + } + + @override + String get navHome => 'หน้าแรก'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => 'ใบอนุญาตโอเพนซอร์ส'; + + @override + String get weatherRankingLowest => 'ต่ำสุด'; + + @override + String get reportFilterSortDepth => 'ความลึก'; + + @override + String mapTimelineDataTime(String time) { + return 'เวลาข้อมูล $time'; + } + + @override + String get radarScanRange => 'แสดงขอบเขตการสแกน'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return 'ช่วง $value°C'; + } + + @override + String get weatherRankingExtremeHigh => 'สูงสุดวันนี้'; + + @override + String get changelogVersionDetails => 'รายละเอียดเวอร์ชัน'; + + @override + String get sponsorPrivacy => 'นโยบายความเป็นส่วนตัว'; + + @override + String get reportDetailLocalIntensity => 'ความเข้มที่ตำแหน่งของคุณ'; + + @override + String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; + + @override + String shelterCapacityValue(int n) { + return '$n คน'; + } + + @override + String lightningLegendCc(int minutes) { + return 'เมฆสู่เมฆ · $minutes นาที'; + } @override - String get mapLayerTemperature => 'อุณหภูมิ'; + String get meshtasticSendHint => 'Message to broadcast'; @override - String get trendRange24h => '24 ชม.'; + String monitorDelay(String value) { + return 'หน่วงเวลา $value s'; + } @override - String get trendRange7d => '7 วัน'; + String get dpmNo => 'ไม่ใช่'; @override - String get trendNoData => 'ไม่มีข้อมูลแนวโน้ม'; + String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; @override - String trendCumulativeTotal(String total) { - return 'สะสม $total มม.'; + String get meshtasticReconnecting => 'Reconnecting…'; + + @override + String get radarTownOutlineSubtitle => + 'ทำให้เส้นแบ่งเขตอำเภอยังอ่านออกใต้ภาพเอคโคเรดาร์'; + + @override + String get typhoonOverlayWeatherSatelliteTooltip => + 'Infrared closest to the typhoon bulletin time'; + + @override + String get radarScanRangeHint => 'นอกกรอบคือไม่ได้ตรวจวัด'; + + @override + String typhoonPickerTd(String no) { + return 'Tropical depression TD $no'; } @override - String chartHourLabel(int hour) { - return '$hourน.'; + String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + + @override + String get regionAddButton => 'เพิ่มพื้นที่'; + + @override + String get displaySettings => 'การแสดงผล'; + + @override + String get restroomGradePoor => 'ต่ำกว่ามาตรฐาน'; + + @override + String get restroomCategoryTourist => 'แหล่งท่องเที่ยว'; + + @override + String get locationBannerServiceOff => + 'บริการระบุตำแหน่งถูกปิด — ไม่สามารถส่งการเตือนภัยเฉพาะพื้นที่ของคุณได้'; + + @override + String get mapLayerStyleTooltip => 'Colour style'; + + @override + String lightningLegendCg(int minutes) { + return 'เมฆสู่พื้น · $minutes นาที'; } @override - String get mapLayerHumidity => 'ความชื้น'; + String get skyTimeAuto => 'อัตโนมัติ'; @override - String get mapLayerPressure => 'ความกดอากาศ'; + String get appLogs => 'บันทึกแอป'; @override - String get mapLayerWind => 'ลม'; + String get feedConnecting => 'กำลังเชื่อมต่อ…'; @override - String get mapLayerRain => 'ปริมาณฝน'; + String get notifyBannerDisabled => + 'ปิดการแจ้งเตือนอยู่ — คุณจะไม่ได้รับการเตือนภัยพิบัติ'; @override - String get rainIntervalMenu => 'ช่วงสะสม'; + String get weatherHumidity => 'ความชื้น'; @override - String get rainIntervalNow => 'วันนี้'; + String typhoonValueMs(String n) { + return '$n m/s'; + } @override - String get rainInterval10m => '10 นาที'; + String homeForecastHumidity(String value) { + return 'ความชื้น $value%'; + } @override - String get rainInterval1h => '1 ชม.'; + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; @override - String get rainInterval3h => '3 ชม.'; + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; @override - String get rainInterval6h => '6 ชม.'; + String get restroomCategoryTransport => 'การคมนาคม'; @override - String get rainInterval12h => '12 ชม.'; + String get reportFilterLocationHint => 'เช่น ฮวาเหลียน'; @override - String get rainInterval24h => '24 ชม.'; + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; @override - String get rainInterval2d => '2 วัน'; + String get meshtasticBattery => 'Battery'; @override - String get rainInterval3d => '3 วัน'; + String get meshtasticDistance => 'ระยะทาง'; @override - String get mapLayerTyphoon => 'ไต้ฝุ่น'; + String get meshtasticSnrTrend => 'แนวโน้มสัญญาณ (SNR)'; @override - String get typhoonNoActive => 'ไม่มีไต้ฝุ่น'; + String get meshtasticBatteryTrend => 'แนวโน้มแบตเตอรี่'; @override - String get typhoonWind => 'ความเร็วลม'; + String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; @override - String get typhoonGust => 'ลมกระโชก'; + String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; @override - String get typhoonPressure => 'ความกดอากาศ'; + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } @override - String get typhoonMotion => 'เคลื่อนที่'; + String get notifySectionEarthquake => 'แผ่นดินไหว'; @override - String get typhoonLabelPosition => 'Centre location'; + String get mapLayerDisasterMap => 'แผนที่ป้องกันภัย'; @override - String get typhoonLabelDirection => 'Past movement direction'; + String get weatherModeFog => 'หมอกหนา'; @override - String get typhoonLabelSpeed => 'Past movement speed'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } @override - String get typhoonLabelPressure => 'Central pressure'; + String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; @override - String get typhoonLabelWind => 'Max. sustained wind near centre'; + String get moreAnnouncements => 'ประกาศ'; @override - String get typhoonLabelGust => 'Peak gust'; + String get mapLayerSatelliteTransparentNoData => + 'No data (land) = transparent'; @override - String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; + String get restroomCategoryGovernment => 'สำนักงานราชการ'; @override - String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; + String get typhoonLegendCurrent => 'ศูนย์กลางปัจจุบัน'; + + @override + String get aedAddress => 'ที่อยู่'; + + @override + String get mapLayerAed => 'AED'; + + @override + String get changelogTypePrerelease => 'เบต้า'; + + @override + String get reportFilterIntensityInfoModernBody => + 'ระดับ 0–4, 5−, 5+, 6−, 6+, 7 แถบตัวกรองใช้แบบใหม่ เหตุการณ์เก่าในรายการยังแสดงป้ายแบบเก่า'; + + @override + String get typhoonOverlayWeatherNone => 'None'; + + @override + String get mapLayerStyleGray => 'Grayscale (JMA)'; + + @override + String get weatherModeAuto => 'อัตโนมัติ'; @override String get typhoonLabelProbCircle => '70% probability circle'; @override - String typhoonForecastLead(String hours) { - return 'Forecast +$hours h'; + String get notifyOptAll => 'รับทั้งหมด'; + + @override + String get displayTheme => 'ธีม'; + + @override + String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + + @override + String get typhoonLabelDirection => 'Past movement direction'; + + @override + String get regionManageTitle => 'พื้นที่ที่ใช้บ่อย'; + + @override + String get typhoonLegendCone => 'กรวยพยากรณ์'; + + @override + String get moreCwaEew => + 'การเตือนแผ่นดินไหวล่วงหน้าของกรมอุตุนิยมวิทยากลาง (CWA)'; + + @override + String get onboardingPermsTitle => 'การอนุญาตสิทธิ์'; + + @override + String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + + @override + String get rainInterval10m => '10 นาที'; + + @override + String weatherRankingAnalysisLow(String value) { + return 'ต่ำ $value'; + } + + @override + String get meshtasticConnectAnyway => 'Connect anyway'; + + @override + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => + 'Low reflectance / night = transparent, the basemap shows'; + + @override + String chartHourLabel(int hour) { + return '$hourน.'; + } + + @override + String get mapLayerShelter => 'ศูนย์อพยพ'; + + @override + String get typhoonOverlayProbabilityTooltip => + 'Show strike probability (hides the forecast cone)'; + + @override + String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => 'แสดงศูนย์อพยพ'; + + @override + String get mapNavHumidity => 'ความชื้น'; + + @override + String get reportDetailSortByIntensity => 'เรียงตามความเข้ม'; + + @override + String get homeRainTrendNoData => 'ไม่มีข้อมูล'; + + @override + String get mapLayerCategoryRadar => 'เรดาร์'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + + @override + String get typhoonTrackDetail => 'รายละเอียดเส้นทาง'; + + @override + String get dataSectionWeather => 'อากาศ'; + + @override + String get aedHoursWeekday => 'เวลาวันธรรมดา'; + + @override + String get homeActiveEventsTitle => 'เหตุการณ์ที่ยังมีผล'; + + @override + String weatherRankingAnalysisHigh(String value) { + return 'สูง $value'; } @override - String get typhoonLabelNw => 'NW'; + String get faq => 'คำถามที่พบบ่อย'; + + @override + String get typhoonHistoryLive => 'สด'; + + @override + String eewSerial(int serial) { + return 'รายงาน $serial'; + } + + @override + String get reportFilterSort => 'เรียงลำดับ'; + + @override + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; + + @override + String get dataEarthquakeSubtitle => 'รายงานแผ่นดินไหว'; @override - String get typhoonLabelNe => 'NE'; + String get typhoonNoActive => 'ไม่มีไต้ฝุ่น'; @override - String get typhoonLabelSw => 'SW'; + String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; @override - String get typhoonLabelSe => 'SE'; + String get navEvents => 'เหตุการณ์'; @override - String typhoonValueLat(String lat) { - return '$lat°N'; - } + String get onboardingTermsTitle => 'ข้อกำหนดการให้บริการ'; @override - String typhoonValueLon(String lon) { - return '$lon°E'; - } + String get mapTownLabels => 'ชื่อตำบล'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => 'ไม่สามารถบันทึกการตั้งค่าได้ โปรดลองอีกครั้ง'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '$n m/s'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return 'Data time\n$time'; - } + String get notifyAnnouncement => 'ประกาศ'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'ยินดีต้อนรับสู่ DPIP'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => 'ไม่สามารถระบุตำแหน่งปัจจุบันได้'; @override - String get mapLayerMonitor => 'เครื่องตรวจแผ่นดินไหว'; + String get languageSystem => 'ค่าเริ่มต้นของระบบ'; @override - String get mapLayerDisasterMap => 'แผนที่ป้องกันภัย'; + String get skyTimeSunset => 'พระอาทิตย์ตก'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'Himawari Dust'; @override - String get disasterMapOverlayMenuTooltip => 'ชั้นแผนที่ป้องกันภัย'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => 'ชั้น'; + String get regionEdit => 'แก้ไข'; @override - String get disasterMapOverlayAedTooltip => 'แสดงตำแหน่ง AED'; + String get weatherDynamicState => 'แอนิเมชันสภาพอากาศ'; @override - String get aedAddress => 'ที่อยู่'; + String get mapPlaceholderDisabled => 'แผนที่ (ปิดใช้งานชั่วคราว)'; @override - String get aedRegion => 'พื้นที่'; + String get moonNow => 'ตอนนี้'; @override - String get aedCategory => 'หมวดหมู่'; + String get moonSectionAppearance => 'ลักษณะ'; @override - String get aedType => 'ประเภท'; + String get moonSectionRiseSet => 'จันทร์ขึ้นและตก'; @override - String get aedPlaceDesc => 'ตำแหน่งติดตั้ง'; + String get moonSectionUpcoming => 'ถัดไป'; @override - String get aedDescription => 'หมายเหตุ'; + String get moonSectionCalendar => 'ปฏิทิน'; @override - String get aedHoursWeekday => 'เวลาวันธรรมดา'; + String get moonDistance => 'ระยะทาง'; @override - String get aedHoursSaturday => 'เวลาวันเสาร์'; + String get moonKilometres => 'กม.'; @override - String get aedHoursSunday => 'เวลาวันอาทิตย์'; + String get moonApparentSize => 'ขนาดปรากฏ'; @override - String get aedOpenRemark => 'หมายเหตุเวลาเปิด'; + String get moonRise => 'จันทร์ขึ้น'; @override - String get aedEmergencyPhone => 'โทรศัพท์ฉุกเฉิน'; + String get moonSet => 'จันทร์ตก'; @override - String get mapLayerRestroom => 'ห้องน้ำสาธารณะ'; + String get moonNextNewMoon => 'นิวมูนครั้งถัดไป'; @override - String get mapLayerShelter => 'ศูนย์อพยพ'; + String get moonAlwaysUp => 'อยู่เหนือขอบฟ้าทั้งวัน'; @override - String get disasterMapOverlayRestroomTooltip => 'แสดงห้องน้ำสาธารณะ'; + String get moonNoEvent => 'ไม่มีในวันนี้'; @override - String get disasterMapOverlayShelterTooltip => 'แสดงศูนย์อพยพ'; + String get sunTitle => 'ดวงอาทิตย์'; @override - String get dpmOpenInMaps => 'เปิดในแผนที่'; + String get sunSubtitle => 'พระอาทิตย์ขึ้น สนธยา และปักษ์'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => 'แสงกลางวัน'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => 'สนธยา'; @override - String mapAppDefault(String app) { - return '$app (ค่าเริ่มต้น)'; - } + String get sunSectionLight => 'แสง'; @override - String get mapAppCopyCoordinates => 'คัดลอกพิกัด'; + String get sunSectionSundial => 'นาฬิกาแดด'; @override - String get mapAppCoordinatesCopied => 'คัดลอกพิกัดแล้ว'; + String get sunSectionTerms => 'ปักษ์'; @override - String mapAppOpenFailed(String app) { - return 'ไม่สามารถเปิด $app ได้'; - } + String get sunRise => 'พระอาทิตย์ขึ้น'; @override - String get mapAppCallFailed => 'อุปกรณ์นี้ไม่สามารถโทรออกได้'; + String get sunSet => 'พระอาทิตย์ตก'; @override - String get mapOverlaySectionReference => 'เลเยอร์อ้างอิง'; + String get sunNoon => 'เที่ยงสุริยะ'; @override - String get mapLayerCategoryEarthquake => 'แผ่นดินไหว'; + String get sunDayLength => 'ความยาววัน'; @override - String get mapLayerCategoryTyphoon => 'พายุไต้ฝุ่น'; + String get sunTwilightCivil => 'พลเรือน'; @override - String get mapLayerCategoryWeather => 'การสังเกตสภาพอากาศ'; + String get sunTwilightNautical => 'เดินเรือ'; @override - String get mapLayerCategorySatellite => 'ดาวเทียม'; + String get sunTwilightAstronomical => 'ดาราศาสตร์'; @override - String get mapLayerCategoryRadar => 'เรดาร์'; + String get sunGoldenHourMorning => 'โกลเดนอาวร์เช้า'; @override - String get mapLayerCategoryLife => 'ชีวิตประจำวัน'; + String get sunGoldenHourEvening => 'โกลเดนอาวร์เย็น'; @override - String get mapLayerCategoryForecast => 'การพยากรณ์เชิงตัวเลข'; + String get sunBlueHour => 'บลูอาวร์'; @override - String get mapOverlaySectionMap => 'แผนที่'; + String get sunEquationOfTime => 'สมการเวลา'; @override - String get rainIntervalSection => 'ช่วงเวลา'; + String get sunMinutes => 'นาที'; @override - String get mapTownLabels => 'ชื่อตำบล'; + String get solarTermNext => 'ปักษ์ถัดไป'; @override - String get mapTownLabelsHint => 'แสดงชื่อตำบลเมื่อขยายแผนที่'; + String get planetsTitle => 'ดาวเคราะห์'; @override - String get mapTerrainRelief => 'ความนูนของภูมิประเทศ'; + String get planetsSubtitle => 'คืนนี้อยู่ไหน สว่างแค่ไหน'; @override - String get mapTerrainReliefHint => 'แสดงความนูนของภูมิประเทศบนแผนที่ฐาน'; + String get planetsSectionTonight => 'ขณะนี้'; @override - String get dpmSheetEmpty => 'แตะเครื่องหมายบนแผนที่เพื่อดูรายละเอียด'; + String get planetUp => 'เหนือขอบฟ้า'; @override - String get dpmAddress => 'ที่อยู่'; + String get planetDown => 'ใต้ขอบฟ้า'; @override - String get restroomTypeLabel => 'ประเภท'; + String get planetInGlare => 'ใกล้ดวงอาทิตย์'; @override - String get restroomCategoryLabel => 'หมวดหมู่'; + String get planetMagnitude => 'โชติมาตร'; @override - String get restroomGradeLabel => 'ระดับ'; + String get planetElongation => 'มุมห่าง'; @override - String get restroomTypeFemale => 'ห้องน้ำหญิง'; + String get planetSky => 'ช่วงเวลา'; @override - String get restroomTypeMale => 'ห้องน้ำชาย'; + String get planetEvening => 'หัวค่ำ'; @override - String get restroomTypeMixed => 'ห้องน้ำรวม'; + String get planetMorning => 'ก่อนรุ่ง'; @override - String get restroomTypeAccessible => 'ห้องน้ำคนพิการ'; + String get planetDistance => 'ระยะทาง'; @override - String get restroomTypeGenderNeutral => 'ห้องน้ำเป็นกลางทางเพศ'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => 'ห้องน้ำครอบครัว'; + String get planetAltitude => 'มุมเงย'; @override - String get restroomTypeUnspecified => 'ไม่ระบุ'; + String get planetMercury => 'พุธ'; @override - String get restroomCategoryTransport => 'การคมนาคม'; + String get planetVenus => 'ศุกร์'; @override - String get restroomCategoryPark => 'สวนสาธารณะ'; + String get planetMars => 'อังคาร'; @override - String get restroomCategoryCommercial => 'สถานประกอบการพาณิชย์'; + String get planetJupiter => 'พฤหัสบดี'; @override - String get restroomCategoryReligious => 'สถานที่ทางศาสนา'; + String get planetSaturn => 'เสาร์'; @override - String get restroomCategoryCultural => 'สถานที่ทางวัฒนธรรม'; + String get planetUranus => 'ยูเรนัส'; @override - String get restroomCategoryGovernment => 'สำนักงานราชการ'; + String get planetNeptune => 'เนปจูน'; @override - String get restroomCategoryWelfare => 'สถานสงเคราะห์'; + String get solarTermVernalEquinox => 'วสันตวิษุวัต'; @override - String get restroomCategoryTourist => 'แหล่งท่องเที่ยว'; + String get solarTermPureBrightness => 'เช็งเม้ง'; @override - String get restroomCategoryLeisure => 'สถานที่พักผ่อนหย่อนใจ'; + String get solarTermGrainRain => 'ฝนธัญพืช'; @override - String get restroomCategoryOther => 'อื่น ๆ'; + String get solarTermStartOfSummer => 'เริ่มฤดูร้อน'; @override - String get restroomGradeExcellent => 'ดีเยี่ยม'; + String get solarTermGrainFull => 'ธัญพืชเต็ม'; @override - String get restroomGradeGood => 'ดี'; + String get solarTermGrainInEar => 'ธัญพืชออกรวง'; @override - String get restroomGradeAverage => 'ปานกลาง'; + String get solarTermSummerSolstice => 'ครีษมายัน'; @override - String get restroomGradePoor => 'ต่ำกว่ามาตรฐาน'; + String get solarTermMinorHeat => 'ร้อนน้อย'; @override - String get shelterAddressLabel => 'ที่อยู่'; + String get solarTermMajorHeat => 'ร้อนมาก'; @override - String get shelterCapacityLabel => 'ความจุ'; + String get solarTermStartOfAutumn => 'เริ่มฤดูใบไม้ร่วง'; @override - String shelterCapacityValue(int n) { - return '$n คน'; - } + String get solarTermEndOfHeat => 'สิ้นสุดความร้อน'; @override - String get shelterCategoryLabel => 'ประเภทภัยพิบัติ'; + String get solarTermWhiteDew => 'น้ำค้างขาว'; @override - String get shelterIndoorLabel => 'การอพยพในอาคาร'; + String get solarTermAutumnalEquinox => 'ศารทวิษุวัต'; @override - String get shelterOutdoorLabel => 'การอพยพกลางแจ้ง'; + String get solarTermColdDew => 'น้ำค้างเย็น'; @override - String get shelterVulnerableOkLabel => 'เหมาะกับผู้เปราะบาง'; + String get solarTermFrostDescent => 'น้ำค้างแข็ง'; @override - String get dpmYes => 'ใช่'; + String get solarTermStartOfWinter => 'เริ่มฤดูหนาว'; @override - String get dpmNo => 'ไม่ใช่'; + String get solarTermMinorSnow => 'หิมะน้อย'; @override - String get stationSheetEmpty => 'แตะสถานีเพื่อดูค่าที่วัดได้'; + String get solarTermMajorSnow => 'หิมะมาก'; @override - String monitorDelay(String value) { - return 'หน่วงเวลา $value s'; - } + String get solarTermWinterSolstice => 'เหมายัน'; @override - String get monitorWaiting => 'กำลังรอข้อมูล…'; + String get solarTermMinorCold => 'หนาวน้อย'; @override - String mapLegendUnit(String unit) { - return 'หน่วย: $unit'; - } + String get solarTermMajorCold => 'หนาวมาก'; @override - String get typhoonLegendPast => 'เส้นทางจริง'; + String get solarTermStartOfSpring => 'เริ่มฤดูใบไม้ผลิ'; @override - String get typhoonIntensityTd => 'Tropical depression'; + String get solarTermRainWater => 'ฝนน้ำ'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => 'แมลงตื่น'; @override - String typhoonPickerTd(String no) { - return 'Tropical depression TD $no'; - } + String get tonightTitle => 'คืนนี้'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => 'มีอะไรให้ดู และเมื่อไร'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => 'ช่วงสังเกตการณ์'; @override - String get typhoonIntensityMild => 'Mild typhoon'; + String get tonightAstronomicalNight => 'กลางคืนทางดาราศาสตร์'; @override - String get typhoonIntensityModerate => 'Moderate typhoon'; + String get tonightNeverDark => 'ไม่มืดสนิท'; @override - String get typhoonIntensityIntense => 'Intense typhoon'; + String get tonightDarkWindow => 'ช่วงมืด'; @override - String get typhoonLegendForecast => 'เส้นทางพยากรณ์'; + String get tonightMoonAllNight => 'ดวงจันทร์อยู่ทั้งคืน'; @override - String get typhoonLegendForecastPoint => 'จุดพยากรณ์'; + String get tonightDarkTotal => 'เวลามืดรวม'; @override - String get typhoonLegendCurrent => 'ศูนย์กลางปัจจุบัน'; + String get tonightMoonlight => 'แสงจันทร์'; @override - String get typhoonLegendCone => 'กรวยพยากรณ์'; + String get tonightSectionShowers => 'ฝนดาวตก'; @override - String get mapLegendExpand => 'คำอธิบาย'; + String get tonightRadiantDown => 'จุดกระจายไม่ขึ้น'; @override - String get mapLegendCollapse => 'ซ่อนคำอธิบาย'; + String get tonightPerHour => 'ดวง/ชม.'; @override - String get mapMyLocation => 'ตำแหน่งของฉัน'; + String get tonightSectionSatellites => 'การผ่านของดาวเทียม'; @override - String get mapResetNorth => 'กลับไปทางเหนือ'; + String get tonightSectionTargets => 'เป้าหมายที่เห็นได้ตอนนี้'; @override - String get typhoonLegendCircle15 => 'วงพายุ (แรง)'; + String get showerQuadrantids => 'ควอดรานติดส์'; @override - String get typhoonLegendCircleAvg => 'Average circle'; + String get showerLyrids => 'ไลริดส์'; @override - String get typhoonLegendCircle25 => 'วงพายุ (รุนแรง)'; + String get showerEtaAquariids => 'อีตาอควาริดส์'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => 'เดลตาอควาริดส์'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => 'เพอร์เซอิดส์'; @override - String get typhoonLegendProbability => 'โอกาสกระทบ'; + String get showerOrionids => 'โอไรออนิดส์'; @override - String get typhoonLegendWarningAreas => 'พื้นที่เตือนภัย'; + String get showerSouthernTaurids => 'เทาริดส์ใต้'; @override - String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + String get showerLeonids => 'ลีโอนิดส์'; @override - String get typhoonOverlaySectionStorm => 'Storm wind'; + String get showerGeminids => 'เจมินิดส์'; @override - String get typhoonOverlaySectionExtra => 'Overlays'; + String get showerUrsids => 'เออร์ซิดส์'; @override - String get typhoonOverlayStormBandSubtitle => 'With average circle'; + String get deepSkyOpenCluster => 'กระจุกดาวเปิด'; @override - String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; + String get deepSkyGlobularCluster => 'กระจุกดาวทรงกลม'; @override - String get typhoonOverlayProbabilityTooltip => - 'Show strike probability (hides the forecast cone)'; + String get deepSkySpiralGalaxy => 'ดาราจักรกังหัน'; @override - String get typhoonOverlayWarningTooltip => - 'Highlight counties under a typhoon warning'; + String get deepSkyEllipticalGalaxy => 'ดาราจักรรี'; @override - String get typhoonOverlayStormL7Tooltip => - 'Level-7 wind field + average circle (purple)'; + String get deepSkyIrregularGalaxy => 'ดาราจักรไร้รูปแบบ'; @override - String get typhoonOverlayStormL10Tooltip => - 'Level-10 wind field + average circle (yellow)'; + String get deepSkyPlanetaryNebula => 'เนบิวลาดาวเคราะห์'; @override - String get typhoonOverlaySectionWeather => 'Weather underlay'; + String get deepSkySupernovaRemnant => 'ซากซูเปอร์โนวา'; @override - String get typhoonOverlayWeatherNone => 'None'; + String get deepSkyEmissionNebula => 'เนบิวลาเปล่งแสง'; @override - String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; + String get deepSkyReflectionNebula => 'เนบิวลาสะท้อนแสง'; @override - String get typhoonOverlayWeatherNoneTooltip => - 'No radar or infrared underlay'; + String get deepSkyAsterism => 'กลุ่มดาวย่อย'; @override - String get typhoonOverlayWeatherRadarTooltip => - 'Radar echo closest to the typhoon bulletin time'; + String get almanacTitle => 'ปฏิทิน'; @override - String get typhoonOverlayWeatherSatelliteTooltip => - 'Infrared closest to the typhoon bulletin time'; + String get almanacSubtitle => 'ปฏิทินจันทรคติและอุปราคาข้างหน้า'; @override - String get typhoonWarningTitle => 'ประกาศเตือนไต้ฝุ่น'; + String get almanacSectionToday => 'วันนี้'; @override - String typhoonWarningAreas(String areas) { - return 'พื้นที่: $areas'; - } + String get almanacGregorian => 'สุริยคติ'; @override - String get typhoonTrackDetail => 'รายละเอียดเส้นทาง'; + String get almanacLunar => 'จันทรคติ'; @override - String get typhoonHistoryTitle => 'เวลาข้อมูล'; + String get almanacYear => 'ปีนักษัตร'; @override - String get typhoonHistoryLive => 'สด'; + String get almanacMonthLength => 'ความยาวเดือน'; @override - String get typhoonSatelliteTitle => 'ดาวเทียม'; + String get almanacLongMonth => '30 วัน'; @override - String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + String get almanacShortMonth => '29 วัน'; @override - String get typhoonOverlayForecastCalloutsTooltip => - 'Show forecast-point detail cards when zoomed in'; + String get almanacLeapPrefix => 'อธิกมาส '; @override - String get dpmFilterSectionRestroom => 'ประเภทสถานที่'; + String get almanacSectionLunarEclipses => 'จันทรุปราคา'; @override - String get dpmFilterSectionRestroomType => 'ประเภทห้องน้ำ'; + String get almanacSectionSolarEclipses => 'สุริยุปราคา'; @override - String get dpmFilterSectionShelter => 'ประเภทภัยพิบัติของศูนย์อพยพ'; + String get almanacNoSolarEclipse => 'ไม่มีในช่วงนี้'; @override - String get dpmDisasterFlood => 'น้ำท่วม'; + String get eclipseTotal => 'เต็มดวง'; @override - String get dpmDisasterEarthquake => 'แผ่นดินไหว'; + String get eclipsePartial => 'บางส่วน'; @override - String get dpmDisasterLandslide => 'ดินถล่ม'; + String get eclipseAnnular => 'วงแหวน'; @override - String get dpmDisasterTsunami => 'สึนามิ'; + String get eclipsePenumbral => 'เงามัว'; @override - String get dpmDisasterSlope => 'ภัยพิบัติลาดชัน'; + String get zodiacRat => 'ชวด'; @override - String get dpmDisasterNuclear => 'อุบัติเหตุนิวเคลียร์'; + String get zodiacOx => 'ฉลู'; @override - String get skyTime => 'เวลาท้องฟ้า'; + String get zodiacTiger => 'ขาล'; @override - String get skyTimeAuto => 'อัตโนมัติ'; + String get zodiacRabbit => 'เถาะ'; @override - String get skyTimeDawn => 'รุ่งอรุณ'; + String get zodiacDragon => 'มะโรง'; @override - String get skyTimeSunrise => 'พระอาทิตย์ขึ้น'; + String get zodiacSnake => 'มะเส็ง'; @override - String get skyTimeMorning => 'ตอนเช้า'; + String get zodiacHorse => 'มะเมีย'; @override - String get skyTimeNoon => 'เที่ยงวัน'; + String get zodiacGoat => 'มะแม'; @override - String get skyTimeAfternoon => 'ตอนบ่าย'; + String get zodiacMonkey => 'วอก'; @override - String get skyTimeGolden => 'ช่วงเวลาทอง'; + String get zodiacRooster => 'ระกา'; @override - String get skyTimeSunset => 'พระอาทิตย์ตก'; + String get zodiacDog => 'จอ'; @override - String get skyTimeDusk => 'สนธยา'; + String get zodiacPig => 'กุน'; @override - String get skyTimeNight => 'กลางคืน'; + String get tideTitle => 'น้ำขึ้นน้ำลง'; @override - String get weatherModeCloudy => 'มีเมฆมาก'; + String get tideSubtitle => 'น้ำเกิด น้ำตาย และแรงดึงดูดของดวงจันทร์'; @override - String get weatherModeOvercast => 'ฟ้าปิด'; + String get tideDisclaimer => + 'แรงดาราศาสตร์เท่านั้น ไม่ใช่ตารางน้ำท่า ระดับน้ำโปรดดูตารางที่กรมอุตุนิยมวิทยาเผยแพร่'; @override - String get weatherModeSnow => 'หิมะตก'; + String get tideSectionNow => 'ขณะนี้'; @override - String get weatherModeSand => 'ฝุ่นทราย'; + String get tidePhase => 'วัฏจักร'; @override - String get radarScanRange => 'แสดงขอบเขตการสแกน'; + String get tideSpring => 'น้ำเกิด'; @override - String get radarScanRangeSubtitle => - 'แสดงพื้นที่ที่เรดาร์ทั้งสี่ตรวจวัดได้จริง'; + String get tideNeap => 'น้ำตาย'; @override - String get radarScanRangeHint => 'นอกกรอบคือไม่ได้ตรวจวัด'; + String get tideMiddling => 'ปานกลาง'; @override - String get radarOverlayMenuTooltip => 'ตัวเลือกชั้นเรดาร์'; + String get tideLunarDistanceFactor => 'แรงดึงดวงจันทร์'; @override - String get radarCountyOutline => 'เส้นแบ่งเขตจังหวัด'; + String get tideEquilibrium => 'ระดับสมดุล'; @override - String get radarGlobalOutline => 'เส้นแบ่งเขตประเทศ'; + String get tideMetres => 'ม.'; @override - String get radarGlobalOutlineHint => 'กรอบนอกของทุกประเทศ'; + String get tidePerigeanSpring => 'น้ำเกิดใกล้โลกครั้งถัดไป'; @override - String get radarCountyOutlineHint => 'วาดทับภาพเอคโค'; + String get tideSectionTurningPoints => 'จุดเปลี่ยน'; @override - String get radarCountyOutlineSubtitle => - 'ทำให้เส้นแบ่งเขตยังอ่านออกใต้ภาพเอคโคเรดาร์'; + String get tideHigh => 'สูง'; @override - String get radarTownOutline => 'เส้นแบ่งเขตอำเภอ'; + String get tideLow => 'ต่ำ'; @override - String get radarTownOutlineHint => 'เส้นแบ่งย่อยกว่า'; + String get skyChartTitle => 'แผนที่ดาว'; @override - String get radarTownOutlineSubtitle => - 'ทำให้เส้นแบ่งเขตอำเภอยังอ่านออกใต้ภาพเอคโคเรดาร์'; + String get skyChartSubtitle => 'ท้องฟ้าที่ตาเปล่ามองเห็น'; @override - String get qpesumsOverlayMenuTooltip => 'ตัวเลือกชั้นพยากรณ์น้ำฝน'; + String get skyChartNorth => 'N'; @override - String get windForecastOverlayMenuTooltip => 'ตัวเลือกชั้นพยากรณ์ลม'; + String get skyChartEast => 'E'; @override - String get windForecastCountyOutlineHint => 'วาดทับบนสนามลม'; + String get skyChartSouth => 'S'; @override - String get windForecastGlobalOutlineHint => 'กรอบนอกของทุกประเทศ'; + String get skyChartWest => 'W'; @override - String get windForecastTownOutlineHint => 'ตาข่ายที่ละเอียดกว่า'; + String tonightElementAge(int days) { + return 'ข้อมูลวงโคจร $days วันก่อน'; + } @override - String eewSerial(int serial) { - return 'รายงาน $serial'; + String almanacLunarDate(String leap, int month, int day) { + return '$leapเดือน $month วันที่ $day'; } @override - String get eewMaxIntensity => 'ความรุนแรงสูงสุด'; + String get tonightNoShowers => 'ไม่มีฝนดาวตก'; @override - String get eewLocalIntensity => 'ประมาณ ณ ตำแหน่ง'; + String get tonightNoPasses => 'ไม่มีการผ่านที่มองเห็นใน 48 ชม.'; @override - String get eewSWave => 'คลื่น S'; + String get tonightSatellitesUnavailable => 'อ่านข้อมูลวงโคจรไม่ได้'; @override - String get eewArrived => 'มาถึงแล้ว'; + String get tonightNoTargets => 'ไม่มีเป้าหมายที่สูงพอ'; @override - String eewCountdown(int seconds) { - return '$seconds วินาที'; - } + String get skyChartUnavailable => 'อ่านแคตตาล็อกดาวไม่ได้'; } diff --git a/lib/l10n/gen/app_localizations_vi.dart b/lib/l10n/gen/app_localizations_vi.dart index 7118c3a89..0aba6f080 100644 --- a/lib/l10n/gen/app_localizations_vi.dart +++ b/lib/l10n/gen/app_localizations_vi.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,510 +10,503 @@ class AppLocalizationsVi extends AppLocalizations { AppLocalizationsVi([String locale = 'vi']) : super(locale); @override - String get languageName => 'Tiếng Việt'; + String typhoonValueLat(String lat) { + return '$lat°N'; + } @override - String get navHome => 'Trang chủ'; + String get onboardingSkipBody => + 'Nếu không có quyền vị trí và thông báo, DPIP không thể cảnh báo tức thời về động đất và thiên tai gần bạn. Bạn vẫn có thể cấp quyền sau trong Cài đặt.'; @override - String get navEvents => 'Sự kiện'; + String get rainInterval24h => '24 giờ'; @override - String get navMap => 'Bản đồ'; + String homeRainTrendHeavyStopping(int minutes) { + return 'Mưa lớn có thể tạnh trong $minutes phút nữa'; + } @override - String get navData => 'Dữ liệu'; + String get mapTimelineObserved => 'Quan trắc'; @override - String get navEarthquake => 'Động đất'; + String get regionSelectTitle => 'Chọn khu vực'; @override - String get dataSectionSeismic => 'Địa chấn'; + String get skyTimeNoon => 'Buổi trưa'; @override - String get dataEarthquakeSubtitle => 'Báo cáo động đất'; + String get radarCountyOutlineSubtitle => + 'Giữ ranh giới rõ ràng dưới lớp phản hồi radar.'; @override - String get dataSectionWeather => 'Thời tiết'; + String get dpmFilterSectionRestroomType => 'Loại nhà vệ sinh'; @override - String get dataWeatherRankingSubtitle => 'Xếp hạng trạm trực tiếp'; + String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; @override - String get weatherRankingTitle => 'Xếp hạng quan trắc'; + String get reportFilterIntensity => 'Cường độ'; @override - String weatherRankingMeta(String time, int count) { - return 'Thời gian: $time\n$count trạm'; - } + String get mapLayerLightning => 'Sét'; @override - String get weatherRankingEmpty => 'Không có quan trắc để xếp hạng'; + String get restroomTypeMale => 'Nhà vệ sinh nam'; @override - String get weatherRankingBy => 'Theo'; + String get meshtasticLastReceived => 'Last received'; @override - String get weatherRankingHighest => 'Cao nhất'; + String get reportDetailSortByCounty => 'Sắp xếp theo khu vực'; @override - String get weatherRankingLowest => 'Thấp nhất'; + String get homeRainTrendScattered => 'Có thể có mưa rào nhẹ'; @override - String get weatherRankingMergeTo => 'Gộp'; + String get meshtasticUptime => 'Uptime'; @override - String get weatherRankingMergeTown => 'Xã/trấn'; + String get weatherRankingTempExtremes => 'Cực trị nhiệt độ'; @override - String get weatherRankingMergeCounty => 'Huyện/thành'; + String get themeLight => 'Sáng'; @override - String get weatherRankingWind => 'Tốc độ gió'; + String get mapTerrainReliefHint => 'Hiển thị địa hình nổi trên bản đồ nền'; @override - String get weatherRankingGust => 'Gió giật'; + String get meshtasticEmptyMessage => '(empty message)'; @override - String get weatherRankingTempExtremes => 'Cực trị nhiệt độ'; + String get moreSectionRegion => 'Khu vực'; @override - String get weatherRankingExtremeHigh => 'Cao nhất ngày'; + String get dpmDisasterEarthquake => 'Động đất'; @override - String get weatherRankingExtremeLow => 'Thấp nhất ngày'; + String get mapLayerSatellite => 'Himawari Infrared (B13)'; @override - String get weatherRankingExtremeRange => 'Biên độ ngày'; + String get aedHoursSaturday => 'Giờ thứ Bảy'; @override - String weatherRankingRecordedAt(String time) { - return 'Ghi nhận lúc $time'; - } + String get dpmDisasterSlope => 'Thiên tai sườn dốc'; @override - String weatherRankingAnalysisCurrent(String value) { - return 'Hiện tại $value°C'; - } + String get moonPhaseNew => 'New moon'; @override - String weatherRankingAnalysisHigh(String value) { - return 'Cao $value'; - } + String get notifySectionEew => 'Cảnh báo sớm động đất'; @override - String weatherRankingAnalysisLow(String value) { - return 'Thấp $value'; - } + String get mapResetNorth => 'Về hướng bắc'; @override - String weatherRankingAnalysisRange(String value) { - return 'Biên độ $value°C'; - } + String get rainInterval2d => '2 ngày'; @override - String get reportListEmpty => 'Không có báo cáo động đất'; + String get mapTownLabelsHint => 'Hiển thị tên hương trấn khi phóng to'; @override - String get reportListEmptyFiltered => 'Không có báo cáo khớp bộ lọc'; + String get commonCancel => 'Cancel'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth km'; - } + String get notifyOptTsunamiWarning => 'Chỉ cảnh báo sóng thần'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; @override - String get reportListDepthUnit => 'km'; + String get moreSectionAdvanced => 'Nâng cao'; @override - String get reportListLocalFelt => 'Cảm nhận cục bộ'; + String get weatherRankingExtremeRange => 'Biên độ ngày'; @override - String get reportListToday => 'Hôm nay'; + String get notifySettingsMenu => 'Cài đặt thông báo'; @override - String get reportListYesterday => 'Hôm qua'; + String get typhoonHistoryTitle => 'Thời điểm dữ liệu'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app (mặc định)'; } @override - String get reportListEnd => 'Hết danh sách'; - - @override - String get reportFilterTitle => 'Bộ lọc'; + String get trendRange24h => '24 giờ'; @override - String get reportFilterSort => 'Sắp xếp'; + String get mapLayerStyleJmaTooltip => + 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; @override - String get reportFilterSortTime => 'Thời gian'; + String weatherRankingRecordedAt(String time) { + return 'Ghi nhận lúc $time'; + } @override - String get reportFilterSortIntensity => 'Cường độ'; + String get mapLayerRain => 'Lượng mưa'; @override - String get reportFilterSortMagnitude => 'Độ lớn'; + String get mapLayerQpesums => 'Dự báo mưa 1 giờ tới'; @override - String get reportFilterSortDepth => 'Độ sâu'; + String get mapOverlaySectionMap => 'Bản đồ'; @override - String get reportFilterOrderDesc => 'Giảm dần'; + String get mapTerrainRelief => 'Độ nổi địa hình'; @override - String get reportFilterOrderAsc => 'Tăng dần'; + String get eewMaxIntensity => 'Cường độ tối đa'; @override - String get reportFilterIntensity => 'Cường độ'; + String get mapLegendCollapse => 'Ẩn chú giải'; @override - String get reportFilterIntensityInfoTitle => 'Thang cường độ mới và cũ'; + String get changelogTitle => 'Nhật ký cập nhật'; @override - String get reportFilterIntensityInfoIntro => - 'CWA đổi thang cường độ từ 1/1/2020 (giờ Đài Bắc).'; + String get reportFilterOrderDesc => 'Giảm dần'; @override - String get reportFilterIntensityInfoLegacyTitle => 'Cũ (trước 2020)'; + String get meshtasticExcludeMqttSubtitle => + 'Nodes bridged over the internet, not heard by radio'; @override - String get reportFilterIntensityInfoLegacyBody => - 'Chỉ có mức 0–7, không tách 5−/5+/6−/6+.'; + String get reportFilterIntensityInfoTitle => 'Thang cường độ mới và cũ'; @override - String get reportFilterIntensityInfoModernTitle => 'Mới (từ 2020)'; + String get mapLayerTyphoon => 'Bão'; @override - String get reportFilterIntensityInfoModernBody => - 'Các mức 0–4, 5−, 5+, 6−, 6+, 7. Thanh lọc dùng thang mới; sự kiện cũ vẫn hiện nhãn cũ trong danh sách.'; + String get radarOverlayMenuTooltip => 'Tùy chọn lớp radar'; @override - String get reportFilterMagnitude => 'Độ lớn'; + String get mapMyLocation => 'Vị trí của tôi'; @override - String get reportFilterDepth => 'Độ sâu'; + String get meshtasticNodes => 'Nodes'; @override - String reportFilterDepthKm(String depth) { - return '$depth km'; - } + String get meshtasticSend => 'Send'; @override - String get reportFilterDate => 'Ngày'; + String get typhoonOverlayStormL7Tooltip => + 'Level-7 wind field + average circle (purple)'; @override - String get reportFilterDatePick => 'Chọn ngày'; + String get aedType => 'Loại'; @override - String get reportFilterDateStartNote => 'Ngày bắt đầu: từ 00:00(Đài Bắc)'; + String get termsOfService => 'Điều khoản dịch vụ'; @override - String get reportFilterDateEndNote => 'Ngày kết thúc: đến 24:00(Đài Bắc)'; + String get typhoonLegendCircle25 => 'Vòng bão'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get sponsorTitle => 'Ủng hộ DPIP'; @override - String get reportFilterLocation => 'Địa điểm'; + String get mapNavSatellite => 'Vệ tinh'; @override - String get reportFilterLocationHint => 'vd: Hoa Liên, ngoài khơi'; + String homeRainTrendUpdated(String time) { + return 'Cập nhật $time'; + } @override - String get reportFilterAny => 'Tất cả'; + String get onboardingNext => 'Tiếp theo'; @override - String get reportFilterApply => 'Áp dụng'; + String get weatherRankingMergeTown => 'Xã/trấn'; @override - String get reportFilterReset => 'Đặt lại'; + String get mapLayerMonitor => 'Giám sát địa chấn'; @override - String get reportListSearch => 'Tìm'; + String get moreYoutube => 'YouTube'; @override - String get reportDetailTitle => 'Báo cáo động đất'; + String get sponsorSubscriptions => 'Gói đăng ký'; @override - String reportDetailNumbered(String number) { - return 'Động đất có cảm nhận đáng kể số $number'; + String typhoonValueLon(String lon) { + return '$lon°E'; } @override - String get reportDetailLocalFelt => 'Động đất cảm nhận cục bộ'; + String get skyTime => 'Thời gian bầu trời'; @override - String get reportDetailInfo => 'Chi tiết'; + String get weatherModeCloudy => 'Nhiều mây'; @override - String get reportDetailOriginTime => 'Thời gian xảy ra'; + String get skyTimeDusk => 'Chạng vạng'; @override - String get reportDetailEpicenter => 'Tọa độ tâm chấn'; + String get meshtasticFirmware => 'Firmware'; @override - String get reportDetailMagnitude => 'Độ lớn'; + String get reportFilterDateEndNote => 'Ngày kết thúc: đến 24:00(Đài Bắc)'; @override - String get reportDetailDepth => 'Độ sâu chấn tiêu'; + String get reportFilterSortMagnitude => 'Độ lớn'; @override - String get reportDetailAreaIntensity => 'Cường độ theo khu vực'; + String get meshtasticSilent => 'Silent'; @override - String get reportDetailLocalIntensity => 'Cường độ tại vị trí của bạn'; + String get mapLayerCategoryEarthquake => 'Động đất'; @override - String get reportDetailLocalIntensityUnavailable => - 'Không có dữ liệu cường độ'; + String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; @override - String get reportDetailSortByIntensity => 'Sắp xếp theo cường độ'; + String get typhoonLegendPast => 'Quỹ đạo thực tế'; @override - String get reportDetailSortByCounty => 'Sắp xếp theo khu vực'; + String get restroomCategoryOther => 'Khác'; @override - String get reportDetailImage => 'Hình ảnh báo cáo'; + String homeForecastHighLow(String high, String low) { + return 'Cao $high° · Thấp $low°'; + } @override - String get reportDetailImageUnavailable => 'Hình ảnh báo cáo chưa có sẵn'; + String get locationBannerFix => 'Mở cài đặt'; @override - String get reportDetailOpenReport => 'Trang báo cáo'; + String get mapLegendExpand => 'Chú giải'; @override - String get reportDetailReplay => 'Phát lại'; + String get eewNone => 'Hiện không có cảnh báo sớm động đất'; @override - String get navMore => 'Thêm'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get appLogs => 'Nhật ký ứng dụng'; + String get notifyOptTsunamiAll => 'Tin và cảnh báo sóng thần'; @override - String get changelogTitle => 'Nhật ký cập nhật'; + String get meshtasticLayerOptions => 'Node options'; @override - String get changelogEmpty => 'Chưa có ghi chú phát hành'; + String get onboardingAgreeContinue => 'Đồng ý và tiếp tục'; @override - String get changelogTypePrerelease => 'Thử nghiệm'; + String get commonRetry => 'Thử lại'; @override - String get changelogTypeStable => 'Chính thức'; + String get meshtasticNodeId => 'Node ID'; @override - String get changelogCurrentVersion => 'Hiện tại'; + String reportDetailNumbered(String number) { + return 'Động đất có cảm nhận đáng kể số $number'; + } @override - String get changelogVersionDetails => 'Chi tiết phiên bản'; + String get typhoonOverlayStormBandSubtitle => 'With average circle'; @override - String get changelogBodyEmpty => 'Không có ghi chú cho bản phát hành này.'; + String get disasterMapOverlayRestroomTooltip => + 'Hiển thị nhà vệ sinh công cộng'; @override - String get mapPlaceholderDisabled => 'Bản đồ (tạm thời vô hiệu hóa)'; + String get weatherRankingTitle => 'Xếp hạng quan trắc'; @override - String get moreSectionRegion => 'Khu vực'; + String get homeRainTrendHeavySustained => 'Mưa lớn tiếp diễn trong 1 giờ tới'; @override - String get moreSectionNotify => 'Thông báo'; + String get notifySectionTsunami => 'Sóng thần'; @override - String get moreSectionDisplay => 'Hiển thị'; + String get restroomCategoryPark => 'Công viên'; @override - String get regionManageTitle => 'Khu vực đã lưu'; + String get moreLinkOpenFailed => 'Không thể mở liên kết'; @override - String get regionAddButton => 'Thêm khu vực'; + String get themeDark => 'Tối'; @override - String get regionEmpty => 'Chưa có khu vực nào được lưu'; + String get sponsorRestore => 'Khôi phục giao dịch'; @override - String get regionSelectTitle => 'Chọn khu vực'; + String get meshtasticChannelWorking => 'Setting up the DPIP channel…'; @override - String regionSelectCount(int count, int max) { - return 'Đã chọn $count/$max'; - } + String get meshtasticRegionSwitch => 'Switch to TW'; @override - String regionSelectFull(int max) { - return 'Bạn chỉ có thể lưu tối đa $max khu vực'; - } + String get meshtasticTraffic => 'Traffic'; @override - String get regionEdit => 'Sửa'; + String get mapLayerStyleBdTooltip => + 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; @override - String get moreSectionAdvanced => 'Nâng cao'; + String get disasterMapOverlayAedTooltip => 'Hiện vị trí AED'; @override - String get moreDeveloper => 'Thông tin gỡ lỗi'; + String get mapLayerHumidity => 'Độ ẩm'; @override - String get experimentalFeatures => 'Tính năng thử nghiệm'; + String get mapLayerSatelliteTransparentNight => + 'Night = transparent, the basemap shows'; @override - String get moreSectionLinks => 'Liên kết'; + String get meshtasticScanning => 'Scanning…'; @override - String get moreCwaEew => 'Cảnh báo sớm động đất của CWA'; + String regionSelectFull(int max) { + return 'Bạn chỉ có thể lưu tối đa $max khu vực'; + } @override - String get moreTremReport => 'Báo cáo phát hiện TREM'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreServerStatus => 'Trạng thái máy chủ'; + String get navMore => 'Thêm'; @override - String get moreAnnouncements => 'Thông báo'; + String get meshtasticDpipChannel => 'DPIP channel'; @override - String get moreDiscord => 'Cộng đồng Discord'; + String get disasterMapOverlaySectionLayers => 'Lớp'; @override - String get moreNotifyLog => 'Nhật ký thông báo DPIP'; + String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; @override - String get moreLinkOpenFailed => 'Không thể mở liên kết'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; + } @override - String get weatherDynamicState => 'Hoạt ảnh thời tiết'; + String get typhoonLabelNe => 'NE'; @override - String get weatherDynamicStateSubtitle => - 'Ghi đè thời tiết nền của trang chủ'; + String get meshtasticCopied => 'Message copied'; @override - String get weatherModeAuto => 'Tự động'; + String get reportListEmpty => 'Không có báo cáo động đất'; @override - String get weatherModeClear => 'Trời quang'; + String get reportListEnd => 'Hết danh sách'; @override - String get weatherModeRain => 'Mưa'; + String get mapLayerSatelliteTruecolor => 'Himawari True Color'; @override - String get weatherModeFog => 'Sương mù'; + String get typhoonOverlaySectionExtra => 'Overlays'; @override - String get weatherModeThunderstorm => 'Mưa dông'; + String get eewSWave => 'Sóng S'; @override - String get commonLoading => 'Đang tải…'; + String get meshtasticBusyTitle => 'Another app is using this radio'; @override - String get commonRetry => 'Thử lại'; + String get restroomCategoryCultural => 'Địa điểm văn hóa giải trí'; @override - String get commonError => 'Đã xảy ra lỗi'; + String get typhoonLabelWind => 'Max. sustained wind near centre'; @override - String get commonFetchFailed => 'Không thể tải dữ liệu. Vui lòng thử lại.'; + String get radarGlobalOutlineHint => 'Khung ngoài của mỗi quốc gia'; @override - String get commonEmpty => 'Không có dữ liệu'; + String get notifyEvacuation => 'Thông tin thảm họa'; @override - String get feedConnecting => 'Đang kết nối…'; + String get typhoonLegendCircle15 => 'Vòng gió mạnh'; @override - String get feedStale => 'Dữ liệu có thể đã lỗi thời'; + String get dataSectionAstronomy => 'Astronomy'; @override - String get feedOffline => 'Mất kết nối'; + String get homeRainTrendLightSustained => 'Mưa nhỏ tiếp diễn trong 1 giờ tới'; @override - String get eewTitle => 'Cảnh báo sớm động đất'; + String get commonError => 'Đã xảy ra lỗi'; @override - String get eewNone => 'Hiện không có cảnh báo sớm động đất'; + String get moonPhaseWaningCrescent => 'Waning crescent'; @override - String eewSummary(String magnitude, String depth) { - return 'M$magnitude · độ sâu $depth km'; + String get meshtasticPower => 'Power'; + + @override + String get mapTimelineNow => 'Bây giờ'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => 'Toàn quốc'; + String get reportDetailOpenReport => 'Trang báo cáo'; @override - String get regionCurrent => 'Vị trí hiện tại'; + String get trendRange7d => '7 ngày'; @override - String get regionCurrentUnavailable => 'Không thể lấy vị trí hiện tại'; + String typhoonWarningAreas(String areas) { + return 'Khu vực: $areas'; + } @override - String get weatherPrecipitation => 'Lượng mưa'; + String get rainIntervalSection => 'Khoảng thời gian'; @override - String get weatherHumidity => 'Độ ẩm'; + String get notifyTitle => 'Thông báo'; @override - String weatherDataTime(String station, String time) { - return '$station · Thời gian dữ liệu $time'; - } + String get meshtasticTxPower => 'TX power'; @override - String get homeViewOnMap => 'Xem trên bản đồ'; + String get restroomCategoryLabel => 'Hạng mục'; @override - String get homeForecastTitle => 'Dự báo 24 giờ'; + String get sponsorRestoring => 'Đang khôi phục giao dịch…'; @override - String homeForecastHighLow(String high, String low) { - return 'Cao $high° · Thấp $low°'; - } + String get sponsorIntro => + 'DPIP cam kết cung cấp thông tin phòng chống thiên tai theo thời gian thực, không có quảng cáo hay mô hình lợi nhuận nào khác. Sự ủng hộ của bạn giúp chúng tôi duy trì máy chủ và tiếp tục phát triển.'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => 'Địa chỉ'; @override - String homeForecastFeelsLike(String temp) { - return 'Cảm giác $temp°'; - } + String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; @override - String homeForecastHumidity(String value) { - return 'Độ ẩm $value%'; - } + String get restroomCategoryCommercial => 'Cơ sở thương mại'; @override - String homeForecastWind(String direction, String level) { - return '$direction · Cấp $level'; - } + String get aedRegion => 'Khu vực'; @override - String get homeForecastUnavailable => 'Chọn khu vực để xem dự báo'; + String homeRainTrendLightStopping(int minutes) { + return 'Mưa nhỏ có thể tạnh trong $minutes phút nữa'; + } @override - String get homeForecastEmpty => 'Không có dữ liệu dự báo'; + String get reportDetailInfo => 'Chi tiết'; @override - String get homeActiveEventsTitle => 'Sự kiện đang hiệu lực'; + String get mapNavWind => 'Gió'; @override - String get homeActiveEventsEmpty => 'Không có sự kiện đang hiệu lực'; + String get windForecastOverlayMenuTooltip => 'Tùy chọn lớp dự báo gió'; @override - String get homeRainTrendTitle => 'Mưa 1 giờ tới'; + String get dataWeatherRankingSubtitle => 'Xếp hạng trạm trực tiếp'; @override String homeRainTrendMinute(int minute) { @@ -520,1315 +514,2126 @@ class AppLocalizationsVi extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return 'Cập nhật $time'; - } + String get rainInterval6h => '6 giờ'; @override - String get homeRainTrendNoData => 'Không có dữ liệu'; + String get restroomTypeUnspecified => 'Không xác định'; @override - String get homeRainTrendScattered => 'Có thể có mưa rào nhẹ'; + String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; @override - String get homeRainTrendLightSustained => 'Mưa nhỏ tiếp diễn trong 1 giờ tới'; + String get mapLayerSatelliteGlobalOutline => 'Country border'; @override - String homeRainTrendLightStopping(int minutes) { - return 'Mưa nhỏ có thể tạnh trong $minutes phút nữa'; - } + String get mapNavTemperature => 'Nhiệt độ'; @override - String get homeRainTrendHeavySustained => 'Mưa lớn tiếp diễn trong 1 giờ tới'; + String get typhoonLegendForecastPoint => 'Điểm dự báo'; @override - String homeRainTrendHeavyStopping(int minutes) { - return 'Mưa lớn có thể tạnh trong $minutes phút nữa'; - } + String get reportListYesterday => 'Hôm qua'; @override - String get mapLayers => 'Lớp bản đồ'; + String get moreSectionLinks => 'Liên kết'; @override - String get mapLayerOrderTitle => 'Sắp xếp thứ tự lớp'; + String get feedOffline => 'Mất kết nối'; @override - String get mapLayerOrderReset => 'Đặt lại thứ tự mặc định'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => 'Radar phản xạ tổng hợp'; + String get moreSectionDisplay => 'Hiển thị'; @override - String get mapLayerSatellite => 'Himawari Infrared (B13)'; + String get rainInterval3d => '3 ngày'; @override - String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; + String get defaultMapLayerSubtitle => + 'Tab Bản đồ mở lớp này. Biểu tượng và nhãn thanh điều hướng dưới cũng theo lựa chọn.'; @override - String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; + String get aedDescription => 'Ghi chú'; @override - String get mapLayerSatelliteB03 => 'Himawari Red (B03)'; + String get typhoonOverlayWeatherRadarTooltip => + 'Radar echo closest to the typhoon bulletin time'; @override - String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; + String get onboardingPermLocationDesc => + 'Gửi cảnh báo phù hợp với nơi bạn đang ở.'; @override - String get mapLayerSatelliteB05 => 'Himawari Near-Infrared (B05)'; + String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; @override - String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + String get homeActiveEventsEmpty => 'Không có sự kiện đang hiệu lực'; @override - String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + String get typhoonLabelPosition => 'Centre location'; @override - String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; + String get weatherRankingBy => 'Theo'; @override - String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; + String get typhoonIntensityMild => 'Mild typhoon'; @override - String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; + String get windForecastGlobalOutlineHint => 'Khung ngoài của mỗi quốc gia'; @override - String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; + String get rainInterval1h => '1 giờ'; @override - String get mapLayerSatelliteB12 => 'Himawari Ozone (B12)'; + String get eewLocalIntensity => 'Ước tính tại vị trí'; @override - String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + String get mapLayerRadar => 'Radar phản xạ tổng hợp'; @override - String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + String get restroomCategoryReligious => 'Nơi tôn giáo'; @override - String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + String get meshtasticRole => 'Role'; @override - String get mapLayerSatelliteB16 => 'Himawari CO₂ (B16)'; + String get mapLayerSatelliteCloudCloudy => 'Cloudy'; @override - String get mapLayerSatelliteTruecolor => 'Himawari True Color'; + String get skyTimeSunrise => 'Bình minh'; @override - String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + String get meshtasticNoMessages => 'No messages yet'; @override - String get mapLayerSatelliteAsh => 'Himawari Ash'; + String get onboardingPermNotifyDesc => + 'Gửi cảnh báo động đất, thời tiết và thảm họa ngay khi chúng xảy ra.'; @override - String get mapLayerSatelliteDust => 'Himawari Dust'; + String get radarTownOutline => 'Ranh giới xã phường'; @override - String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + String get mapLayerStyleSection => 'Colour style'; @override - String get mapLayerSatelliteNightmicrophysics => - 'Himawari Night Microphysics'; + String get disasterMapOverlayMenuTooltip => 'Lớp bản đồ phòng chống'; @override - String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + String get meshtasticOnline => 'Heard recently'; @override - String get mapLayerSatelliteBtdFog => 'Himawari Night Fog'; + String get typhoonLabelSw => 'SW'; @override - String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + String typhoonForecastLead(String hours) { + return 'Forecast +$hours h'; + } @override - String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + String get dpmDisasterTsunami => 'Sóng thần'; @override - String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; + String get changelogTypeStable => 'Chính thức'; @override - String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; + String get mapLayerSatelliteTransparentClear => + 'Clear sky = transparent, the basemap shows'; @override - String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + String get mapOverlaySectionReference => 'Lớp tham chiếu'; @override - String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; + String get mapLayerSatelliteB02 => 'Himawari Green (B02)'; @override - String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; + String get reportListLocalFelt => 'Cảm nhận cục bộ'; @override - String get mapLayerSatelliteNdvi => 'Himawari NDVI'; + String get weatherRankingEmpty => 'Không có quan trắc để xếp hạng'; @override - String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + String get notifySectionOther => 'Khác'; @override - String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; + String weatherRankingMeta(String time, int count) { + return 'Thời gian: $time\n$count trạm'; + } @override - String get mapLayerSatelliteGlobalOutline => 'Country border'; + String get onboardingTermsAgree => + 'Tôi đã đọc và đồng ý với Điều khoản Dịch vụ'; @override - String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; + String get mapLayerSatelliteTransparentNoVegetation => + 'Below 0.1 = transparent (no vegetation)'; @override - String get mapLayerSatelliteCloudClear => 'Clear'; + String get notifyOptLocalIntensity4 => 'Cường độ tại chỗ từ 4 trở lên'; @override - String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + String get eewArrived => 'Đã đến'; @override - String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; + String get meshtasticNoDevices => 'No Meshtastic devices found'; @override - String get mapLayerSatelliteCloudCloudy => 'Cloudy'; + String get mapLayerCategoryLife => 'Đời sống'; @override - String get mapLayerSatelliteTransparentWarm => - 'Clear sky (warm end) = transparent, the basemap shows'; + String get reportFilterSortIntensity => 'Cường độ'; @override - String get mapLayerSatelliteTransparentReflectance => - 'Low reflectance / night = transparent, the basemap shows'; + String get typhoonMotion => 'Di chuyển'; @override - String get mapLayerSatelliteTransparentZero => - 'Zero difference = transparent (no signal)'; + String get meshtasticStateDisconnected => 'Disconnected'; @override - String get mapLayerSatelliteTransparentNight => - 'Night = transparent, the basemap shows'; + String get typhoonIntensityIntense => 'Intense typhoon'; @override - String get mapLayerSatelliteTransparentNoData => - 'No data (land) = transparent'; + String get mapLayerOrderTitle => 'Sắp xếp thứ tự lớp'; @override - String get mapLayerSatelliteTransparentNoVegetation => - 'Below 0.1 = transparent (no vegetation)'; + String get dpmYes => 'Có'; @override - String get mapLayerSatelliteTransparentNoWater => - '≤ 0 = transparent (no water)'; + String get meshtasticNoHistory => 'Not enough history yet'; @override - String get mapLayerSatelliteTransparentClear => - 'Clear sky = transparent, the basemap shows'; + String get reportDetailLocalIntensityUnavailable => + 'Không có dữ liệu cường độ'; @override - String get mapLayerStyleSection => 'Colour style'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => 'Colour style'; + String get reportListDepthUnit => 'km'; @override - String get mapLayerStyleGray => 'Grayscale (JMA)'; + String get reportFilterDepth => 'Độ sâu'; @override - String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; + String get onboardingScrollHint => 'Cuộn xuống để tiếp tục'; @override - String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + String get mapNavQpesums => 'Dự báo'; @override - String get mapLayerStyleJmaTooltip => - 'Grayscale base, tinted below −40 °C to highlight cloud-top height'; + String get navMap => 'Bản đồ'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => 'Tin cảnh báo thời tiết'; @override - String get mapLayerStyleBdTooltip => - 'Dvorak BD curve — the stepped grayscale for tropical-cyclone intensity analysis'; + String get reportFilterReset => 'Đặt lại'; @override - String get mapLayerQpesums => 'Dự báo mưa 1 giờ tới'; + String get mapLayerSatelliteMndwi => 'Himawari MNDWI'; @override - String get mapLayerLightning => 'Sét'; + String get typhoonOverlaySectionStorm => 'Storm wind'; @override - String lightningLegendCg(int minutes) { - return 'Mây–đất · $minutes phút'; - } + String get moonPhaseFull => 'Full moon'; @override - String lightningLegendCc(int minutes) { - return 'Mây–mây · $minutes phút'; - } + String get moonPhaseWaningGibbous => 'Waning gibbous'; @override - String get mapTimelineNow => 'Bây giờ'; + String get weatherDynamicStateSubtitle => + 'Ghi đè thời tiết nền của trang chủ'; @override - String get mapTimelinePast => 'Quá khứ'; + String get reportFilterIntensityInfoModernTitle => 'Mới (từ 2020)'; @override - String get mapTimelineFuture => 'Tương lai'; + String typhoonDataTime(String time) { + return 'Data time\n$time'; + } @override - String get mapTimelineObserved => 'Quan trắc'; + String get restroomTypeAccessible => 'Nhà vệ sinh tiếp cận được'; @override - String get mapTimelineForecast => 'Dự báo'; + String get moreSectionAbout => 'Giới thiệu'; @override - String mapTimelineDataTime(String time) { - return 'Thời gian dữ liệu $time'; - } + String get meshtasticSelectDevice => 'Select a radio'; @override - String get notifySettingsMenu => 'Cài đặt thông báo'; + String get onboardingIntroBody => + 'DPIP là người bạn đồng hành phòng chống thiên tai của bạn. Ứng dụng tích hợp cảnh báo sớm động đất, báo cáo động đất, thời tiết và thông tin về hiểm họa, đồng thời cảnh báo bạn ngay tại thời điểm quan trọng.\n\n• Động đất: cảnh báo sớm, báo cáo cường độ và báo cáo chi tiết\n• Thời tiết: tin nhắn mưa dông theo thời gian thực và cảnh báo thời tiết\n• Thông tin sóng thần và thảm họa\n\nTiếp theo, chúng tôi sẽ mời bạn xem lại Điều khoản Dịch vụ và cấp một vài quyền để DPIP có thể bảo vệ bạn theo thời gian thực.'; @override - String get notifyTitle => 'Thông báo'; + String get shelterCapacityLabel => 'Sức chứa'; @override - String get notifyUnavailable => - 'Thông báo đẩy chưa sẵn sàng — vui lòng thử lại sau giây lát.'; + String get reportDetailImage => 'Hình ảnh báo cáo'; @override - String get notifySetFailed => 'Không thể lưu cài đặt. Vui lòng thử lại.'; + String get meshtasticStateConfiguring => 'Configuring…'; @override - String get notifySectionEew => 'Cảnh báo sớm động đất'; + String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; @override - String get notifySectionEarthquake => 'Động đất'; + String get onboardingPermNotify => 'Thông báo'; @override - String get notifySectionWeather => 'Thời tiết'; + String get meshtasticClearMessages => 'Clear messages'; @override - String get notifySectionTsunami => 'Sóng thần'; + String get meshtasticNotifyMessages => 'Notify on new messages'; @override - String get notifySectionOther => 'Khác'; + String get defaultMapLayerSettings => 'Lớp bản đồ mặc định'; @override - String get notifyEew => 'Cảnh báo động đất khẩn cấp'; + String get moreSectionNotify => 'Thông báo'; @override - String get notifyMonitor => 'Giám sát rung chấn mạnh'; + String get notifyUnavailable => + 'Thông báo đẩy chưa sẵn sàng — vui lòng thử lại sau giây lát.'; @override - String get notifyReport => 'Báo cáo động đất'; + String get mapLayerOrderReset => 'Đặt lại thứ tự mặc định'; @override - String get notifyIntensity => 'Báo cáo cường độ chấn động'; + String get dpmAddress => 'Địa chỉ'; @override - String get notifyThunderstorm => 'Cảnh báo mưa dông'; + String get weatherRankingMergeCounty => 'Huyện/thành'; @override - String get notifyAdvisory => 'Tin cảnh báo thời tiết'; + String get moreSectionApp => 'Tải ứng dụng'; @override - String get notifyEvacuation => 'Thông tin thảm họa'; + String get reportFilterIntensityInfoLegacyBody => + 'Chỉ có mức 0–7, không tách 5−/5+/6−/6+.'; @override - String get notifyTsunami => 'Thông tin sóng thần'; + String get mapLayerSatelliteSst => 'Himawari Sea Surface Temperature'; @override - String get notifyAnnouncement => 'Thông báo'; + String get qpesumsOverlayMenuTooltip => 'Tùy chọn lớp dự báo mưa định lượng'; @override - String get notifyOptOff => 'Tắt'; + String get mapTimelineFuture => 'Tương lai'; @override - String get notifyOptAll => 'Nhận tất cả'; + String get typhoonLegendCircleAvg => 'Average circle'; @override - String get notifyOptLocalIntensity4 => 'Cường độ tại chỗ từ 4 trở lên'; + String reportFilterDepthKm(String depth) { + return '$depth km'; + } @override - String get notifyOptLocalIntensity1 => 'Cường độ tại chỗ từ 1 trở lên'; + String get typhoonLabelSe => 'SE'; @override - String get notifyOptWeatherLocal => 'Chỉ vị trí hiện tại'; + String get radarTownOutlineHint => 'Lưới chi tiết hơn'; @override - String get notifyOptTsunamiWarning => 'Chỉ cảnh báo sóng thần'; + String eewCountdown(int seconds) { + return '$seconds giây'; + } @override - String get notifyOptTsunamiAll => 'Tin và cảnh báo sóng thần'; + String get typhoonLabelGust => 'Peak gust'; @override - String get onboardingNext => 'Tiếp theo'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => 'Quay lại'; + String get sponsorTerms => 'Điều khoản sử dụng'; @override - String get onboardingScrollHint => 'Cuộn xuống để tiếp tục'; + String get restroomTypeGenderNeutral => 'Nhà vệ sinh trung tính giới'; @override - String get onboardingIntroTitle => 'Chào mừng đến với DPIP'; + String get notifyThunderstorm => 'Cảnh báo mưa dông'; @override - String get onboardingIntroBody => - 'DPIP là người bạn đồng hành phòng chống thiên tai của bạn. Ứng dụng tích hợp cảnh báo sớm động đất, báo cáo động đất, thời tiết và thông tin về hiểm họa, đồng thời cảnh báo bạn ngay tại thời điểm quan trọng.\n\n• Động đất: cảnh báo sớm, báo cáo cường độ và báo cáo chi tiết\n• Thời tiết: tin nhắn mưa dông theo thời gian thực và cảnh báo thời tiết\n• Thông tin sóng thần và thảm họa\n\nTiếp theo, chúng tôi sẽ mời bạn xem lại Điều khoản Dịch vụ và cấp một vài quyền để DPIP có thể bảo vệ bạn theo thời gian thực.'; + String get skyTimeGolden => 'Giờ vàng'; @override - String get onboardingTermsTitle => 'Điều khoản Dịch vụ'; + String get moonAge => 'Age'; @override - String get onboardingTermsBody => - 'Vui lòng đọc kỹ các lưu ý sau đây trước khi sử dụng DPIP:\n\n• Mọi thông tin phải căn cứ theo nội dung do Cục Khí tượng Trung ương Đài Loan (CWA) công bố.\n\n• Tùy thuộc vào tình trạng mạng, máy chủ, ứng dụng và nguồn dữ liệu đầu nguồn, có khả năng không nhận được thông tin; chúng tôi nỗ lực hết sức để tránh điều này nhưng không thể bảo đảm rằng nó không bao giờ xảy ra.\n\n• Rung lắc mạnh có thể lan đến vị trí của bạn trước khi thông báo được gửi tới.\n\n• Cảnh báo sớm động đất là kết quả được tính toán nhanh nên có thể chứa sai số đáng kể — hãy hiểu rõ điều này và sử dụng một cách thận trọng.\n\n• Bất kỳ hành vi nào không được cơ quan chức năng cho phép đều có thể mang rủi ro pháp lý; vui lòng tuân thủ mọi quy định hiện hành.\n\nNgoài ra, để cung cấp cảnh báo theo khu vực, dịch vụ này thu thập và tải lên vị trí gần đúng cùng mã định danh thông báo đẩy của bạn — cả ở nền trước lẫn nền sau — chỉ nhằm quyết định những cảnh báo nào sẽ gửi cho bạn.\n\nBằng việc nhấn \"Đồng ý và tiếp tục\", bạn xác nhận rằng đã đọc, hiểu và đồng ý với những điều trên.'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => - 'Tôi đã đọc và đồng ý với Điều khoản Dịch vụ'; + String weatherRankingAnalysisCurrent(String value) { + return 'Hiện tại $value°C'; + } @override - String get onboardingAgreeContinue => 'Đồng ý và tiếp tục'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => 'Quyền truy cập'; + String get homeForecastUnavailable => 'Chọn khu vực để xem dự báo'; @override - String get onboardingPermsBody => - 'Để DPIP có thể cảnh báo bạn ngay khi thảm họa xảy ra, vui lòng cấp các quyền sau. Bạn có thể thay đổi chúng bất cứ lúc nào trong cài đặt hệ thống.'; + String get mapLayers => 'Lớp bản đồ'; @override - String get onboardingPermNotify => 'Thông báo'; + String get meshtasticHardware => 'Hardware'; @override - String get onboardingPermNotifyDesc => - 'Gửi cảnh báo động đất, thời tiết và thảm họa ngay khi chúng xảy ra.'; + String get languageSettings => 'Ngôn ngữ'; @override - String get onboardingPermCritical => 'Cảnh báo quan trọng'; + String get dpmDisasterNuclear => 'Sự cố hạt nhân'; @override - String get onboardingPermCriticalDesc => - 'Cho phép các cảnh báo động đất nguy hiểm đến tính mạng phát âm thanh ngay cả khi ở chế độ im lặng hoặc Không làm phiền.'; + String get language => 'Ngôn ngữ'; @override - String get onboardingPermLocation => 'Vị trí'; + String homeForecastFeelsLike(String temp) { + return 'Cảm giác $temp°'; + } @override - String get onboardingPermLocationDesc => - 'Gửi cảnh báo phù hợp với nơi bạn đang ở.'; + String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; @override - String get onboardingPermBackground => 'Vị trí chạy nền'; + String get skyTimeDawn => 'Rạng đông'; @override - String get onboardingPermBackgroundDesc => - 'Cho phép \"Luôn luôn\" để cảnh báo vẫn nhắm đúng vị trí của bạn ngay cả khi đã đóng ứng dụng.'; + String get skyTimeAfternoon => 'Buổi chiều'; @override - String get onboardingPermBattery => 'Miễn trừ tối ưu hóa pin'; + String get meshtasticLastHeard => 'Last heard'; @override - String get onboardingPermBatteryDesc => - 'Cho phép DPIP tiếp tục chạy ở chế độ nền để cảnh báo không bị trì hoãn hay bỏ lỡ.'; + String get typhoonWarningTitle => 'Cảnh báo bão'; @override - String get onboardingGrant => 'Cấp quyền'; + String get moreSourceCode => 'Mã nguồn'; @override - String get onboardingGranted => 'Đã cấp'; + String get mapLayerCategoryWeather => 'Quan sát thời tiết'; @override - String get onboardingStart => 'Bắt đầu'; + String get mapLayerSatelliteB09 => 'Himawari Mid Water Vapour (B09)'; @override - String get language => 'Ngôn ngữ'; + String get windForecastTownOutlineHint => 'Lưới mịn hơn'; @override - String get languageSettings => 'Ngôn ngữ'; + String get mapLayerSatelliteCloudmask => 'Himawari Cloud Mask'; @override - String get languageSystem => 'Mặc định hệ thống'; + String get mapAppCopyCoordinates => 'Sao chép tọa độ'; @override - String get locationBannerServiceOff => - 'Dịch vụ vị trí đang tắt — cảnh báo khu vực không thể nhắm đúng vùng của bạn.'; + String get reportFilterIntensityInfoIntro => + 'CWA đổi thang cường độ từ 1/1/2020 (giờ Đài Bắc).'; @override - String get locationBannerPermission => - 'Chưa cấp quyền vị trí — cảnh báo khu vực không thể nhắm đúng vùng của bạn.'; + String get mapNavEarthquake => 'Động đất'; @override - String get locationBannerFix => 'Mở cài đặt'; + String get typhoonGust => 'Gió giật'; @override - String get notifyBannerDisabled => - 'Thông báo đã tắt — bạn sẽ không nhận được cảnh báo thiên tai.'; + String get restroomGradeAverage => 'Trung bình'; @override - String get onboardingSkipTitle => 'Chưa cấp quyền'; + String get mapLayerSatelliteBtdCo2 => 'Himawari Cirrus / Cloud Height'; @override - String get onboardingSkipBody => - 'Nếu không có quyền vị trí và thông báo, DPIP không thể cảnh báo tức thời về động đất và thiên tai gần bạn. Bạn vẫn có thể cấp quyền sau trong Cài đặt.'; + String get onboardingPermBackgroundDesc => + 'Cho phép \"Luôn luôn\" để cảnh báo vẫn nhắm đúng vị trí của bạn ngay cả khi đã đóng ứng dụng.'; @override - String get onboardingSkipStay => 'Quay lại'; + String get mapTimelineForecast => 'Dự báo'; @override - String get onboardingSkipLeave => 'Vẫn bỏ qua'; + String get restroomTypeLabel => 'Loại'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => 'Động đất'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => + 'Level-10 wind field + average circle (yellow)'; @override - String get moreSourceCode => 'Mã nguồn'; + String get moonPhaseWaxingGibbous => 'Waxing gibbous'; @override - String get moreSectionApp => 'Tải ứng dụng'; + String get reportDetailTitle => 'Báo cáo động đất'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'Báo cáo phát hiện TREM'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station · Thời gian dữ liệu $time'; + } @override - String get displaySettings => 'Hiển thị'; + String get meshtasticNoNodes => 'No nodes heard yet'; @override - String get defaultMapLayerSettings => 'Lớp bản đồ mặc định'; + String get meshtasticViaMqtt => 'Via MQTT (internet)'; @override - String get defaultMapLayerSubtitle => - 'Tab Bản đồ mở lớp này. Biểu tượng và nhãn thanh điều hướng dưới cũng theo lựa chọn.'; + String get radarCountyOutline => 'Ranh giới huyện thị'; @override - String get mapNavRadar => 'Radar'; + String get onboardingGranted => 'Đã cấp'; @override - String get mapNavQpesums => 'Dự báo'; + String get commonClose => 'Đóng'; @override - String get mapNavSatellite => 'Vệ tinh'; + String get restroomGradeLabel => 'Hạng'; @override - String get mapNavLightning => 'Sét'; + String get rainIntervalNow => 'Hôm nay'; @override - String get mapNavTyphoon => 'Bão'; + String get changelogCurrentVersion => 'Hiện tại'; @override - String get mapNavEarthquake => 'Động đất'; + String get typhoonLabelPressure => 'Central pressure'; @override - String get mapNavTemperature => 'Nhiệt độ'; + String get typhoonOverlayForecastCalloutsTooltip => + 'Show forecast-point detail cards when zoomed in'; @override - String get mapNavHumidity => 'Độ ẩm'; + String get aedOpenRemark => 'Ghi chú giờ mở'; @override - String get mapNavPressure => 'Khí áp'; + String get onboardingPermsBody => + 'Để DPIP có thể cảnh báo bạn ngay khi thảm họa xảy ra, vui lòng cấp các quyền sau. Bạn có thể thay đổi chúng bất cứ lúc nào trong cài đặt hệ thống.'; @override - String get mapNavWind => 'Gió'; + String get typhoonOverlaySectionWeather => 'Weather underlay'; @override - String get mapNavRain => 'Mưa'; + String get notifyOptWeatherLocal => 'Chỉ vị trí hiện tại'; @override - String get mapNavDisaster => 'Phòng thảm'; + String get mapNavRain => 'Mưa'; @override - String get displayTheme => 'Giao diện'; + String get moonDays => 'days'; @override - String get themeSystem => 'Hệ thống'; + String mapLegendUnit(String unit) { + return 'Đơn vị: $unit'; + } @override - String get themeLight => 'Sáng'; + String get weatherModeClear => 'Trời quang'; @override - String get themeDark => 'Tối'; + String get meshtasticRadio => 'Radio'; @override - String get moreSectionAbout => 'Giới thiệu'; + String get commonEmpty => 'Không có dữ liệu'; @override - String get termsOfService => 'Điều khoản dịch vụ'; + String get mapLayerSatelliteB01 => 'Himawari Blue (B01)'; @override - String get faq => 'Câu hỏi thường gặp'; + String get meshtasticExternalPower => 'External power'; @override - String get openSourceLicenses => 'Giấy phép mã nguồn mở'; + String get moonPhaseLastQuarter => 'Last quarter'; @override - String get sponsorTitle => 'Ủng hộ DPIP'; + String get reportFilterOrderAsc => 'Tăng dần'; @override - String get sponsorIntro => - 'DPIP cam kết cung cấp thông tin phòng chống thiên tai theo thời gian thực, không có quảng cáo hay mô hình lợi nhuận nào khác. Sự ủng hộ của bạn giúp chúng tôi duy trì máy chủ và tiếp tục phát triển.'; + String get reportFilterApply => 'Áp dụng'; @override - String get sponsorSubscriptions => 'Gói đăng ký'; + String get reportDetailImageUnavailable => 'Hình ảnh báo cáo chưa có sẵn'; @override - String get sponsorRecommended => 'Đề xuất'; + String get weatherRankingHighest => 'Cao nhất'; @override - String get sponsorOneTime => 'Ủng hộ một lần'; + String get reportDetailReplay => 'Phát lại'; @override - String sponsorPerMonth(String price) { - return '$price / tháng'; - } + String get mapLayerRestroom => 'Nhà vệ sinh công cộng'; @override - String get sponsorRestore => 'Khôi phục giao dịch'; + String get restroomCategoryWelfare => 'Cơ sở phúc lợi'; @override - String get sponsorTerms => 'Điều khoản sử dụng'; + String get restroomGradeExcellent => 'Xuất sắc'; @override - String get sponsorPrivacy => 'Chính sách quyền riêng tư'; + String get meshtasticLastSent => 'Last sent'; @override - String get sponsorRestoring => 'Đang khôi phục giao dịch…'; + String get meshtasticName => 'Name'; @override - String get sponsorRestoreUnavailable => - 'Không thể kết nối tới cửa hàng. Vui lòng thử lại sau.'; + String get meshtasticScan => 'Scan'; @override - String get commonClose => 'Đóng'; + String get mapLayerCategoryForecast => 'Dự báo số'; @override - String get mapLayerTemperature => 'Nhiệt độ'; + String get meshtasticChannelFailed => 'Couldn\'t set up the DPIP channel'; @override - String get trendRange24h => '24 giờ'; + String get themeSystem => 'Hệ thống'; @override - String get trendRange7d => '7 ngày'; + String get mapLayerSatelliteNdvi => 'Himawari NDVI'; @override - String get trendNoData => 'Không có dữ liệu xu hướng'; + String get typhoonLegendForecast => 'Quỹ đạo dự báo'; @override - String trendCumulativeTotal(String total) { - return 'Tổng cộng $total mm'; + String typhoonValueHpa(String n) { + return '$n hPa'; } @override - String chartHourLabel(int hour) { - return '${hour}h'; + String get weatherPrecipitation => 'Lượng mưa'; + + @override + String get moonNextFullMoon => 'Next full moon'; + + @override + String get dpmSheetEmpty => + 'Chạm vào điểm đánh dấu trên bản đồ để xem chi tiết'; + + @override + String get onboardingSkipLeave => 'Vẫn bỏ qua'; + + @override + String get onboardingBack => 'Quay lại'; + + @override + String get aedPlaceDesc => 'Vị trí đặt'; + + @override + String get onboardingSkipTitle => 'Chưa cấp quyền'; + + @override + String get restroomTypeFamily => 'Nhà vệ sinh gia đình'; + + @override + String typhoonValueKm(String n) { + return '$n km'; } @override - String get mapLayerHumidity => 'Độ ẩm'; + String get typhoonPressure => 'Áp suất'; @override - String get mapLayerPressure => 'Áp suất'; + String get onboardingPermBattery => 'Miễn trừ tối ưu hóa pin'; @override - String get mapLayerWind => 'Gió'; + String get typhoonLabelNw => 'NW'; @override - String get mapLayerRain => 'Lượng mưa'; + String get dpmDisasterFlood => 'Lũ lụt'; @override - String get rainIntervalMenu => 'Khung tích lũy'; + String get moonPhaseWaxingCrescent => 'Waxing crescent'; @override - String get rainIntervalNow => 'Hôm nay'; + String get restroomCategoryLeisure => 'Địa điểm vui chơi giải trí'; @override - String get rainInterval10m => '10 phút'; + String get mapLayerTemperature => 'Nhiệt độ'; @override - String get rainInterval1h => '1 giờ'; + String get aedCategory => 'Phân loại'; @override - String get rainInterval3h => '3 giờ'; + String get meshtasticChannels => 'Channels'; @override - String get rainInterval6h => '6 giờ'; + String get monitorWaiting => 'Đang chờ dữ liệu…'; + + @override + String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + + @override + String get reportDetailEpicenter => 'Tọa độ tâm chấn'; + + @override + String get meshtasticVoltage => 'Voltage'; + + @override + String get mapLayerMeshtasticSubtitle => + 'LoRa mesh nodes heard by your radio'; + + @override + String get mapLayerWind => 'Gió'; + + @override + String get reportDetailMagnitude => 'Độ lớn'; + + @override + String get reportDetailAreaIntensity => 'Cường độ theo khu vực'; @override String get rainInterval12h => '12 giờ'; @override - String get rainInterval24h => '24 giờ'; + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } @override - String get rainInterval2d => '2 ngày'; + String get dpmDisasterLandslide => 'Sạt lở đất'; @override - String get rainInterval3d => '3 ngày'; + String get notifyMonitor => 'Giám sát rung chấn mạnh'; @override - String get mapLayerTyphoon => 'Bão'; + String get onboardingStart => 'Bắt đầu'; @override - String get typhoonNoActive => 'Không có bão'; + String sponsorPerMonth(String price) { + return '$price / tháng'; + } @override - String get typhoonWind => 'Sức gió'; + String get mapLayerPressure => 'Áp suất'; @override - String get typhoonGust => 'Gió giật'; + String get mapLayerSatelliteB04 => 'Himawari Near-Infrared (B04)'; @override - String get typhoonPressure => 'Áp suất'; + String get mapLayerSatelliteTransparentZero => + 'Zero difference = transparent (no signal)'; @override - String get typhoonMotion => 'Di chuyển'; + String get shelterIndoorLabel => 'Trú ẩn trong nhà'; @override - String get typhoonLabelPosition => 'Centre location'; + String get notifyOptOff => 'Tắt'; @override - String get typhoonLabelDirection => 'Past movement direction'; + String get reportFilterSortTime => 'Thời gian'; + + @override + String get mapLayerSatelliteCloudProbablyClear => 'Probably clear'; + + @override + String get weatherModeThunderstorm => 'Mưa dông'; + + @override + String get homeViewOnMap => 'Xem trên bản đồ'; + + @override + String get reportFilterIntensityInfoLegacyTitle => 'Cũ (trước 2020)'; @override String get typhoonLabelSpeed => 'Past movement speed'; @override - String get typhoonLabelPressure => 'Central pressure'; + String mapAppOpenFailed(String app) { + return 'Không thể mở $app'; + } @override - String get typhoonLabelWind => 'Max. sustained wind near centre'; + String get mapLayerSatelliteRgbComposite => 'RGB composite (JMA recipe)'; @override - String get typhoonLabelGust => 'Peak gust'; + String get meshtasticReceived => 'Received'; @override - String get typhoonLabelGaleAvg => 'Avg. radius of Beaufort 7 winds'; + String get weatherRankingExtremeLow => 'Thấp nhất ngày'; @override - String get typhoonLabelStormAvg => 'Avg. radius of Beaufort 10 winds'; + String get mapLayerSatelliteB10 => 'Himawari Lower Water Vapour (B10)'; @override - String get typhoonLabelProbCircle => '70% probability circle'; + String get mapLayerSatelliteCloudProbablyCloudy => 'Probably cloudy'; @override - String typhoonForecastLead(String hours) { - return 'Forecast +$hours h'; + String get mapLayerSatelliteTransparentNoWater => + '≤ 0 = transparent (no water)'; + + @override + String get shelterCategoryLabel => 'Loại thảm họa'; + + @override + String get meshtasticStateConnecting => 'Connecting…'; + + @override + String get moonTitle => 'Moon'; + + @override + String get weatherRankingGust => 'Gió giật'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => 'Loại thiên tai nơi trú ẩn'; + + @override + String get moreServerStatus => 'Trạng thái máy chủ'; + + @override + String get notifySectionWeather => 'Thời tiết'; + + @override + String get meshtasticPreset => 'Modem preset'; + + @override + String get dataSectionSeismic => 'Địa chấn'; + + @override + String get changelogBodyEmpty => 'Không có ghi chú cho bản phát hành này.'; + + @override + String get radarGlobalOutline => 'Biên giới quốc gia'; + + @override + String get notifyEew => 'Cảnh báo động đất khẩn cấp'; + + @override + String get regionNationwide => 'Toàn quốc'; + + @override + String get moreNotifyLog => 'Nhật ký thông báo DPIP'; + + @override + String get regionCurrent => 'Vị trí hiện tại'; + + @override + String get dpmFilterSectionRestroom => 'Loại địa điểm'; + + @override + String get meshtasticNotConnected => 'Not connected to a radio'; + + @override + String get weatherModeSnow => 'Tuyết rơi'; + + @override + String get mapLayerMeshtastic => 'Meshtastic nodes'; + + @override + String get moreDeveloper => 'Thông tin gỡ lỗi'; + + @override + String get mapLayerSatelliteB14 => 'Himawari Longwave Infrared (B14)'; + + @override + String get meshtasticChannelUse => 'Channel use'; + + @override + String get mapNavLightning => 'Sét'; + + @override + String get homeForecastEmpty => 'Không có dữ liệu dự báo'; + + @override + String get sponsorOneTime => 'Ủng hộ một lần'; + + @override + String get mapLayerSatelliteBtdSplit => 'Himawari Split Window'; + + @override + String get onboardingPermBackground => 'Vị trí chạy nền'; + + @override + String get aedEmergencyPhone => 'Điện thoại khẩn cấp'; + + @override + String get dpmOpenInMaps => 'Mở trong bản đồ'; + + @override + String get meshtasticNotifyNodes => 'Notify on new nodes'; + + @override + String get onboardingPermCriticalDesc => + 'Cho phép các cảnh báo động đất nguy hiểm đến tính mạng phát âm thanh ngay cả khi ở chế độ im lặng hoặc Không làm phiền.'; + + @override + String get mapLayerSatelliteTransparentWarm => + 'Clear sky (warm end) = transparent, the basemap shows'; + + @override + String get meshtasticSent => 'Sent'; + + @override + String get homeForecastTitle => 'Dự báo 24 giờ'; + + @override + String get typhoonLegendWarningAreas => 'Vùng cảnh báo'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '$count hidden'; } @override - String get typhoonLabelNw => 'NW'; + String get notifyOptLocalIntensity1 => 'Cường độ tại chỗ từ 1 trở lên'; + + @override + String get mapTimelinePast => 'Quá khứ'; + + @override + String get restroomTypeFemale => 'Nhà vệ sinh nữ'; + + @override + String get reportListToday => 'Hôm nay'; + + @override + String get meshtasticTapNode => 'Tap a node for details'; + + @override + String get commonLoading => 'Đang tải…'; + + @override + String get typhoonIntensityModerate => 'Moderate typhoon'; + + @override + String get typhoonWind => 'Sức gió'; + + @override + String get mapLayerSatelliteAsh => 'Himawari Ash'; + + @override + String get rainInterval3h => '3 giờ'; + + @override + String get reportListSearch => 'Tìm'; + + @override + String get mapLayerCategorySatellite => 'Vệ tinh'; + + @override + String get meshtasticChannelReady => 'DPIP channel ready'; + + @override + String get reportFilterLocation => 'Địa điểm'; + + @override + String get mapLayerSatelliteNightmicrophysics => + 'Himawari Night Microphysics'; + + @override + String get typhoonIntensityTd => 'Tropical depression'; + + @override + String get reportFilterDate => 'Ngày'; + + @override + String get sponsorRestoreUnavailable => + 'Không thể kết nối tới cửa hàng. Vui lòng thử lại sau.'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => 'Chưa có khu vực nào được lưu'; + + @override + String get onboardingPermBatteryDesc => + 'Cho phép DPIP tiếp tục chạy ở chế độ nền để cảnh báo không bị trì hoãn hay bỏ lỡ.'; + + @override + String get mapNavDisaster => 'Phòng thảm'; + + @override + String get radarScanRangeSubtitle => + 'Đánh dấu vùng bốn radar thực sự quan trắc.'; + + @override + String get aedHoursSunday => 'Giờ Chủ nhật'; + + @override + String get reportDetailOriginTime => 'Thời gian xảy ra'; + + @override + String get trendNoData => 'Không có dữ liệu xu hướng'; + + @override + String get onboardingPermLocation => 'Vị trí'; + + @override + String get moreDiscord => 'Cộng đồng Discord'; + + @override + String get mapNavPressure => 'Khí áp'; + + @override + String get mapLayerSatelliteB13 => 'Himawari Infrared (B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => 'Chưa có ghi chú phát hành'; + + @override + String get reportFilterDateStartNote => 'Ngày bắt đầu: từ 00:00(Đài Bắc)'; + + @override + String get eewTitle => 'Cảnh báo sớm động đất'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return 'Đã chọn $count/$max'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'Himawari SO₂ / Cloud Phase'; + + @override + String get meshtasticStateError => 'Error'; + + @override + String get weatherModeOvercast => 'Trời âm u'; + + @override + String get reportDetailDepth => 'Độ sâu chấn tiêu'; + + @override + String get typhoonOverlayWarningTooltip => + 'Highlight counties under a typhoon warning'; + + @override + String get reportFilterDatePick => 'Chọn ngày'; + + @override + String get onboardingSkipStay => 'Quay lại'; + + @override + String get commonFetchFailed => 'Không thể tải dữ liệu. Vui lòng thử lại.'; + + @override + String get shelterOutdoorLabel => 'Trú ẩn ngoài trời'; + + @override + String get meshtasticStateConnected => 'Connected'; + + @override + String get mapNavRadar => 'Radar'; + + @override + String get mapLayerSatelliteCloudClear => 'Clear'; + + @override + String eewSummary(String magnitude, String depth) { + return 'M$magnitude · độ sâu $depth km'; + } + + @override + String get locationBannerPermission => + 'Chưa cấp quyền vị trí — cảnh báo khu vực không thể nhắm đúng vùng của bạn.'; + + @override + String get typhoonOverlayWeatherNoneTooltip => + 'No radar or infrared underlay'; + + @override + String get radarCountyOutlineHint => 'Vẽ đè lên tiếng vọng'; + + @override + String get windForecastCountyOutlineHint => 'Vẽ trên trường gió'; + + @override + String get homeRainTrendTitle => 'Mưa 1 giờ tới'; + + @override + String get moonPhaseFirstQuarter => 'First quarter'; + + @override + String get mapLayerCategoryTyphoon => 'Bão'; + + @override + String get meshtasticUtilization => 'Airtime (24h)'; + + @override + String get restroomTypeMixed => 'Nhà vệ sinh chung'; + + @override + String get restroomGradeGood => 'Tốt'; + + @override + String get notifyTsunami => 'Thông tin sóng thần'; + + @override + String get navData => 'Dữ liệu'; + + @override + String get mapLayerSatelliteBtdWvirw => 'Himawari Overshooting Top'; + + @override + String get meshtasticReadingAge => 'Reading taken'; + + @override + String get mapAppCallFailed => 'Thiết bị này không thể thực hiện cuộc gọi'; + + @override + String get reportFilterAny => 'Tất cả'; + + @override + String get weatherRankingMergeTo => 'Gộp'; + + @override + String get notifyIntensity => 'Báo cáo cường độ chấn động'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => 'Khung tích lũy'; + + @override + String get reportDetailLocalFelt => 'Động đất cảm nhận cục bộ'; + + @override + String get meshtasticDevice => 'Device'; + + @override + String get onboardingGrant => 'Cấp quyền'; + + @override + String get weatherModeRain => 'Mưa'; + + @override + String get shelterVulnerableOkLabel => 'Phù hợp người yếu thế'; + + @override + String get stationSheetEmpty => 'Chạm vào một trạm để xem số liệu'; + + @override + String get typhoonLegendProbability => 'Xác suất đổ bộ'; + + @override + String get reportFilterMagnitude => 'Độ lớn'; + + @override + String get skyTimeMorning => 'Buổi sáng'; + + @override + String get experimentalFeatures => 'Tính năng thử nghiệm'; + + @override + String get onboardingTermsBody => + 'Vui lòng đọc kỹ các lưu ý sau đây trước khi sử dụng DPIP:\n\n• Mọi thông tin phải căn cứ theo nội dung do Cục Khí tượng Trung ương Đài Loan (CWA) công bố.\n\n• Tùy thuộc vào tình trạng mạng, máy chủ, ứng dụng và nguồn dữ liệu đầu nguồn, có khả năng không nhận được thông tin; chúng tôi nỗ lực hết sức để tránh điều này nhưng không thể bảo đảm rằng nó không bao giờ xảy ra.\n\n• Rung lắc mạnh có thể lan đến vị trí của bạn trước khi thông báo được gửi tới.\n\n• Cảnh báo sớm động đất là kết quả được tính toán nhanh nên có thể chứa sai số đáng kể — hãy hiểu rõ điều này và sử dụng một cách thận trọng.\n\n• Bất kỳ hành vi nào không được cơ quan chức năng cho phép đều có thể mang rủi ro pháp lý; vui lòng tuân thủ mọi quy định hiện hành.\n\nNgoài ra, để cung cấp cảnh báo theo khu vực, dịch vụ này thu thập và tải lên vị trí gần đúng cùng mã định danh thông báo đẩy của bạn — cả ở nền trước lẫn nền sau — chỉ nhằm quyết định những cảnh báo nào sẽ gửi cho bạn.\n\nBằng việc nhấn \"Đồng ý và tiếp tục\", bạn xác nhận rằng đã đọc, hiểu và đồng ý với những điều trên.'; + + @override + String get reportFilterTitle => 'Bộ lọc'; + + @override + String get onboardingPermCritical => 'Cảnh báo quan trọng'; + + @override + String trendCumulativeTotal(String total) { + return 'Tổng cộng $total mm'; + } + + @override + String get languageName => 'Tiếng Việt'; + + @override + String get reportListEmptyFiltered => 'Không có báo cáo khớp bộ lọc'; + + @override + String get meshtasticExcludeMqtt => 'Hide MQTT nodes'; + + @override + String get mapNavTyphoon => 'Bão'; + + @override + String get weatherModeSand => 'Bụi cát'; + + @override + String get typhoonSatelliteTitle => 'Vệ tinh'; + + @override + String get notifyReport => 'Báo cáo động đất'; + + @override + String get mapAppCoordinatesCopied => 'Đã sao chép tọa độ'; + + @override + String get skyTimeNight => 'Ban đêm'; + + @override + String get sponsorRecommended => 'Đề xuất'; + + @override + String get mapLayerSatelliteB15 => 'Himawari Longwave Infrared (B15)'; + + @override + String get weatherRankingWind => 'Tốc độ gió'; + + @override + String get feedStale => 'Dữ liệu có thể đã lỗi thời'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · Cấp $level'; + } + + @override + String get navHome => 'Trang chủ'; + + @override + String get meshtasticRegionLabel => 'Region'; + + @override + String get mapLayerSatelliteCloudtop => 'Himawari Cloud Top Temperature'; + + @override + String get moonTimelineCaption => 'Phase'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth km'; + } + + @override + String get openSourceLicenses => 'Giấy phép mã nguồn mở'; + + @override + String get weatherRankingLowest => 'Thấp nhất'; + + @override + String get reportFilterSortDepth => 'Độ sâu'; + + @override + String mapTimelineDataTime(String time) { + return 'Thời gian dữ liệu $time'; + } + + @override + String get radarScanRange => 'Hiện phạm vi quét'; + + @override + String get meshtasticHopLimit => 'Hop limit'; + + @override + String weatherRankingAnalysisRange(String value) { + return 'Biên độ $value°C'; + } + + @override + String get weatherRankingExtremeHigh => 'Cao nhất ngày'; + + @override + String get changelogVersionDetails => 'Chi tiết phiên bản'; + + @override + String get sponsorPrivacy => 'Chính sách quyền riêng tư'; + + @override + String get reportDetailLocalIntensity => 'Cường độ tại vị trí của bạn'; + + @override + String get mapLayerSatelliteNaturalcolor => 'Himawari Natural Color'; + + @override + String get meshtasticAirtime => 'Air time (TX)'; + + @override + String shelterCapacityValue(int n) { + return '$n người'; + } + + @override + String lightningLegendCc(int minutes) { + return 'Mây–mây · $minutes phút'; + } + + @override + String get meshtasticSendHint => 'Message to broadcast'; + + @override + String monitorDelay(String value) { + return 'Độ trễ $value s'; + } + + @override + String get dpmNo => 'Không'; + + @override + String get mapLayerSatelliteB08 => 'Himawari Upper Water Vapour (B08)'; + + @override + String get meshtasticReconnecting => 'Reconnecting…'; + + @override + String get radarTownOutlineSubtitle => + 'Giữ ranh giới xã phường rõ ràng dưới lớp phản hồi radar.'; + + @override + String get typhoonOverlayWeatherSatelliteTooltip => + 'Infrared closest to the typhoon bulletin time'; + + @override + String get radarScanRangeHint => 'Ngoài khung là chưa quan trắc'; + + @override + String typhoonPickerTd(String no) { + return 'Tropical depression TD $no'; + } + + @override + String get mapLayerSatelliteWatervapor => 'Himawari Water Vapour'; + + @override + String get regionAddButton => 'Thêm khu vực'; + + @override + String get displaySettings => 'Hiển thị'; + + @override + String get restroomGradePoor => 'Dưới chuẩn'; + + @override + String get restroomCategoryTourist => 'Khu du lịch thắng cảnh'; + + @override + String get locationBannerServiceOff => + 'Dịch vụ vị trí đang tắt — cảnh báo khu vực không thể nhắm đúng vùng của bạn.'; + + @override + String get mapLayerStyleTooltip => 'Colour style'; + + @override + String lightningLegendCg(int minutes) { + return 'Mây–đất · $minutes phút'; + } + + @override + String get skyTimeAuto => 'Tự động'; + + @override + String get appLogs => 'Nhật ký ứng dụng'; + + @override + String get feedConnecting => 'Đang kết nối…'; + + @override + String get notifyBannerDisabled => + 'Thông báo đã tắt — bạn sẽ không nhận được cảnh báo thiên tai.'; + + @override + String get weatherHumidity => 'Độ ẩm'; + + @override + String typhoonValueMs(String n) { + return '$n m/s'; + } + + @override + String homeForecastHumidity(String value) { + return 'Độ ẩm $value%'; + } + + @override + String get meshtasticBusyBody => + 'Disconnect it in the other Meshtastic app first. Two apps on one radio take each other\'s messages, so some will go missing.'; + + @override + String get meshtasticChannelNoSlot => + 'No free channel slot — free one on the radio'; + + @override + String get restroomCategoryTransport => 'Giao thông'; + + @override + String get reportFilterLocationHint => 'vd: Hoa Liên, ngoài khơi'; + + @override + String get moonSubtitle => 'Lunar phase and illumination — computed locally'; + + @override + String get meshtasticBattery => 'Battery'; + + @override + String get meshtasticDistance => 'Khoảng cách'; + + @override + String get meshtasticSnrTrend => 'Xu hướng tín hiệu (SNR)'; + + @override + String get meshtasticBatteryTrend => 'Xu hướng pin'; + + @override + String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + + @override + String get mapLayerSatelliteBtdOzone => 'Himawari Tropopause'; + + @override + String meshtasticRegionMismatch(String region) { + return 'Radio region is $region — DPIP needs TW'; + } + + @override + String get notifySectionEarthquake => 'Động đất'; + + @override + String get mapLayerDisasterMap => 'Bản đồ phòng chống'; + + @override + String get weatherModeFog => 'Sương mù'; + + @override + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } + + @override + String get mapLayerStyleGrayTooltip => 'JMA grayscale — colder is whiter'; + + @override + String get moreAnnouncements => 'Thông báo'; + + @override + String get mapLayerSatelliteTransparentNoData => + 'No data (land) = transparent'; + + @override + String get restroomCategoryGovernment => 'Cơ quan công quyền'; + + @override + String get typhoonLegendCurrent => 'Tâm hiện tại'; + + @override + String get aedAddress => 'Địa chỉ'; + + @override + String get mapLayerAed => 'AED'; + + @override + String get changelogTypePrerelease => 'Thử nghiệm'; + + @override + String get reportFilterIntensityInfoModernBody => + 'Các mức 0–4, 5−, 5+, 6−, 6+, 7. Thanh lọc dùng thang mới; sự kiện cũ vẫn hiện nhãn cũ trong danh sách.'; + + @override + String get typhoonOverlayWeatherNone => 'None'; + + @override + String get mapLayerStyleGray => 'Grayscale (JMA)'; + + @override + String get weatherModeAuto => 'Tự động'; + + @override + String get typhoonLabelProbCircle => '70% probability circle'; + + @override + String get notifyOptAll => 'Nhận tất cả'; + + @override + String get displayTheme => 'Giao diện'; + + @override + String get mapLayerSatelliteB07 => 'Himawari Shortwave Infrared (B07)'; + + @override + String get typhoonLabelDirection => 'Past movement direction'; + + @override + String get regionManageTitle => 'Khu vực đã lưu'; + + @override + String get typhoonLegendCone => 'Nón dự báo'; + + @override + String get moreCwaEew => 'Cảnh báo sớm động đất của CWA'; + + @override + String get onboardingPermsTitle => 'Quyền truy cập'; + + @override + String get mapLayerStyleJma => 'Cloud-top enhancement (JMA)'; + + @override + String get rainInterval10m => '10 phút'; + + @override + String weatherRankingAnalysisLow(String value) { + return 'Thấp $value'; + } + + @override + String get meshtasticConnectAnyway => 'Connect anyway'; + + @override + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => 'Himawari Near-Infrared (B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => + 'Low reflectance / night = transparent, the basemap shows'; + + @override + String chartHourLabel(int hour) { + return '${hour}h'; + } + + @override + String get mapLayerShelter => 'Nơi trú ẩn'; + + @override + String get typhoonOverlayProbabilityTooltip => + 'Show strike probability (hides the forecast cone)'; + + @override + String get mapLayerSatelliteNdwi => 'Himawari NDWI'; + + @override + String get disasterMapOverlayShelterTooltip => 'Hiển thị nơi trú ẩn'; + + @override + String get mapNavHumidity => 'Độ ẩm'; + + @override + String get reportDetailSortByIntensity => 'Sắp xếp theo cường độ'; + + @override + String get homeRainTrendNoData => 'Không có dữ liệu'; + + @override + String get mapLayerCategoryRadar => 'Ra đa'; + + @override + String get meshtasticShortName => 'Short name'; + + @override + String get mapLayerSatelliteAirmass => 'Himawari Airmass'; + + @override + String get typhoonTrackDetail => 'Chi tiết quỹ đạo'; + + @override + String get dataSectionWeather => 'Thời tiết'; + + @override + String get aedHoursWeekday => 'Giờ ngày thường'; + + @override + String get homeActiveEventsTitle => 'Sự kiện đang hiệu lực'; + + @override + String weatherRankingAnalysisHigh(String value) { + return 'Cao $value'; + } + + @override + String get faq => 'Câu hỏi thường gặp'; + + @override + String get typhoonHistoryLive => 'Trực tiếp'; + + @override + String eewSerial(int serial) { + return 'Bản tin $serial'; + } + + @override + String get reportFilterSort => 'Sắp xếp'; + + @override + String get meshtasticRegionConfirm => + 'Switch this radio to the TW region? It restarts and disconnects for a moment, and every other channel on it moves too.'; + + @override + String get dataEarthquakeSubtitle => 'Báo cáo động đất'; @override - String get typhoonLabelNe => 'NE'; + String get typhoonNoActive => 'Không có bão'; @override - String get typhoonLabelSw => 'SW'; + String get mapLayerSatelliteB11 => 'Himawari SO₂ / Cloud Phase (B11)'; @override - String get typhoonLabelSe => 'SE'; + String get navEvents => 'Sự kiện'; @override - String typhoonValueLat(String lat) { - return '$lat°N'; - } + String get onboardingTermsTitle => 'Điều khoản Dịch vụ'; @override - String typhoonValueLon(String lon) { - return '$lon°E'; - } + String get mapTownLabels => 'Tên hương trấn'; @override - String typhoonValueKm(String n) { - return '$n km'; - } + String get notifySetFailed => 'Không thể lưu cài đặt. Vui lòng thử lại.'; @override - String typhoonValueHpa(String n) { - return '$n hPa'; - } + String get meshtasticDisconnect => 'Disconnect'; @override - String typhoonValueMs(String n) { - return '$n m/s'; - } + String get meshtasticUndecoded => 'Not decrypted'; @override - String typhoonDataTime(String time) { - return 'Data time\n$time'; - } + String get notifyAnnouncement => 'Thông báo'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => 'Chào mừng đến với DPIP'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => 'Không thể lấy vị trí hiện tại'; @override - String get mapLayerMonitor => 'Giám sát địa chấn'; + String get languageSystem => 'Mặc định hệ thống'; @override - String get mapLayerDisasterMap => 'Bản đồ phòng chống'; + String get skyTimeSunset => 'Hoàng hôn'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'Himawari Dust'; @override - String get disasterMapOverlayMenuTooltip => 'Lớp bản đồ phòng chống'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => 'Lớp'; + String get regionEdit => 'Sửa'; @override - String get disasterMapOverlayAedTooltip => 'Hiện vị trí AED'; + String get weatherDynamicState => 'Hoạt ảnh thời tiết'; @override - String get aedAddress => 'Địa chỉ'; + String get mapPlaceholderDisabled => 'Bản đồ (tạm thời vô hiệu hóa)'; @override - String get aedRegion => 'Khu vực'; + String get moonNow => 'Bây giờ'; @override - String get aedCategory => 'Phân loại'; + String get moonSectionAppearance => 'Diện mạo'; @override - String get aedType => 'Loại'; + String get moonSectionRiseSet => 'Mọc và lặn'; @override - String get aedPlaceDesc => 'Vị trí đặt'; + String get moonSectionUpcoming => 'Sắp tới'; @override - String get aedDescription => 'Ghi chú'; + String get moonSectionCalendar => 'Lịch'; @override - String get aedHoursWeekday => 'Giờ ngày thường'; + String get moonDistance => 'Khoảng cách'; @override - String get aedHoursSaturday => 'Giờ thứ Bảy'; + String get moonKilometres => 'km'; @override - String get aedHoursSunday => 'Giờ Chủ nhật'; + String get moonApparentSize => 'Đường kính biểu kiến'; @override - String get aedOpenRemark => 'Ghi chú giờ mở'; + String get moonRise => 'Trăng mọc'; @override - String get aedEmergencyPhone => 'Điện thoại khẩn cấp'; + String get moonSet => 'Trăng lặn'; @override - String get mapLayerRestroom => 'Nhà vệ sinh công cộng'; + String get moonNextNewMoon => 'Trăng non tiếp theo'; @override - String get mapLayerShelter => 'Nơi trú ẩn'; + String get moonAlwaysUp => 'Trên chân trời cả ngày'; @override - String get disasterMapOverlayRestroomTooltip => - 'Hiển thị nhà vệ sinh công cộng'; + String get moonNoEvent => 'Không có hôm nay'; @override - String get disasterMapOverlayShelterTooltip => 'Hiển thị nơi trú ẩn'; + String get sunTitle => 'Mặt Trời'; @override - String get dpmOpenInMaps => 'Mở trong bản đồ'; + String get sunSubtitle => 'Bình minh, hoàng hôn và tiết khí'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => 'Ánh sáng ban ngày'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => 'Hoàng hôn'; @override - String mapAppDefault(String app) { - return '$app (mặc định)'; - } + String get sunSectionLight => 'Ánh sáng'; @override - String get mapAppCopyCoordinates => 'Sao chép tọa độ'; + String get sunSectionSundial => 'Đồng hồ mặt trời'; @override - String get mapAppCoordinatesCopied => 'Đã sao chép tọa độ'; + String get sunSectionTerms => 'Tiết khí'; @override - String mapAppOpenFailed(String app) { - return 'Không thể mở $app'; - } + String get sunRise => 'Mặt Trời mọc'; @override - String get mapAppCallFailed => 'Thiết bị này không thể thực hiện cuộc gọi'; + String get sunSet => 'Mặt Trời lặn'; @override - String get mapOverlaySectionReference => 'Lớp tham chiếu'; + String get sunNoon => 'Chính ngọ'; @override - String get mapLayerCategoryEarthquake => 'Động đất'; + String get sunDayLength => 'Độ dài ngày'; @override - String get mapLayerCategoryTyphoon => 'Bão'; + String get sunTwilightCivil => 'Dân dụng'; @override - String get mapLayerCategoryWeather => 'Quan sát thời tiết'; + String get sunTwilightNautical => 'Hàng hải'; @override - String get mapLayerCategorySatellite => 'Vệ tinh'; + String get sunTwilightAstronomical => 'Thiên văn'; @override - String get mapLayerCategoryRadar => 'Ra đa'; + String get sunGoldenHourMorning => 'Giờ vàng buổi sáng'; @override - String get mapLayerCategoryLife => 'Đời sống'; + String get sunGoldenHourEvening => 'Giờ vàng buổi chiều'; @override - String get mapLayerCategoryForecast => 'Dự báo số'; + String get sunBlueHour => 'Giờ xanh'; @override - String get mapOverlaySectionMap => 'Bản đồ'; + String get sunEquationOfTime => 'Phương trình thời gian'; @override - String get rainIntervalSection => 'Khoảng thời gian'; + String get sunMinutes => 'phút'; @override - String get mapTownLabels => 'Tên hương trấn'; + String get solarTermNext => 'Tiết khí tiếp theo'; @override - String get mapTownLabelsHint => 'Hiển thị tên hương trấn khi phóng to'; + String get planetsTitle => 'Hành tinh'; @override - String get mapTerrainRelief => 'Độ nổi địa hình'; + String get planetsSubtitle => 'Đêm nay ở đâu, sáng bao nhiêu'; @override - String get mapTerrainReliefHint => 'Hiển thị địa hình nổi trên bản đồ nền'; + String get planetsSectionTonight => 'Hiện tại'; @override - String get dpmSheetEmpty => - 'Chạm vào điểm đánh dấu trên bản đồ để xem chi tiết'; + String get planetUp => 'Trên chân trời'; @override - String get dpmAddress => 'Địa chỉ'; + String get planetDown => 'Dưới chân trời'; @override - String get restroomTypeLabel => 'Loại'; + String get planetInGlare => 'Quá gần Mặt Trời'; @override - String get restroomCategoryLabel => 'Hạng mục'; + String get planetMagnitude => 'Cấp sao'; @override - String get restroomGradeLabel => 'Hạng'; + String get planetElongation => 'Ly giác'; @override - String get restroomTypeFemale => 'Nhà vệ sinh nữ'; + String get planetSky => 'Thời điểm'; @override - String get restroomTypeMale => 'Nhà vệ sinh nam'; + String get planetEvening => 'Sao Hôm'; @override - String get restroomTypeMixed => 'Nhà vệ sinh chung'; + String get planetMorning => 'Sao Mai'; @override - String get restroomTypeAccessible => 'Nhà vệ sinh tiếp cận được'; + String get planetDistance => 'Khoảng cách'; @override - String get restroomTypeGenderNeutral => 'Nhà vệ sinh trung tính giới'; + String get planetAu => 'au'; @override - String get restroomTypeFamily => 'Nhà vệ sinh gia đình'; + String get planetAltitude => 'Độ cao'; @override - String get restroomTypeUnspecified => 'Không xác định'; + String get planetMercury => 'Sao Thủy'; @override - String get restroomCategoryTransport => 'Giao thông'; + String get planetVenus => 'Sao Kim'; @override - String get restroomCategoryPark => 'Công viên'; + String get planetMars => 'Sao Hỏa'; @override - String get restroomCategoryCommercial => 'Cơ sở thương mại'; + String get planetJupiter => 'Sao Mộc'; @override - String get restroomCategoryReligious => 'Nơi tôn giáo'; + String get planetSaturn => 'Sao Thổ'; @override - String get restroomCategoryCultural => 'Địa điểm văn hóa giải trí'; + String get planetUranus => 'Sao Thiên Vương'; @override - String get restroomCategoryGovernment => 'Cơ quan công quyền'; + String get planetNeptune => 'Sao Hải Vương'; @override - String get restroomCategoryWelfare => 'Cơ sở phúc lợi'; + String get solarTermVernalEquinox => 'Xuân phân'; @override - String get restroomCategoryTourist => 'Khu du lịch thắng cảnh'; + String get solarTermPureBrightness => 'Thanh minh'; @override - String get restroomCategoryLeisure => 'Địa điểm vui chơi giải trí'; + String get solarTermGrainRain => 'Cốc vũ'; @override - String get restroomCategoryOther => 'Khác'; + String get solarTermStartOfSummer => 'Lập hạ'; @override - String get restroomGradeExcellent => 'Xuất sắc'; + String get solarTermGrainFull => 'Tiểu mãn'; @override - String get restroomGradeGood => 'Tốt'; + String get solarTermGrainInEar => 'Mang chủng'; @override - String get restroomGradeAverage => 'Trung bình'; + String get solarTermSummerSolstice => 'Hạ chí'; @override - String get restroomGradePoor => 'Dưới chuẩn'; + String get solarTermMinorHeat => 'Tiểu thử'; @override - String get shelterAddressLabel => 'Địa chỉ'; + String get solarTermMajorHeat => 'Đại thử'; @override - String get shelterCapacityLabel => 'Sức chứa'; + String get solarTermStartOfAutumn => 'Lập thu'; @override - String shelterCapacityValue(int n) { - return '$n người'; - } + String get solarTermEndOfHeat => 'Xử thử'; @override - String get shelterCategoryLabel => 'Loại thảm họa'; + String get solarTermWhiteDew => 'Bạch lộ'; @override - String get shelterIndoorLabel => 'Trú ẩn trong nhà'; + String get solarTermAutumnalEquinox => 'Thu phân'; @override - String get shelterOutdoorLabel => 'Trú ẩn ngoài trời'; + String get solarTermColdDew => 'Hàn lộ'; @override - String get shelterVulnerableOkLabel => 'Phù hợp người yếu thế'; + String get solarTermFrostDescent => 'Sương giáng'; @override - String get dpmYes => 'Có'; + String get solarTermStartOfWinter => 'Lập đông'; @override - String get dpmNo => 'Không'; + String get solarTermMinorSnow => 'Tiểu tuyết'; @override - String get stationSheetEmpty => 'Chạm vào một trạm để xem số liệu'; + String get solarTermMajorSnow => 'Đại tuyết'; @override - String monitorDelay(String value) { - return 'Độ trễ $value s'; - } + String get solarTermWinterSolstice => 'Đông chí'; @override - String get monitorWaiting => 'Đang chờ dữ liệu…'; + String get solarTermMinorCold => 'Tiểu hàn'; @override - String mapLegendUnit(String unit) { - return 'Đơn vị: $unit'; - } + String get solarTermMajorCold => 'Đại hàn'; @override - String get typhoonLegendPast => 'Quỹ đạo thực tế'; + String get solarTermStartOfSpring => 'Lập xuân'; @override - String get typhoonIntensityTd => 'Tropical depression'; + String get solarTermRainWater => 'Vũ thủy'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => 'Kinh trập'; @override - String typhoonPickerTd(String no) { - return 'Tropical depression TD $no'; - } + String get tonightTitle => 'Đêm nay'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => 'Có thể quan sát gì, và khi nào'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => 'Cửa sổ quan sát'; @override - String get typhoonIntensityMild => 'Mild typhoon'; + String get tonightAstronomicalNight => 'Đêm thiên văn'; @override - String get typhoonIntensityModerate => 'Moderate typhoon'; + String get tonightNeverDark => 'Không bao giờ tối hẳn'; @override - String get typhoonIntensityIntense => 'Intense typhoon'; + String get tonightDarkWindow => 'Cửa sổ tối'; @override - String get typhoonLegendForecast => 'Quỹ đạo dự báo'; + String get tonightMoonAllNight => 'Trăng lên suốt đêm'; @override - String get typhoonLegendForecastPoint => 'Điểm dự báo'; + String get tonightDarkTotal => 'Tổng thời gian tối'; @override - String get typhoonLegendCurrent => 'Tâm hiện tại'; + String get tonightMoonlight => 'Ánh trăng'; @override - String get typhoonLegendCone => 'Nón dự báo'; + String get tonightSectionShowers => 'Mưa sao băng'; @override - String get mapLegendExpand => 'Chú giải'; + String get tonightRadiantDown => 'Tâm điểm không mọc'; @override - String get mapLegendCollapse => 'Ẩn chú giải'; + String get tonightPerHour => 'sao/giờ'; @override - String get mapMyLocation => 'Vị trí của tôi'; + String get tonightSectionSatellites => 'Vệ tinh bay qua'; @override - String get mapResetNorth => 'Về hướng bắc'; + String get tonightSectionTargets => 'Mục tiêu đang lên'; @override - String get typhoonLegendCircle15 => 'Vòng gió mạnh'; + String get showerQuadrantids => 'Quadrantids'; @override - String get typhoonLegendCircleAvg => 'Average circle'; + String get showerLyrids => 'Lyrids'; @override - String get typhoonLegendCircle25 => 'Vòng bão'; + String get showerEtaAquariids => 'Eta Aquariids'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get showerDeltaAquariids => 'Delta Aquariids'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => 'Perseids'; @override - String get typhoonLegendProbability => 'Xác suất đổ bộ'; + String get showerOrionids => 'Orionids'; @override - String get typhoonLegendWarningAreas => 'Vùng cảnh báo'; + String get showerSouthernTaurids => 'Nam Taurids'; @override - String get typhoonOverlayMenuTooltip => 'Typhoon overlay options'; + String get showerLeonids => 'Leonids'; @override - String get typhoonOverlaySectionStorm => 'Storm wind'; + String get showerGeminids => 'Geminids'; @override - String get typhoonOverlaySectionExtra => 'Overlays'; + String get showerUrsids => 'Ursids'; @override - String get typhoonOverlayStormBandSubtitle => 'With average circle'; + String get deepSkyOpenCluster => 'Cụm sao mở'; @override - String get typhoonOverlayProbabilityHint => 'Hides the forecast cone'; + String get deepSkyGlobularCluster => 'Cụm sao cầu'; @override - String get typhoonOverlayProbabilityTooltip => - 'Show strike probability (hides the forecast cone)'; + String get deepSkySpiralGalaxy => 'Thiên hà xoắn ốc'; @override - String get typhoonOverlayWarningTooltip => - 'Highlight counties under a typhoon warning'; + String get deepSkyEllipticalGalaxy => 'Thiên hà elip'; @override - String get typhoonOverlayStormL7Tooltip => - 'Level-7 wind field + average circle (purple)'; + String get deepSkyIrregularGalaxy => 'Thiên hà vô định hình'; @override - String get typhoonOverlayStormL10Tooltip => - 'Level-10 wind field + average circle (yellow)'; + String get deepSkyPlanetaryNebula => 'Tinh vân hành tinh'; @override - String get typhoonOverlaySectionWeather => 'Weather underlay'; + String get deepSkySupernovaRemnant => 'Tàn dư siêu tân tinh'; @override - String get typhoonOverlayWeatherNone => 'None'; + String get deepSkyEmissionNebula => 'Tinh vân phát xạ'; @override - String get typhoonOverlayWeatherHint => 'Aligned to bulletin time'; + String get deepSkyReflectionNebula => 'Tinh vân phản xạ'; @override - String get typhoonOverlayWeatherNoneTooltip => - 'No radar or infrared underlay'; + String get deepSkyAsterism => 'Chòm sao nhỏ'; @override - String get typhoonOverlayWeatherRadarTooltip => - 'Radar echo closest to the typhoon bulletin time'; + String get almanacTitle => 'Lịch pháp'; @override - String get typhoonOverlayWeatherSatelliteTooltip => - 'Infrared closest to the typhoon bulletin time'; + String get almanacSubtitle => + 'Ngày âm lịch và nhật thực, nguyệt thực sắp tới'; @override - String get typhoonWarningTitle => 'Cảnh báo bão'; + String get almanacSectionToday => 'Hôm nay'; @override - String typhoonWarningAreas(String areas) { - return 'Khu vực: $areas'; - } + String get almanacGregorian => 'Dương lịch'; @override - String get typhoonTrackDetail => 'Chi tiết quỹ đạo'; + String get almanacLunar => 'Âm lịch'; @override - String get typhoonHistoryTitle => 'Thời điểm dữ liệu'; + String get almanacYear => 'Can chi'; @override - String get typhoonHistoryLive => 'Trực tiếp'; + String get almanacMonthLength => 'Độ dài tháng'; @override - String get typhoonSatelliteTitle => 'Vệ tinh'; + String get almanacLongMonth => '30 ngày'; @override - String get typhoonOverlayForecastCallouts => 'Forecast tooltips'; + String get almanacShortMonth => '29 ngày'; @override - String get typhoonOverlayForecastCalloutsTooltip => - 'Show forecast-point detail cards when zoomed in'; + String get almanacLeapPrefix => 'Nhuận '; @override - String get dpmFilterSectionRestroom => 'Loại địa điểm'; + String get almanacSectionLunarEclipses => 'Nguyệt thực'; @override - String get dpmFilterSectionRestroomType => 'Loại nhà vệ sinh'; + String get almanacSectionSolarEclipses => 'Nhật thực'; @override - String get dpmFilterSectionShelter => 'Loại thiên tai nơi trú ẩn'; + String get almanacNoSolarEclipse => 'Không có trong phạm vi'; @override - String get dpmDisasterFlood => 'Lũ lụt'; + String get eclipseTotal => 'Toàn phần'; @override - String get dpmDisasterEarthquake => 'Động đất'; + String get eclipsePartial => 'Một phần'; @override - String get dpmDisasterLandslide => 'Sạt lở đất'; + String get eclipseAnnular => 'Hình khuyên'; @override - String get dpmDisasterTsunami => 'Sóng thần'; + String get eclipsePenumbral => 'Nửa tối'; @override - String get dpmDisasterSlope => 'Thiên tai sườn dốc'; + String get zodiacRat => 'Tý'; @override - String get dpmDisasterNuclear => 'Sự cố hạt nhân'; + String get zodiacOx => 'Sửu'; @override - String get skyTime => 'Thời gian bầu trời'; + String get zodiacTiger => 'Dần'; @override - String get skyTimeAuto => 'Tự động'; + String get zodiacRabbit => 'Mão'; @override - String get skyTimeDawn => 'Rạng đông'; + String get zodiacDragon => 'Thìn'; @override - String get skyTimeSunrise => 'Bình minh'; + String get zodiacSnake => 'Tỵ'; @override - String get skyTimeMorning => 'Buổi sáng'; + String get zodiacHorse => 'Ngọ'; @override - String get skyTimeNoon => 'Buổi trưa'; + String get zodiacGoat => 'Mùi'; @override - String get skyTimeAfternoon => 'Buổi chiều'; + String get zodiacMonkey => 'Thân'; @override - String get skyTimeGolden => 'Giờ vàng'; + String get zodiacRooster => 'Dậu'; @override - String get skyTimeSunset => 'Hoàng hôn'; + String get zodiacDog => 'Tuất'; @override - String get skyTimeDusk => 'Chạng vạng'; + String get zodiacPig => 'Hợi'; @override - String get skyTimeNight => 'Ban đêm'; + String get tideTitle => 'Thủy triều'; @override - String get weatherModeCloudy => 'Nhiều mây'; + String get tideSubtitle => 'Triều cường, triều kém và lực hút của Mặt Trăng'; @override - String get weatherModeOvercast => 'Trời âm u'; + String get tideDisclaimer => + 'Chỉ là lực triều thiên văn, không phải bảng thủy triều cảng. Mực nước xin xem bảng do CWA công bố.'; @override - String get weatherModeSnow => 'Tuyết rơi'; + String get tideSectionNow => 'Hiện tại'; @override - String get weatherModeSand => 'Bụi cát'; + String get tidePhase => 'Chu kỳ'; @override - String get radarScanRange => 'Hiện phạm vi quét'; + String get tideSpring => 'Triều cường'; @override - String get radarScanRangeSubtitle => - 'Đánh dấu vùng bốn radar thực sự quan trắc.'; + String get tideNeap => 'Triều kém'; @override - String get radarScanRangeHint => 'Ngoài khung là chưa quan trắc'; + String get tideMiddling => 'Trung bình'; @override - String get radarOverlayMenuTooltip => 'Tùy chọn lớp radar'; + String get tideLunarDistanceFactor => 'Lực hút Mặt Trăng'; @override - String get radarCountyOutline => 'Ranh giới huyện thị'; + String get tideEquilibrium => 'Triều cân bằng'; @override - String get radarGlobalOutline => 'Biên giới quốc gia'; + String get tideMetres => 'm'; @override - String get radarGlobalOutlineHint => 'Khung ngoài của mỗi quốc gia'; + String get tidePerigeanSpring => 'Triều cường cận điểm tới'; @override - String get radarCountyOutlineHint => 'Vẽ đè lên tiếng vọng'; + String get tideSectionTurningPoints => 'Điểm ngoặt'; @override - String get radarCountyOutlineSubtitle => - 'Giữ ranh giới rõ ràng dưới lớp phản hồi radar.'; + String get tideHigh => 'Cao'; @override - String get radarTownOutline => 'Ranh giới xã phường'; + String get tideLow => 'Thấp'; @override - String get radarTownOutlineHint => 'Lưới chi tiết hơn'; + String get skyChartTitle => 'Bản đồ sao'; @override - String get radarTownOutlineSubtitle => - 'Giữ ranh giới xã phường rõ ràng dưới lớp phản hồi radar.'; + String get skyChartSubtitle => 'Bầu trời nhìn bằng mắt thường'; @override - String get qpesumsOverlayMenuTooltip => 'Tùy chọn lớp dự báo mưa định lượng'; + String get skyChartNorth => 'B'; @override - String get windForecastOverlayMenuTooltip => 'Tùy chọn lớp dự báo gió'; + String get skyChartEast => 'Đ'; @override - String get windForecastCountyOutlineHint => 'Vẽ trên trường gió'; + String get skyChartSouth => 'N'; @override - String get windForecastGlobalOutlineHint => 'Khung ngoài của mỗi quốc gia'; + String get skyChartWest => 'T'; @override - String get windForecastTownOutlineHint => 'Lưới mịn hơn'; + String tonightElementAge(int days) { + return 'dữ liệu quỹ đạo $days ngày trước'; + } @override - String eewSerial(int serial) { - return 'Bản tin $serial'; + String almanacLunarDate(String leap, int month, int day) { + return '${leap}tháng $month ngày $day'; } @override - String get eewMaxIntensity => 'Cường độ tối đa'; + String get tonightNoShowers => 'Không có mưa sao băng'; @override - String get eewLocalIntensity => 'Ước tính tại vị trí'; + String get tonightNoPasses => 'Không có lượt bay qua nhìn thấy trong 48 giờ'; @override - String get eewSWave => 'Sóng S'; + String get tonightSatellitesUnavailable => 'Không đọc được dữ liệu quỹ đạo'; @override - String get eewArrived => 'Đã đến'; + String get tonightNoTargets => 'Không có mục tiêu đủ cao'; @override - String eewCountdown(int seconds) { - return '$seconds giây'; - } + String get skyChartUnavailable => 'Không đọc được danh mục sao'; } diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 1773ae645..5a059ada4 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -9,507 +10,496 @@ class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); @override - String get languageName => '繁體中文(臺灣)'; + String typhoonValueLat(String lat) { + return '北緯 $lat 度'; + } @override - String get navHome => '首頁'; + String get onboardingSkipBody => + '未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。'; @override - String get navEvents => '事件'; + String get rainInterval24h => '24 時'; @override - String get navMap => '地圖'; + String homeRainTrendHeavyStopping(int minutes) { + return '預計 $minutes 分鐘後停止下大雨'; + } @override - String get navData => '資料'; + String get mapTimelineObserved => '觀測'; @override - String get navEarthquake => '地震'; + String get regionSelectTitle => '選擇地區'; @override - String get dataSectionSeismic => '地震'; + String get skyTimeNoon => '正午'; @override - String get dataEarthquakeSubtitle => '地震報告'; + String get radarCountyOutlineSubtitle => '讓縣市界線在雷達回波下仍然清楚。'; @override - String get dataSectionWeather => '氣象'; + String get dpmFilterSectionRestroomType => '廁所類型'; @override - String get dataWeatherRankingSubtitle => '即時觀測排行'; + String get mapLayerSatelliteB03 => 'ひまわり 可見光-紅(B03)'; @override - String get weatherRankingTitle => '觀測排行'; + String get reportFilterIntensity => '震度'; @override - String weatherRankingMeta(String time, int count) { - return '資料時間:$time\n共 $count 觀測點'; - } + String get mapLayerLightning => '閃電'; @override - String get weatherRankingEmpty => '目前沒有可排序的觀測'; + String get restroomTypeMale => '男廁所'; @override - String get weatherRankingBy => '依'; + String get meshtasticLastReceived => '最近接收'; @override - String get weatherRankingHighest => '最高'; + String get reportDetailSortByCounty => '依縣市排序'; @override - String get weatherRankingLowest => '最低'; + String get homeRainTrendScattered => '可能會有零星降雨'; @override - String get weatherRankingMergeTo => '合併至'; + String get meshtasticUptime => '運行時間'; @override - String get weatherRankingMergeTown => '鄉鎮'; + String get weatherRankingTempExtremes => '溫度極值'; @override - String get weatherRankingMergeCounty => '縣市'; + String get themeLight => '淺色'; @override - String get weatherRankingWind => '風速'; + String get mapTerrainReliefHint => '在底圖上顯示立體地形陰影'; @override - String get weatherRankingGust => '陣風'; + String get meshtasticEmptyMessage => '(空白訊息)'; @override - String get weatherRankingTempExtremes => '溫度極值'; + String get moreSectionRegion => '地區'; @override - String get weatherRankingExtremeHigh => '今日最高'; + String get dpmDisasterEarthquake => '震災'; @override - String get weatherRankingExtremeLow => '今日最低'; + String get mapLayerSatellite => 'ひまわり 紅外線(B13)'; @override - String get weatherRankingExtremeRange => '日溫差'; + String get aedHoursSaturday => '週六開放時間'; @override - String weatherRankingRecordedAt(String time) { - return '記錄於 $time'; - } + String get dpmDisasterSlope => '坡地災害'; @override - String weatherRankingAnalysisCurrent(String value) { - return '當下 $value°C'; - } + String get moonPhaseNew => '新月'; @override - String weatherRankingAnalysisHigh(String value) { - return '最高 $value'; - } + String get notifySectionEew => '地震速報'; @override - String weatherRankingAnalysisLow(String value) { - return '最低 $value'; - } + String get mapResetNorth => '回到北方'; @override - String weatherRankingAnalysisRange(String value) { - return '溫差 $value°C'; - } + String get rainInterval2d => '2 日'; @override - String get reportListEmpty => '目前沒有地震報告'; + String get mapTownLabelsHint => '放大時顯示鄉鎮名稱'; @override - String get reportListEmptyFiltered => '沒有符合條件的地震報告'; + String get commonCancel => '取消'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth 公里'; - } + String get notifyOptTsunamiWarning => '只接收海嘯警報'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; @override - String get reportListDepthUnit => '公里'; + String get moreSectionAdvanced => '進階'; @override - String get reportListLocalFelt => '小區域有感'; + String get weatherRankingExtremeRange => '日溫差'; @override - String get reportListToday => '今天'; + String get notifySettingsMenu => '通知設定'; @override - String get reportListYesterday => '昨天'; + String get typhoonHistoryTitle => '資料時間'; @override - String reportListDayCount(int count) { - return '$count'; + String mapAppDefault(String app) { + return '$app(預設)'; } @override - String get reportListEnd => '已到最後一頁'; - - @override - String get reportFilterTitle => '篩選'; + String get trendRange24h => '24 小時'; @override - String get reportFilterSort => '排序方式'; + String get mapLayerStyleJmaTooltip => '灰階為底,−40 °C 以下上色,凸顯雲頂高度'; @override - String get reportFilterSortTime => '時間'; + String weatherRankingRecordedAt(String time) { + return '記錄於 $time'; + } @override - String get reportFilterSortIntensity => '震度'; + String get mapLayerRain => '雨量'; @override - String get reportFilterSortMagnitude => '規模'; + String get mapLayerQpesums => '未來 1 小時降水預報'; @override - String get reportFilterSortDepth => '深度'; + String get mapOverlaySectionMap => '地圖'; @override - String get reportFilterOrderDesc => '降序'; + String get mapTerrainRelief => '地形立體感'; @override - String get reportFilterOrderAsc => '升序'; + String get eewMaxIntensity => '最大震度'; @override - String get reportFilterIntensity => '震度'; + String get mapLegendCollapse => '收合圖例'; @override - String get reportFilterIntensityInfoTitle => '震度新制與舊制'; + String get changelogTitle => '更新日誌'; @override - String get reportFilterIntensityInfoIntro => - '中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。'; + String get reportFilterOrderDesc => '降序'; @override - String get reportFilterIntensityInfoLegacyTitle => '舊制(2020 以前)'; + String get meshtasticExcludeMqttSubtitle => '經網際網路橋接、並非無線電聽到的節點'; @override - String get reportFilterIntensityInfoLegacyBody => '震度僅 0–7,沒有 5弱/5強/6弱/6強。'; + String get reportFilterIntensityInfoTitle => '震度新制與舊制'; @override - String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; + String get mapLayerTyphoon => '颱風'; @override - String get reportFilterIntensityInfoModernBody => - '震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。'; + String get radarOverlayMenuTooltip => '雷達圖層選項'; @override - String get reportFilterMagnitude => '規模'; + String get mapMyLocation => '我的位置'; @override - String get reportFilterDepth => '深度'; + String get meshtasticNodes => '節點'; @override - String reportFilterDepthKm(String depth) { - return '$depth 公里'; - } + String get meshtasticSend => '傳送'; @override - String get reportFilterDate => '日期'; + String get typhoonOverlayStormL7Tooltip => '七級暴風圈+平均圓(紫色)'; @override - String get reportFilterDatePick => '選擇日期'; + String get aedType => '場所類型'; @override - String get reportFilterDateStartNote => '開始日:當日 00:00(臺北時間)'; + String get termsOfService => '服務條款'; @override - String get reportFilterDateEndNote => '結束日:當日 24:00(臺北時間)'; + String get typhoonLegendCircle25 => '十級風暴風圈'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get sponsorTitle => '支持 DPIP'; @override - String get reportFilterLocation => '地點'; + String get mapNavSatellite => '衛星'; @override - String get reportFilterLocationHint => '例如:花蓮、東部海域'; + String homeRainTrendUpdated(String time) { + return '更新 $time'; + } @override - String get reportFilterAny => '不限'; + String get onboardingNext => '下一步'; @override - String get reportFilterApply => '套用'; + String get weatherRankingMergeTown => '鄉鎮'; @override - String get reportFilterReset => '重設'; + String get mapLayerMonitor => '強震監視器'; @override - String get reportListSearch => '查詢'; + String get moreYoutube => 'YouTube'; @override - String get reportDetailTitle => '地震報告'; + String get sponsorSubscriptions => '訂閱制'; @override - String reportDetailNumbered(String number) { - return '編號 $number 顯著有感地震'; + String typhoonValueLon(String lon) { + return '東經 $lon 度'; } @override - String get reportDetailLocalFelt => '小區域有感地震'; + String get skyTime => '天空時間'; @override - String get reportDetailInfo => '詳細資訊'; + String get weatherModeCloudy => '多雲'; @override - String get reportDetailOriginTime => '發震時間'; + String get skyTimeDusk => '暮色'; @override - String get reportDetailEpicenter => '震央座標'; + String get meshtasticFirmware => '韌體'; @override - String get reportDetailMagnitude => '地震規模'; + String get reportFilterDateEndNote => '結束日:當日 24:00(臺北時間)'; @override - String get reportDetailDepth => '震源深度'; + String get reportFilterSortMagnitude => '規模'; @override - String get reportDetailAreaIntensity => '各地震度'; + String get meshtasticSilent => '已靜默'; @override - String get reportDetailLocalIntensity => '所在地的震度'; + String get mapLayerCategoryEarthquake => '地震'; @override - String get reportDetailLocalIntensityUnavailable => '沒有震度訊息'; + String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; @override - String get reportDetailSortByIntensity => '依震度排序'; + String get typhoonLegendPast => '實際路徑'; @override - String get reportDetailSortByCounty => '依縣市排序'; + String get restroomCategoryOther => '其他'; @override - String get reportDetailImage => '地震報告圖'; + String homeForecastHighLow(String high, String low) { + return '高 $high° · 低 $low°'; + } @override - String get reportDetailImageUnavailable => '報告圖尚未提供'; + String get locationBannerFix => '開啟設定'; @override - String get reportDetailOpenReport => '報告頁面'; + String get mapLegendExpand => '圖例'; @override - String get reportDetailReplay => '重播'; + String get eewNone => '目前沒有地震速報'; @override - String get navMore => '更多'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get appLogs => 'App 日誌'; + String get notifyOptTsunamiAll => '海嘯消息、海嘯警報'; @override - String get changelogTitle => '更新日誌'; + String get meshtasticLayerOptions => '節點選項'; @override - String get changelogEmpty => '目前沒有更新日誌'; + String get onboardingAgreeContinue => '同意並繼續'; @override - String get changelogTypePrerelease => '公測'; + String get commonRetry => '重試'; @override - String get changelogTypeStable => '正式'; + String get meshtasticNodeId => '節點 ID'; @override - String get changelogCurrentVersion => '目前版本'; + String reportDetailNumbered(String number) { + return '編號 $number 顯著有感地震'; + } @override - String get changelogVersionDetails => '版本資訊'; + String get typhoonOverlayStormBandSubtitle => '含平均圓'; @override - String get changelogBodyEmpty => '此版本沒有說明。'; + String get disasterMapOverlayRestroomTooltip => '顯示公廁'; @override - String get mapPlaceholderDisabled => '地圖(暫時停用)'; + String get weatherRankingTitle => '觀測排行'; @override - String get moreSectionRegion => '地區'; + String get homeRainTrendHeavySustained => '未來 1 小時會有持續大雨'; @override - String get moreSectionNotify => '通知'; + String get notifySectionTsunami => '海嘯'; @override - String get moreSectionDisplay => '顯示'; + String get restroomCategoryPark => '公園'; @override - String get regionManageTitle => '常用地區'; + String get moreLinkOpenFailed => '無法開啟連結'; @override - String get regionAddButton => '新增地區'; + String get themeDark => '深色'; @override - String get regionEmpty => '尚未新增常用地區'; + String get sponsorRestore => '恢復購買'; @override - String get regionSelectTitle => '選擇地區'; + String get meshtasticChannelWorking => '正在設定 DPIP 頻道…'; @override - String regionSelectCount(int count, int max) { - return '已選 $count/$max'; - } + String get meshtasticRegionSwitch => '切換為 TW'; @override - String regionSelectFull(int max) { - return '最多只能選擇 $max 個地區'; - } + String get meshtasticTraffic => '流量'; @override - String get regionEdit => '修改'; + String get mapLayerStyleBdTooltip => 'Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階'; @override - String get moreSectionAdvanced => '進階'; + String get disasterMapOverlayAedTooltip => '顯示 AED 位置'; @override - String get moreDeveloper => '除錯資訊'; + String get mapLayerHumidity => '濕度'; @override - String get experimentalFeatures => '實驗性功能'; + String get mapLayerSatelliteTransparentNight => '夜間 = 透明,顯示底圖'; @override - String get moreSectionLinks => '相關連結'; + String get meshtasticScanning => '掃描中…'; @override - String get moreCwaEew => '中央氣象署強震即時警報'; + String regionSelectFull(int max) { + return '最多只能選擇 $max 個地區'; + } @override - String get moreTremReport => 'TREM 檢知報告'; + String get meshtasticTitle => 'Meshtastic'; @override - String get moreServerStatus => '伺服器狀態'; + String get navMore => '更多'; @override - String get moreAnnouncements => '公告'; + String get meshtasticDpipChannel => 'DPIP 頻道'; @override - String get moreDiscord => 'Discord 社群'; + String get disasterMapOverlaySectionLayers => '圖層'; @override - String get moreNotifyLog => 'DPIP 通知發送記錄'; + String get mapLayerSatelliteB05 => 'ひまわり 近紅外(B05)'; @override - String get moreLinkOpenFailed => '無法開啟連結'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return '東北 $ne · 東南 $se · 西南 $sw · 西北 $nw km'; + } @override - String get weatherDynamicState => '天氣動態狀態'; + String get typhoonLabelNe => '東北側'; @override - String get weatherDynamicStateSubtitle => '覆寫主頁背景天氣'; + String get meshtasticCopied => '已複製訊息'; @override - String get weatherModeAuto => '自動'; + String get reportListEmpty => '目前沒有地震報告'; @override - String get weatherModeClear => '晴天'; + String get reportListEnd => '已到最後一頁'; @override - String get weatherModeRain => '雨天'; + String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; @override - String get weatherModeFog => '大霧'; + String get typhoonOverlaySectionExtra => '覆蓋層'; @override - String get weatherModeThunderstorm => '雷雨'; + String get eewSWave => '震波'; @override - String get commonLoading => '載入中…'; + String get meshtasticBusyTitle => '另一個 App 正在使用這台裝置'; @override - String get commonRetry => '重試'; + String get restroomCategoryCultural => '文化育樂活動場所'; @override - String get commonError => '發生錯誤'; + String get typhoonLabelWind => '近中心最大風速'; @override - String get commonFetchFailed => '無法獲取資料,請稍後重試'; + String get radarGlobalOutlineHint => '各國國界外框'; @override - String get commonEmpty => '沒有資料'; + String get notifyEvacuation => '防災資訊'; @override - String get feedConnecting => '連線中…'; + String get typhoonLegendCircle15 => '七級風暴風圈'; @override - String get feedStale => '資料可能已過期'; + String get dataSectionAstronomy => '天文'; @override - String get feedOffline => '連線中斷'; + String get homeRainTrendLightSustained => '未來 1 小時會有持續小雨'; @override - String get eewTitle => '地震速報'; + String get commonError => '發生錯誤'; @override - String get eewNone => '目前沒有地震速報'; + String get moonPhaseWaningCrescent => '殘月'; @override - String eewSummary(String magnitude, String depth) { - return '規模 $magnitude・深度 $depth 公里'; + String get meshtasticPower => '電力'; + + @override + String get mapTimelineNow => '現在'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; } @override - String get regionNationwide => '全國'; + String get reportDetailOpenReport => '報告頁面'; @override - String get regionCurrent => '所在地'; + String get trendRange7d => '7 天'; @override - String get regionCurrentUnavailable => '無法取得所在地位置資訊'; + String typhoonWarningAreas(String areas) { + return '警戒區域:$areas'; + } @override - String get weatherPrecipitation => '降水量'; + String get rainIntervalSection => '統計時間'; @override - String get weatherHumidity => '濕度'; + String get notifyTitle => '通知'; @override - String weatherDataTime(String station, String time) { - return '$station ∙ 資料時間 $time'; - } + String get meshtasticTxPower => '發射功率'; @override - String get homeViewOnMap => '前往地圖察看'; + String get restroomCategoryLabel => '類別'; @override - String get homeForecastTitle => '24小時預報'; + String get sponsorRestoring => '正在恢復購買…'; @override - String homeForecastHighLow(String high, String low) { - return '高 $high° · 低 $low°'; - } + String get sponsorIntro => + 'DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get shelterAddressLabel => '地址'; @override - String homeForecastFeelsLike(String temp) { - return '體感 $temp°'; - } + String get typhoonLabelStormAvg => '十級風平均暴風半徑'; @override - String homeForecastHumidity(String value) { - return '濕度 $value%'; - } + String get restroomCategoryCommercial => '商業營業場所'; @override - String homeForecastWind(String direction, String level) { - return '$direction · $level 級'; - } + String get aedRegion => '縣市區域'; @override - String get homeForecastUnavailable => '選擇鄉鎮後可查看預報'; + String homeRainTrendLightStopping(int minutes) { + return '預計 $minutes 分鐘後停止下小雨'; + } @override - String get homeForecastEmpty => '目前沒有預報資料'; + String get reportDetailInfo => '詳細資訊'; @override - String get homeActiveEventsTitle => '生效中事件'; + String get mapNavWind => '風向'; @override - String get homeActiveEventsEmpty => '目前沒有生效中的事件'; + String get windForecastOverlayMenuTooltip => '風場預報圖層選項'; @override - String get homeRainTrendTitle => '近 1 小時降水趨勢'; + String get dataWeatherRankingSubtitle => '即時觀測排行'; @override String homeRainTrendMinute(int minute) { @@ -517,6645 +507,9852 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String homeRainTrendUpdated(String time) { - return '更新 $time'; - } + String get rainInterval6h => '6 時'; @override - String get homeRainTrendNoData => '無資料'; + String get restroomTypeUnspecified => '未設定'; @override - String get homeRainTrendScattered => '可能會有零星降雨'; + String get typhoonOverlayProbabilityHint => '會隱藏預測圓錐'; @override - String get homeRainTrendLightSustained => '未來 1 小時會有持續小雨'; + String get mapLayerSatelliteGlobalOutline => '國界'; @override - String homeRainTrendLightStopping(int minutes) { - return '預計 $minutes 分鐘後停止下小雨'; - } + String get mapNavTemperature => '溫度'; @override - String get homeRainTrendHeavySustained => '未來 1 小時會有持續大雨'; + String get typhoonLegendForecastPoint => '預測點'; @override - String homeRainTrendHeavyStopping(int minutes) { - return '預計 $minutes 分鐘後停止下大雨'; - } + String get reportListYesterday => '昨天'; @override - String get mapLayers => '圖層'; + String get moreSectionLinks => '相關連結'; @override - String get mapLayerOrderTitle => '調整圖層順序'; + String get feedOffline => '連線中斷'; @override - String get mapLayerOrderReset => '回復預設順序'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get mapLayerRadar => '雷達合成回波圖'; + String get moreSectionDisplay => '顯示'; @override - String get mapLayerSatellite => 'ひまわり 紅外線(B13)'; + String get rainInterval3d => '3 日'; @override - String get mapLayerSatelliteB01 => 'ひまわり 可見光-藍(B01)'; + String get defaultMapLayerSubtitle => '開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。'; @override - String get mapLayerSatelliteB02 => 'ひまわり 可見光-綠(B02)'; + String get aedDescription => '備註'; @override - String get mapLayerSatelliteB03 => 'ひまわり 可見光-紅(B03)'; + String get typhoonOverlayWeatherRadarTooltip => '雷達回波(對齊颱風報文時間)'; @override - String get mapLayerSatelliteB04 => 'ひまわり 近紅外(B04)'; + String get onboardingPermLocationDesc => '依你所在位置推送在地警報。'; @override - String get mapLayerSatelliteB05 => 'ひまわり 近紅外(B05)'; + String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; @override - String get mapLayerSatelliteB06 => 'ひまわり 近紅外(B06)'; + String get homeActiveEventsEmpty => '目前沒有生效中的事件'; @override - String get mapLayerSatelliteB07 => 'ひまわり 短波紅外(B07)'; + String get typhoonLabelPosition => '中心位置'; @override - String get mapLayerSatelliteB08 => 'ひまわり 上層水氣(B08)'; + String get weatherRankingBy => '依'; @override - String get mapLayerSatelliteB09 => 'ひまわり 中層水氣(B09)'; + String get typhoonIntensityMild => '輕度颱風'; @override - String get mapLayerSatelliteB10 => 'ひまわり 低層水氣(B10)'; + String get windForecastGlobalOutlineHint => '各國國界外框'; @override - String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/雲相(B11)'; + String get rainInterval1h => '1 時'; @override - String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; + String get eewLocalIntensity => '所在地預估'; @override - String get mapLayerSatelliteB13 => 'ひまわり 紅外線(B13)'; + String get mapLayerRadar => '雷達合成回波圖'; @override - String get mapLayerSatelliteB14 => 'ひまわり 長波紅外線(B14)'; + String get restroomCategoryReligious => '宗教禮儀場所'; @override - String get mapLayerSatelliteB15 => 'ひまわり 長波紅外線(B15)'; + String get meshtasticRole => '角色'; @override - String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; + String get mapLayerSatelliteCloudCloudy => '有雲'; @override - String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; + String get skyTimeSunrise => '日出'; @override - String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; + String get meshtasticNoMessages => '尚無訊息'; @override - String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + String get onboardingPermNotifyDesc => '在地震、天氣與災害發生時,即時傳遞警報通知。'; @override - String get mapLayerSatelliteDust => 'ひまわり 沙塵'; + String get radarTownOutline => '鄉鎮界線'; @override - String get mapLayerSatelliteAirmass => 'ひまわり 氣團'; + String get mapLayerStyleSection => '顯示樣式'; @override - String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; + String get disasterMapOverlayMenuTooltip => '防災地圖圖層'; @override - String get mapLayerSatelliteWatervapor => 'ひまわり 水氣'; + String get moreGooglePlay => 'Google Play'; @override - String get mapLayerSatelliteBtdSplit => 'ひまわり 分割視窗'; + String get meshtasticOnline => '近期聽到'; @override - String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; + String get typhoonLabelSw => '西南側'; @override - String get mapLayerSatelliteBtdWvirw => 'ひまわり 過衝雲頂'; + String typhoonForecastLead(String hours) { + return '預測 +$hours 小時'; + } @override - String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/雲相'; + String get dpmDisasterTsunami => '海嘯'; @override - String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷雲/雲高'; + String get changelogTypeStable => '正式'; @override - String get mapLayerSatelliteBtdOzone => 'ひまわり 對流層頂'; + String get mapLayerSatelliteTransparentClear => '晴空 = 透明,顯示底圖'; @override - String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂溫度'; + String get mapOverlaySectionReference => '參考圖層'; @override - String get mapLayerSatelliteCloudmask => 'ひまわり 雲遮罩'; + String get mapLayerSatelliteB02 => 'ひまわり 可見光-綠(B02)'; @override - String get mapLayerSatelliteSst => 'ひまわり 海表溫度'; + String get reportListLocalFelt => '小區域有感'; @override - String get mapLayerSatelliteNdvi => 'ひまわり 植生指數'; + String get weatherRankingEmpty => '目前沒有可排序的觀測'; @override - String get mapLayerSatelliteNdwi => 'ひまわり 水體指數'; + String get notifySectionOther => '其他'; @override - String get mapLayerSatelliteMndwi => 'ひまわり 改良水體指數'; + String weatherRankingMeta(String time, int count) { + return '資料時間:$time\n共 $count 觀測點'; + } @override - String get mapLayerSatelliteGlobalOutline => '國界'; + String get onboardingTermsAgree => '我已閱讀並同意服務條款'; @override - String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; + String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(無植被)'; @override - String get mapLayerSatelliteCloudClear => '晴空'; + String get notifyOptLocalIntensity4 => '所在地震度4以上'; @override - String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; + String get eewArrived => '已抵達'; @override - String get mapLayerSatelliteCloudProbablyCloudy => '可能有雲'; + String get meshtasticNoDevices => '找不到 Meshtastic 裝置'; @override - String get mapLayerSatelliteCloudCloudy => '有雲'; + String get mapLayerCategoryLife => '生活'; @override - String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,顯示底圖'; + String get reportFilterSortIntensity => '震度'; @override - String get mapLayerSatelliteTransparentReflectance => '低反射率/夜間 = 透明,顯示底圖'; + String get typhoonMotion => '移動'; @override - String get mapLayerSatelliteTransparentZero => '零差值 = 透明(無訊號)'; + String get meshtasticStateDisconnected => '未連線'; @override - String get mapLayerSatelliteTransparentNight => '夜間 = 透明,顯示底圖'; + String get typhoonIntensityIntense => '強烈颱風'; @override - String get mapLayerSatelliteTransparentNoData => '無資料(陸地) = 透明'; + String get mapLayerOrderTitle => '調整圖層順序'; @override - String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(無植被)'; + String get dpmYes => '是'; @override - String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(無水體)'; + String get meshtasticNoHistory => '歷史紀錄還不夠'; @override - String get mapLayerSatelliteTransparentClear => '晴空 = 透明,顯示底圖'; + String get reportDetailLocalIntensityUnavailable => '沒有震度訊息'; @override - String get mapLayerStyleSection => '顯示樣式'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get mapLayerStyleTooltip => '顯示樣式'; + String get reportListDepthUnit => '公里'; @override - String get mapLayerStyleGray => '灰階(JMA)'; + String get reportFilterDepth => '深度'; @override - String get mapLayerStyleGrayTooltip => '氣象廳灰階慣例:溫度越低越白'; + String get onboardingScrollHint => '往下捲動以繼續'; @override - String get mapLayerStyleJma => '雲頂強調(JMA)'; + String get mapNavQpesums => '預報'; @override - String get mapLayerStyleJmaTooltip => '灰階為底,−40 °C 以下上色,凸顯雲頂高度'; + String get navMap => '地圖'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get notifyAdvisory => '天氣警特報'; @override - String get mapLayerStyleBdTooltip => 'Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階'; + String get reportFilterReset => '重設'; @override - String get mapLayerQpesums => '未來 1 小時降水預報'; + String get mapLayerSatelliteMndwi => 'ひまわり 改良水體指數'; @override - String get mapLayerLightning => '閃電'; + String get typhoonOverlaySectionStorm => '暴風圈'; @override - String lightningLegendCg(int minutes) { - return '對地 · $minutes 分內'; - } + String get moonPhaseFull => '滿月'; @override - String lightningLegendCc(int minutes) { - return '雲間 · $minutes 分內'; - } + String get moonPhaseWaningGibbous => '虧凸月'; @override - String get mapTimelineNow => '現在'; + String get weatherDynamicStateSubtitle => '覆寫主頁背景天氣'; @override - String get mapTimelinePast => '歷史'; + String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; @override - String get mapTimelineFuture => '未來'; + String typhoonDataTime(String time) { + return '資料時間\n$time'; + } @override - String get mapTimelineObserved => '觀測'; + String get restroomTypeAccessible => '無障礙廁所'; @override - String get mapTimelineForecast => '預報'; + String get moreSectionAbout => '關於'; @override - String mapTimelineDataTime(String time) { - return '資料時間 $time'; - } + String get meshtasticSelectDevice => '選擇裝置'; @override - String get notifySettingsMenu => '通知設定'; + String get onboardingIntroBody => + 'DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷雨即時訊息、天氣警特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。'; @override - String get notifyTitle => '通知'; + String get shelterCapacityLabel => '收容人數'; @override - String get notifyUnavailable => '推播尚未就緒,請稍後再試。'; + String get reportDetailImage => '地震報告圖'; @override - String get notifySetFailed => '設定失敗,請稍後再試。'; + String get meshtasticStateConfiguring => '設定中…'; @override - String get notifySectionEew => '地震速報'; + String get typhoonLabelGaleAvg => '七級風平均暴風半徑'; @override - String get notifySectionEarthquake => '地震'; + String get onboardingPermNotify => '通知'; @override - String get notifySectionWeather => '天氣'; + String get meshtasticClearMessages => '清除訊息'; @override - String get notifySectionTsunami => '海嘯'; + String get meshtasticNotifyMessages => '新訊息通知'; @override - String get notifySectionOther => '其他'; + String get defaultMapLayerSettings => '地圖預設圖層'; @override - String get notifyEew => '緊急地震速報'; + String get moreSectionNotify => '通知'; @override - String get notifyMonitor => '強震監視器'; + String get notifyUnavailable => '推播尚未就緒,請稍後再試。'; @override - String get notifyReport => '地震報告'; + String get mapLayerOrderReset => '回復預設順序'; @override - String get notifyIntensity => '震度速報'; + String get dpmAddress => '地址'; @override - String get notifyThunderstorm => '雷雨即時訊息'; + String get weatherRankingMergeCounty => '縣市'; @override - String get notifyAdvisory => '天氣警特報'; + String get moreSectionApp => '取得 App'; @override - String get notifyEvacuation => '防災資訊'; + String get reportFilterIntensityInfoLegacyBody => '震度僅 0–7,沒有 5弱/5強/6弱/6強。'; @override - String get notifyTsunami => '海嘯資訊'; + String get mapLayerSatelliteSst => 'ひまわり 海表溫度'; @override - String get notifyAnnouncement => '公告'; + String get qpesumsOverlayMenuTooltip => '定量降水預報圖層選項'; @override - String get notifyOptOff => '關閉'; + String get mapTimelineFuture => '未來'; @override - String get notifyOptAll => '接收全部'; + String get typhoonLegendCircleAvg => '平均圓'; @override - String get notifyOptLocalIntensity4 => '所在地震度4以上'; + String reportFilterDepthKm(String depth) { + return '$depth 公里'; + } @override - String get notifyOptLocalIntensity1 => '所在地震度1以上'; + String get typhoonLabelSe => '東南側'; @override - String get notifyOptWeatherLocal => '接收所在地'; + String get radarTownOutlineHint => '較細的分區'; @override - String get notifyOptTsunamiWarning => '只接收海嘯警報'; + String eewCountdown(int seconds) { + return '$seconds 秒'; + } @override - String get notifyOptTsunamiAll => '海嘯消息、海嘯警報'; + String get typhoonLabelGust => '瞬間最大陣風'; @override - String get onboardingNext => '下一步'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get onboardingBack => '上一步'; + String get sponsorTerms => '使用條款'; @override - String get onboardingScrollHint => '往下捲動以繼續'; + String get restroomTypeGenderNeutral => '性別友善廁所'; @override - String get onboardingIntroTitle => '歡迎使用 DPIP'; + String get notifyThunderstorm => '雷雨即時訊息'; @override - String get onboardingIntroBody => - 'DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷雨即時訊息、天氣警特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。'; + String get skyTimeGolden => '黃金時刻'; @override - String get onboardingTermsTitle => '服務條款'; + String get moonAge => '月齡'; @override - String get onboardingTermsBody => - '使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get onboardingTermsAgree => '我已閱讀並同意服務條款'; + String weatherRankingAnalysisCurrent(String value) { + return '當下 $value°C'; + } @override - String get onboardingAgreeContinue => '同意並繼續'; + String get moreGithub => 'ExpTech GitHub'; @override - String get onboardingPermsTitle => '權限授權'; + String get homeForecastUnavailable => '選擇鄉鎮後可查看預報'; @override - String get onboardingPermsBody => '為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。'; + String get mapLayers => '圖層'; @override - String get onboardingPermNotify => '通知'; + String get meshtasticHardware => '硬體'; @override - String get onboardingPermNotifyDesc => '在地震、天氣與災害發生時,即時傳遞警報通知。'; + String get languageSettings => '語言設定'; @override - String get onboardingPermCritical => '重大通知'; + String get dpmDisasterNuclear => '核子事故'; @override - String get onboardingPermCriticalDesc => '讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。'; + String get language => '語言'; @override - String get onboardingPermLocation => '定位'; + String homeForecastFeelsLike(String temp) { + return '體感 $temp°'; + } @override - String get onboardingPermLocationDesc => '依你所在位置推送在地警報。'; + String get typhoonOverlayWeatherHint => '對齊報文時間'; @override - String get onboardingPermBackground => '背景定位'; + String get skyTimeDawn => '黎明'; @override - String get onboardingPermBackgroundDesc => '選擇「一律允許」,關閉 App 也能推送在地警報。'; + String get skyTimeAfternoon => '下午'; @override - String get onboardingPermBattery => '省電白名單'; + String get meshtasticLastHeard => '最後聽到'; @override - String get onboardingPermBatteryDesc => '允許 DPIP 在背景持續運作,避免警報延遲或漏收。'; + String get typhoonWarningTitle => '颱風警報'; @override - String get onboardingGrant => '授權'; + String get moreSourceCode => '原始碼'; @override - String get onboardingGranted => '已授權'; + String get mapLayerCategoryWeather => '氣象觀測'; @override - String get onboardingStart => '開始使用'; + String get mapLayerSatelliteB09 => 'ひまわり 中層水氣(B09)'; @override - String get language => '語言'; + String get windForecastTownOutlineHint => '更細的網格'; @override - String get languageSettings => '語言設定'; + String get mapLayerSatelliteCloudmask => 'ひまわり 雲遮罩'; @override - String get languageSystem => '系統預設'; + String get mapAppCopyCoordinates => '複製座標'; @override - String get locationBannerServiceOff => '定位服務已關閉,無法針對你的所在地推送警報。'; + String get reportFilterIntensityInfoIntro => + '中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。'; @override - String get locationBannerPermission => '尚未授權定位,無法針對你的所在地推送警報。'; + String get mapNavEarthquake => '地震'; @override - String get locationBannerFix => '開啟設定'; + String get typhoonGust => '陣風'; @override - String get notifyBannerDisabled => '通知已關閉,將收不到災害警報。'; + String get restroomGradeAverage => '普通級'; @override - String get onboardingSkipTitle => '尚未完成授權'; + String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷雲/雲高'; @override - String get onboardingSkipBody => - '未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。'; + String get onboardingPermBackgroundDesc => '選擇「一律允許」,關閉 App 也能推送在地警報。'; @override - String get onboardingSkipStay => '返回授權'; + String get mapTimelineForecast => '預報'; @override - String get onboardingSkipLeave => '仍要略過'; + String get restroomTypeLabel => '廁所類型'; @override - String get moreYoutube => 'YouTube'; + String get navEarthquake => '地震'; @override - String get moreGithub => 'ExpTech GitHub'; + String get typhoonOverlayStormL10Tooltip => '十級暴風圈+平均圓(黃色)'; @override - String get moreSourceCode => '原始碼'; + String get moonPhaseWaxingGibbous => '盈凸月'; @override - String get moreSectionApp => '取得 App'; + String get reportDetailTitle => '地震報告'; @override - String get moreGooglePlay => 'Google Play'; + String get moreTremReport => 'TREM 檢知報告'; @override - String get moreAppStore => 'App Store'; + String weatherDataTime(String station, String time) { + return '$station ∙ 資料時間 $time'; + } @override - String get displaySettings => '顯示設定'; + String get meshtasticNoNodes => '尚未聽到任何節點'; @override - String get defaultMapLayerSettings => '地圖預設圖層'; + String get meshtasticViaMqtt => '經 MQTT(網際網路)'; @override - String get defaultMapLayerSubtitle => '開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。'; + String get radarCountyOutline => '縣市界線'; @override - String get mapNavRadar => '雷達'; + String get onboardingGranted => '已授權'; @override - String get mapNavQpesums => '預報'; + String get commonClose => '關閉'; @override - String get mapNavSatellite => '衛星'; + String get restroomGradeLabel => '等級'; @override - String get mapNavLightning => '閃電'; + String get rainIntervalNow => '今日'; @override - String get mapNavTyphoon => '颱風'; + String get changelogCurrentVersion => '目前版本'; @override - String get mapNavEarthquake => '地震'; + String get typhoonLabelPressure => '中心氣壓'; @override - String get mapNavTemperature => '溫度'; + String get typhoonOverlayForecastCalloutsTooltip => '放大時顯示預測點詳細卡片'; @override - String get mapNavHumidity => '濕度'; + String get aedOpenRemark => '開放時間備註'; @override - String get mapNavPressure => '氣壓'; + String get onboardingPermsBody => '為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。'; @override - String get mapNavWind => '風向'; + String get typhoonOverlaySectionWeather => '天氣底圖'; @override - String get mapNavRain => '雨量'; + String get notifyOptWeatherLocal => '接收所在地'; @override - String get mapNavDisaster => '防災'; + String get mapNavRain => '雨量'; @override - String get displayTheme => '主題'; + String get moonDays => '天'; @override - String get themeSystem => '跟隨系統'; + String mapLegendUnit(String unit) { + return '單位:$unit'; + } @override - String get themeLight => '淺色'; + String get weatherModeClear => '晴天'; @override - String get themeDark => '深色'; + String get meshtasticRadio => '電台'; @override - String get moreSectionAbout => '關於'; + String get commonEmpty => '沒有資料'; @override - String get termsOfService => '服務條款'; + String get mapLayerSatelliteB01 => 'ひまわり 可見光-藍(B01)'; @override - String get faq => '常見問題'; + String get meshtasticExternalPower => '外部供電'; @override - String get openSourceLicenses => '引用套件'; + String get moonPhaseLastQuarter => '下弦月'; @override - String get sponsorTitle => '支持 DPIP'; + String get reportFilterOrderAsc => '升序'; @override - String get sponsorIntro => - 'DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。'; + String get reportFilterApply => '套用'; @override - String get sponsorSubscriptions => '訂閱制'; + String get reportDetailImageUnavailable => '報告圖尚未提供'; @override - String get sponsorRecommended => '推薦'; + String get weatherRankingHighest => '最高'; @override - String get sponsorOneTime => '單次支援'; + String get reportDetailReplay => '重播'; @override - String sponsorPerMonth(String price) { - return '$price / 月'; - } + String get mapLayerRestroom => '公廁'; @override - String get sponsorRestore => '恢復購買'; + String get restroomCategoryWelfare => '社福機構、集會場所'; @override - String get sponsorTerms => '使用條款'; + String get restroomGradeExcellent => '特優級'; @override - String get sponsorPrivacy => '隱私權政策'; + String get meshtasticLastSent => '最近送出'; @override - String get sponsorRestoring => '正在恢復購買…'; + String get meshtasticName => '名稱'; @override - String get sponsorRestoreUnavailable => '無法連線至商店,請稍後再試'; + String get meshtasticScan => '掃描'; @override - String get commonClose => '關閉'; + String get mapLayerCategoryForecast => '數值預報'; @override - String get mapLayerTemperature => '溫度'; + String get meshtasticChannelFailed => '無法設定 DPIP 頻道'; @override - String get trendRange24h => '24 小時'; + String get themeSystem => '跟隨系統'; @override - String get trendRange7d => '7 天'; + String get mapLayerSatelliteNdvi => 'ひまわり 植生指數'; @override - String get trendNoData => '沒有趨勢資料'; + String get typhoonLegendForecast => '預測路徑'; @override - String trendCumulativeTotal(String total) { - return '累計 $total mm'; + String typhoonValueHpa(String n) { + return '$n 百帕'; } @override - String chartHourLabel(int hour) { - return '$hour時'; - } + String get weatherPrecipitation => '降水量'; @override - String get mapLayerHumidity => '濕度'; + String get moonNextFullMoon => '下次滿月'; @override - String get mapLayerPressure => '氣壓'; + String get dpmSheetEmpty => '點選地圖上的標記查看詳情'; @override - String get mapLayerWind => '風向'; + String get onboardingSkipLeave => '仍要略過'; @override - String get mapLayerRain => '雨量'; + String get onboardingBack => '上一步'; @override - String get rainIntervalMenu => '累積時段'; + String get aedPlaceDesc => '放置位置說明'; @override - String get rainIntervalNow => '今日'; + String get onboardingSkipTitle => '尚未完成授權'; @override - String get rainInterval10m => '10 分'; + String get restroomTypeFamily => '親子廁所'; @override - String get rainInterval1h => '1 時'; + String typhoonValueKm(String n) { + return '$n 公里'; + } @override - String get rainInterval3h => '3 時'; + String get typhoonPressure => '氣壓'; @override - String get rainInterval6h => '6 時'; + String get onboardingPermBattery => '省電白名單'; @override - String get rainInterval12h => '12 時'; + String get typhoonLabelNw => '西北側'; @override - String get rainInterval24h => '24 時'; + String get dpmDisasterFlood => '水災'; @override - String get rainInterval2d => '2 日'; + String get moonPhaseWaxingCrescent => '眉月'; @override - String get rainInterval3d => '3 日'; + String get restroomCategoryLeisure => '休閒娛樂場所'; @override - String get mapLayerTyphoon => '颱風'; + String get mapLayerTemperature => '溫度'; @override - String get typhoonNoActive => '目前無颱風'; + String get aedCategory => '場所分類'; @override - String get typhoonWind => '風速'; + String get meshtasticChannels => '頻道'; @override - String get typhoonGust => '陣風'; + String get monitorWaiting => '等待資料…'; @override - String get typhoonPressure => '氣壓'; + String get typhoonOverlayForecastCallouts => '預測點資訊'; @override - String get typhoonMotion => '移動'; + String get reportDetailEpicenter => '震央座標'; @override - String get typhoonLabelPosition => '中心位置'; + String get meshtasticVoltage => '電壓'; @override - String get typhoonLabelDirection => '過去移動方向'; + String get mapLayerMeshtasticSubtitle => '電台聽到過的 LoRa 網狀網路節點'; @override - String get typhoonLabelSpeed => '過去移動時速'; + String get mapLayerWind => '風向'; @override - String get typhoonLabelPressure => '中心氣壓'; + String get reportDetailMagnitude => '地震規模'; @override - String get typhoonLabelWind => '近中心最大風速'; + String get reportDetailAreaIntensity => '各地震度'; @override - String get typhoonLabelGust => '瞬間最大陣風'; + String get rainInterval12h => '12 時'; @override - String get typhoonLabelGaleAvg => '七級風平均暴風半徑'; + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } @override - String get typhoonLabelStormAvg => '十級風平均暴風半徑'; + String get dpmDisasterLandslide => '土石流'; @override - String get typhoonLabelProbCircle => '70%機率圓'; + String get notifyMonitor => '強震監視器'; @override - String typhoonForecastLead(String hours) { - return '預測 +$hours 小時'; - } + String get onboardingStart => '開始使用'; @override - String get typhoonLabelNw => '西北側'; + String sponsorPerMonth(String price) { + return '$price / 月'; + } @override - String get typhoonLabelNe => '東北側'; + String get mapLayerPressure => '氣壓'; @override - String get typhoonLabelSw => '西南側'; + String get mapLayerSatelliteB04 => 'ひまわり 近紅外(B04)'; @override - String get typhoonLabelSe => '東南側'; + String get mapLayerSatelliteTransparentZero => '零差值 = 透明(無訊號)'; @override - String typhoonValueLat(String lat) { - return '北緯 $lat 度'; - } + String get shelterIndoorLabel => '室內收容'; @override - String typhoonValueLon(String lon) { - return '東經 $lon 度'; - } + String get notifyOptOff => '關閉'; @override - String typhoonValueKm(String n) { - return '$n 公里'; - } + String get reportFilterSortTime => '時間'; @override - String typhoonValueHpa(String n) { - return '$n 百帕'; - } + String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; @override - String typhoonValueMs(String n) { - return '每秒 $n 公尺'; - } + String get weatherModeThunderstorm => '雷雨'; @override - String typhoonDataTime(String time) { - return '資料時間\n$time'; - } + String get homeViewOnMap => '前往地圖察看'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get reportFilterIntensityInfoLegacyTitle => '舊制(2020 以前)'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get typhoonLabelSpeed => '過去移動時速'; @override - String get mapLayerMonitor => '強震監視器'; + String mapAppOpenFailed(String app) { + return '無法開啟 $app'; + } @override - String get mapLayerDisasterMap => '防災地圖'; + String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; @override - String get mapLayerAed => 'AED'; + String get meshtasticReceived => '已接收'; @override - String get disasterMapOverlayMenuTooltip => '防災地圖圖層'; + String get weatherRankingExtremeLow => '今日最低'; @override - String get disasterMapOverlaySectionLayers => '圖層'; + String get mapLayerSatelliteB10 => 'ひまわり 低層水氣(B10)'; @override - String get disasterMapOverlayAedTooltip => '顯示 AED 位置'; + String get mapLayerSatelliteCloudProbablyCloudy => '可能有雲'; @override - String get aedAddress => '地址'; + String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(無水體)'; @override - String get aedRegion => '縣市區域'; + String get shelterCategoryLabel => '適用災害'; @override - String get aedCategory => '場所分類'; + String get meshtasticStateConnecting => '連線中…'; @override - String get aedType => '場所類型'; + String get moonTitle => '月亮'; @override - String get aedPlaceDesc => '放置位置說明'; + String get weatherRankingGust => '陣風'; @override - String get aedDescription => '備註'; + String get moreAppStore => 'App Store'; @override - String get aedHoursWeekday => '平日開放時間'; + String get dpmFilterSectionShelter => '避難所災害類型'; @override - String get aedHoursSaturday => '週六開放時間'; + String get moreServerStatus => '伺服器狀態'; @override - String get aedHoursSunday => '週日開放時間'; + String get notifySectionWeather => '天氣'; @override - String get aedOpenRemark => '開放時間備註'; + String get meshtasticPreset => '調變預設'; @override - String get aedEmergencyPhone => '緊急聯絡電話'; + String get dataSectionSeismic => '地震'; @override - String get mapLayerRestroom => '公廁'; + String get changelogBodyEmpty => '此版本沒有說明。'; @override - String get mapLayerShelter => '避難收容場所'; + String get radarGlobalOutline => '國界'; @override - String get disasterMapOverlayRestroomTooltip => '顯示公廁'; + String get notifyEew => '緊急地震速報'; @override - String get disasterMapOverlayShelterTooltip => '顯示避難收容場所'; + String get regionNationwide => '全國'; @override - String get dpmOpenInMaps => '開啟地圖'; + String get moreNotifyLog => 'DPIP 通知發送記錄'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get regionCurrent => '所在地'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get dpmFilterSectionRestroom => '場所類型'; @override - String mapAppDefault(String app) { - return '$app(預設)'; - } + String get meshtasticNotConnected => '尚未連線至裝置'; @override - String get mapAppCopyCoordinates => '複製座標'; + String get weatherModeSnow => '下雪'; @override - String get mapAppCoordinatesCopied => '已複製座標'; + String get mapLayerMeshtastic => 'Meshtastic 節點'; @override - String mapAppOpenFailed(String app) { - return '無法開啟 $app'; - } + String get moreDeveloper => '除錯資訊'; @override - String get mapAppCallFailed => '此裝置無法撥打電話'; + String get mapLayerSatelliteB14 => 'ひまわり 長波紅外線(B14)'; @override - String get mapOverlaySectionReference => '參考圖層'; + String get meshtasticChannelUse => '頻道使用率'; @override - String get mapLayerCategoryEarthquake => '地震'; + String get mapNavLightning => '閃電'; @override - String get mapLayerCategoryTyphoon => '颱風'; + String get homeForecastEmpty => '目前沒有預報資料'; @override - String get mapLayerCategoryWeather => '氣象觀測'; + String get sponsorOneTime => '單次支援'; @override - String get mapLayerCategorySatellite => '衛星'; + String get mapLayerSatelliteBtdSplit => 'ひまわり 分割視窗'; @override - String get mapLayerCategoryRadar => '雷達'; + String get onboardingPermBackground => '背景定位'; @override - String get mapLayerCategoryLife => '生活'; + String get aedEmergencyPhone => '緊急聯絡電話'; @override - String get mapLayerCategoryForecast => '數值預報'; + String get dpmOpenInMaps => '開啟地圖'; @override - String get mapOverlaySectionMap => '地圖'; + String get meshtasticNotifyNodes => '新節點通知'; @override - String get rainIntervalSection => '統計時間'; + String get onboardingPermCriticalDesc => '讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。'; @override - String get mapTownLabels => '鄉鎮名稱'; + String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,顯示底圖'; @override - String get mapTownLabelsHint => '放大時顯示鄉鎮名稱'; + String get meshtasticSent => '已送出'; @override - String get mapTerrainRelief => '地形立體感'; + String get homeForecastTitle => '24小時預報'; @override - String get mapTerrainReliefHint => '在底圖上顯示立體地形陰影'; + String get typhoonLegendWarningAreas => '警報區域'; @override - String get dpmSheetEmpty => '點選地圖上的標記查看詳情'; + String meshtasticExcludeMqttHidden(int count) { + return '已隱藏 $count 個'; + } @override - String get dpmAddress => '地址'; + String get notifyOptLocalIntensity1 => '所在地震度1以上'; @override - String get restroomTypeLabel => '廁所類型'; + String get mapTimelinePast => '歷史'; @override - String get restroomCategoryLabel => '類別'; + String get restroomTypeFemale => '女廁所'; @override - String get restroomGradeLabel => '等級'; + String get reportListToday => '今天'; @override - String get restroomTypeFemale => '女廁所'; + String get meshtasticTapNode => '點選節點查看詳細資訊'; @override - String get restroomTypeMale => '男廁所'; + String get commonLoading => '載入中…'; @override - String get restroomTypeMixed => '混合廁所'; + String get typhoonIntensityModerate => '中度颱風'; @override - String get restroomTypeAccessible => '無障礙廁所'; + String get typhoonWind => '風速'; @override - String get restroomTypeGenderNeutral => '性別友善廁所'; + String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; @override - String get restroomTypeFamily => '親子廁所'; + String get rainInterval3h => '3 時'; @override - String get restroomTypeUnspecified => '未設定'; + String get reportListSearch => '查詢'; @override - String get restroomCategoryTransport => '交通'; + String get mapLayerCategorySatellite => '衛星'; @override - String get restroomCategoryPark => '公園'; + String get meshtasticChannelReady => 'DPIP 頻道已就緒'; @override - String get restroomCategoryCommercial => '商業營業場所'; + String get reportFilterLocation => '地點'; @override - String get restroomCategoryReligious => '宗教禮儀場所'; + String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; @override - String get restroomCategoryCultural => '文化育樂活動場所'; + String get typhoonIntensityTd => '熱帶性低氣壓'; @override - String get restroomCategoryGovernment => '民眾洽公場所'; + String get reportFilterDate => '日期'; @override - String get restroomCategoryWelfare => '社福機構、集會場所'; + String get sponsorRestoreUnavailable => '無法連線至商店,請稍後再試'; @override - String get restroomCategoryTourist => '觀光地區及風景區'; + String homeForecastPop(String pop) { + return '$pop%'; + } @override - String get restroomCategoryLeisure => '休閒娛樂場所'; + String get regionEmpty => '尚未新增常用地區'; @override - String get restroomCategoryOther => '其他'; + String get onboardingPermBatteryDesc => '允許 DPIP 在背景持續運作,避免警報延遲或漏收。'; @override - String get restroomGradeExcellent => '特優級'; + String get mapNavDisaster => '防災'; @override - String get restroomGradeGood => '優等級'; + String get radarScanRangeSubtitle => '標示四座雷達實際觀測到的範圍。'; @override - String get restroomGradeAverage => '普通級'; + String get aedHoursSunday => '週日開放時間'; @override - String get restroomGradePoor => '不合格'; + String get reportDetailOriginTime => '發震時間'; @override - String get shelterAddressLabel => '地址'; + String get trendNoData => '沒有趨勢資料'; @override - String get shelterCapacityLabel => '收容人數'; + String get onboardingPermLocation => '定位'; @override - String shelterCapacityValue(int n) { - return '$n 人'; - } + String get moreDiscord => 'Discord 社群'; @override - String get shelterCategoryLabel => '適用災害'; + String get mapNavPressure => '氣壓'; @override - String get shelterIndoorLabel => '室內收容'; + String get mapLayerSatelliteB13 => 'ひまわり 紅外線(B13)'; @override - String get shelterOutdoorLabel => '室外收容'; + String typhoonTdNo(String no) { + return 'TD $no'; + } @override - String get shelterVulnerableOkLabel => '適合避難弱者安置'; + String get changelogEmpty => '目前沒有更新日誌'; @override - String get dpmYes => '是'; + String get reportFilterDateStartNote => '開始日:當日 00:00(臺北時間)'; @override - String get dpmNo => '否'; + String get eewTitle => '地震速報'; @override - String get stationSheetEmpty => '點選任一測站查看觀測值'; + String get mapLayerWindForecastEcmwf => 'ECMWF'; @override - String monitorDelay(String value) { - return '延遲 $value s'; + String regionSelectCount(int count, int max) { + return '已選 $count/$max'; } @override - String get monitorWaiting => '等待資料…'; + String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/雲相'; @override - String mapLegendUnit(String unit) { - return '單位:$unit'; - } + String get meshtasticStateError => '錯誤'; @override - String get typhoonLegendPast => '實際路徑'; + String get weatherModeOvercast => '陰天'; @override - String get typhoonIntensityTd => '熱帶性低氣壓'; + String get reportDetailDepth => '震源深度'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get typhoonOverlayWarningTooltip => '標示警報區域縣市'; @override - String typhoonPickerTd(String no) { - return '熱帶性低氣壓 TD $no'; - } + String get reportFilterDatePick => '選擇日期'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } - - @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get onboardingSkipStay => '返回授權'; @override - String get typhoonIntensityMild => '輕度颱風'; + String get commonFetchFailed => '無法獲取資料,請稍後重試'; @override - String get typhoonIntensityModerate => '中度颱風'; + String get shelterOutdoorLabel => '室外收容'; @override - String get typhoonIntensityIntense => '強烈颱風'; + String get meshtasticStateConnected => '已連線'; @override - String get typhoonLegendForecast => '預測路徑'; + String get mapNavRadar => '雷達'; @override - String get typhoonLegendForecastPoint => '預測點'; + String get mapLayerSatelliteCloudClear => '晴空'; @override - String get typhoonLegendCurrent => '目前中心'; + String eewSummary(String magnitude, String depth) { + return '規模 $magnitude・深度 $depth 公里'; + } @override - String get typhoonLegendCone => '預測圓錐'; + String get locationBannerPermission => '尚未授權定位,無法針對你的所在地推送警報。'; @override - String get mapLegendExpand => '圖例'; + String get typhoonOverlayWeatherNoneTooltip => '不疊雷達或紅外線'; @override - String get mapLegendCollapse => '收合圖例'; + String get radarCountyOutlineHint => '畫在回波之上'; @override - String get mapMyLocation => '我的位置'; + String get windForecastCountyOutlineHint => '繪製於風場之上'; @override - String get mapResetNorth => '回到北方'; + String get homeRainTrendTitle => '近 1 小時降水趨勢'; @override - String get typhoonLegendCircle15 => '七級風暴風圈'; + String get moonPhaseFirstQuarter => '上弦月'; @override - String get typhoonLegendCircleAvg => '平均圓'; + String get mapLayerCategoryTyphoon => '颱風'; @override - String get typhoonLegendCircle25 => '十級風暴風圈'; + String get meshtasticUtilization => '空中工時(24 小時)'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return '東北 $ne · 東南 $se · 西南 $sw · 西北 $nw km'; - } + String get restroomTypeMixed => '混合廁所'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get restroomGradeGood => '優等級'; @override - String get typhoonLegendProbability => '侵襲機率'; + String get notifyTsunami => '海嘯資訊'; @override - String get typhoonLegendWarningAreas => '警報區域'; + String get navData => '資料'; @override - String get typhoonOverlayMenuTooltip => '颱風圖層選項'; + String get mapLayerSatelliteBtdWvirw => 'ひまわり 過衝雲頂'; @override - String get typhoonOverlaySectionStorm => '暴風圈'; + String get meshtasticReadingAge => '數值時間'; @override - String get typhoonOverlaySectionExtra => '覆蓋層'; + String get mapAppCallFailed => '此裝置無法撥打電話'; @override - String get typhoonOverlayStormBandSubtitle => '含平均圓'; + String get reportFilterAny => '不限'; @override - String get typhoonOverlayProbabilityHint => '會隱藏預測圓錐'; + String get weatherRankingMergeTo => '合併至'; @override - String get typhoonOverlayProbabilityTooltip => '顯示侵襲機率(會隱藏預測圓錐)'; + String get notifyIntensity => '震度速報'; @override - String get typhoonOverlayWarningTooltip => '標示警報區域縣市'; + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } @override - String get typhoonOverlayStormL7Tooltip => '七級暴風圈+平均圓(紫色)'; + String get rainIntervalMenu => '累積時段'; @override - String get typhoonOverlayStormL10Tooltip => '十級暴風圈+平均圓(黃色)'; + String get reportDetailLocalFelt => '小區域有感地震'; @override - String get typhoonOverlaySectionWeather => '天氣底圖'; + String get meshtasticDevice => '裝置'; @override - String get typhoonOverlayWeatherNone => '無'; + String get onboardingGrant => '授權'; @override - String get typhoonOverlayWeatherHint => '對齊報文時間'; + String get weatherModeRain => '雨天'; @override - String get typhoonOverlayWeatherNoneTooltip => '不疊雷達或紅外線'; + String get shelterVulnerableOkLabel => '適合避難弱者安置'; @override - String get typhoonOverlayWeatherRadarTooltip => '雷達回波(對齊颱風報文時間)'; + String get stationSheetEmpty => '點選任一測站查看觀測值'; @override - String get typhoonOverlayWeatherSatelliteTooltip => '紅外線(對齊颱風報文時間)'; + String get typhoonLegendProbability => '侵襲機率'; @override - String get typhoonWarningTitle => '颱風警報'; + String get reportFilterMagnitude => '規模'; @override - String typhoonWarningAreas(String areas) { - return '警戒區域:$areas'; - } + String get skyTimeMorning => '上午'; @override - String get typhoonTrackDetail => '路徑詳情'; + String get experimentalFeatures => '實驗性功能'; @override - String get typhoonHistoryTitle => '資料時間'; + String get onboardingTermsBody => + '使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。'; @override - String get typhoonHistoryLive => '即時'; + String get reportFilterTitle => '篩選'; @override - String get typhoonSatelliteTitle => '衛星雲圖'; + String get onboardingPermCritical => '重大通知'; @override - String get typhoonOverlayForecastCallouts => '預測點資訊'; + String trendCumulativeTotal(String total) { + return '累計 $total mm'; + } @override - String get typhoonOverlayForecastCalloutsTooltip => '放大時顯示預測點詳細卡片'; + String get languageName => '繁體中文(臺灣)'; @override - String get dpmFilterSectionRestroom => '場所類型'; + String get reportListEmptyFiltered => '沒有符合條件的地震報告'; @override - String get dpmFilterSectionRestroomType => '廁所類型'; + String get meshtasticExcludeMqtt => '隱藏 MQTT 節點'; @override - String get dpmFilterSectionShelter => '避難所災害類型'; + String get mapNavTyphoon => '颱風'; @override - String get dpmDisasterFlood => '水災'; + String get weatherModeSand => '沙塵'; @override - String get dpmDisasterEarthquake => '震災'; + String get typhoonSatelliteTitle => '衛星雲圖'; @override - String get dpmDisasterLandslide => '土石流'; + String get notifyReport => '地震報告'; @override - String get dpmDisasterTsunami => '海嘯'; + String get mapAppCoordinatesCopied => '已複製座標'; @override - String get dpmDisasterSlope => '坡地災害'; + String get skyTimeNight => '夜晚'; @override - String get dpmDisasterNuclear => '核子事故'; + String get sponsorRecommended => '推薦'; @override - String get skyTime => '天空時間'; + String get mapLayerSatelliteB15 => 'ひまわり 長波紅外線(B15)'; @override - String get skyTimeAuto => '自動'; + String get weatherRankingWind => '風速'; @override - String get skyTimeDawn => '黎明'; + String get feedStale => '資料可能已過期'; @override - String get skyTimeSunrise => '日出'; + String homeForecastWind(String direction, String level) { + return '$direction · $level 級'; + } @override - String get skyTimeMorning => '上午'; + String get navHome => '首頁'; @override - String get skyTimeNoon => '正午'; + String get meshtasticRegionLabel => '地區'; @override - String get skyTimeAfternoon => '下午'; + String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂溫度'; @override - String get skyTimeGolden => '黃金時刻'; + String get moonTimelineCaption => '月相'; @override - String get skyTimeSunset => '日落'; + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth 公里'; + } @override - String get skyTimeDusk => '暮色'; + String get openSourceLicenses => '引用套件'; @override - String get skyTimeNight => '夜晚'; + String get weatherRankingLowest => '最低'; @override - String get weatherModeCloudy => '多雲'; + String get reportFilterSortDepth => '深度'; @override - String get weatherModeOvercast => '陰天'; + String mapTimelineDataTime(String time) { + return '資料時間 $time'; + } @override - String get weatherModeSnow => '下雪'; + String get radarScanRange => '顯示掃描範圍'; @override - String get weatherModeSand => '沙塵'; + String get meshtasticHopLimit => '跳數上限'; @override - String get radarScanRange => '顯示掃描範圍'; + String weatherRankingAnalysisRange(String value) { + return '溫差 $value°C'; + } @override - String get radarScanRangeSubtitle => '標示四座雷達實際觀測到的範圍。'; + String get weatherRankingExtremeHigh => '今日最高'; @override - String get radarScanRangeHint => '框外空白代表未觀測'; + String get changelogVersionDetails => '版本資訊'; @override - String get radarOverlayMenuTooltip => '雷達圖層選項'; + String get sponsorPrivacy => '隱私權政策'; @override - String get radarCountyOutline => '縣市界線'; + String get reportDetailLocalIntensity => '所在地的震度'; @override - String get radarGlobalOutline => '國界'; + String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; @override - String get radarGlobalOutlineHint => '各國國界外框'; + String get meshtasticAirtime => '發射佔空比'; @override - String get radarCountyOutlineHint => '畫在回波之上'; + String shelterCapacityValue(int n) { + return '$n 人'; + } @override - String get radarCountyOutlineSubtitle => '讓縣市界線在雷達回波下仍然清楚。'; + String lightningLegendCc(int minutes) { + return '雲間 · $minutes 分內'; + } @override - String get radarTownOutline => '鄉鎮界線'; + String get meshtasticSendHint => '要廣播的訊息'; @override - String get radarTownOutlineHint => '較細的分區'; + String monitorDelay(String value) { + return '延遲 $value s'; + } @override - String get radarTownOutlineSubtitle => '讓鄉鎮界線在雷達回波下仍然清楚。'; + String get dpmNo => '否'; @override - String get qpesumsOverlayMenuTooltip => '定量降水預報圖層選項'; + String get mapLayerSatelliteB08 => 'ひまわり 上層水氣(B08)'; @override - String get windForecastOverlayMenuTooltip => '風場預報圖層選項'; + String get meshtasticReconnecting => '重新連線中…'; @override - String get windForecastCountyOutlineHint => '繪製於風場之上'; + String get radarTownOutlineSubtitle => '讓鄉鎮界線在雷達回波下仍然清楚。'; @override - String get windForecastGlobalOutlineHint => '各國國界外框'; + String get typhoonOverlayWeatherSatelliteTooltip => '紅外線(對齊颱風報文時間)'; @override - String get windForecastTownOutlineHint => '更細的網格'; + String get radarScanRangeHint => '框外空白代表未觀測'; @override - String eewSerial(int serial) { - return '第 $serial 報'; + String typhoonPickerTd(String no) { + return '熱帶性低氣壓 TD $no'; } @override - String get eewMaxIntensity => '最大震度'; + String get mapLayerSatelliteWatervapor => 'ひまわり 水氣'; @override - String get eewLocalIntensity => '所在地預估'; + String get regionAddButton => '新增地區'; @override - String get eewSWave => '震波'; + String get displaySettings => '顯示設定'; @override - String get eewArrived => '已抵達'; + String get restroomGradePoor => '不合格'; @override - String eewCountdown(int seconds) { - return '$seconds 秒'; - } -} - -/// The translations for Chinese, using the Han script (`zh_Hans`). -class AppLocalizationsZhHans extends AppLocalizationsZh { - AppLocalizationsZhHans() : super('zh_Hans'); + String get restroomCategoryTourist => '觀光地區及風景區'; @override - String get languageName => '简体中文'; + String get locationBannerServiceOff => '定位服務已關閉,無法針對你的所在地推送警報。'; @override - String get navHome => '主页'; + String get mapLayerStyleTooltip => '顯示樣式'; @override - String get navEvents => '事件'; + String lightningLegendCg(int minutes) { + return '對地 · $minutes 分內'; + } @override - String get navMap => '地图'; + String get skyTimeAuto => '自動'; @override - String get navData => '资料'; + String get appLogs => 'App 日誌'; @override - String get navEarthquake => '地震'; + String get feedConnecting => '連線中…'; @override - String get dataSectionSeismic => '地震'; + String get notifyBannerDisabled => '通知已關閉,將收不到災害警報。'; @override - String get dataEarthquakeSubtitle => '地震报告'; + String get weatherHumidity => '濕度'; @override - String get dataSectionWeather => '气象'; + String typhoonValueMs(String n) { + return '每秒 $n 公尺'; + } @override - String get dataWeatherRankingSubtitle => '即时观测排行'; + String homeForecastHumidity(String value) { + return '濕度 $value%'; + } @override - String get weatherRankingTitle => '观测排行'; + String get meshtasticBusyBody => + '請先在另一個 Meshtastic App 中斷線。兩個 App 同時連同一台裝置會互相搶走訊息,導致部分訊息遺失。'; @override - String weatherRankingMeta(String time, int count) { - return '资料时间:$time\n共 $count 观测点'; - } + String get meshtasticChannelNoSlot => '沒有可用的頻道空位 — 請先在裝置上空出一個'; @override - String get weatherRankingEmpty => '目前没有可排序的观测'; + String get restroomCategoryTransport => '交通'; @override - String get weatherRankingBy => '依'; + String get reportFilterLocationHint => '例如:花蓮、東部海域'; @override - String get weatherRankingHighest => '最高'; + String get moonSubtitle => '月相與亮度 — 完全本地計算'; @override - String get weatherRankingLowest => '最低'; + String get meshtasticBattery => '電量'; @override - String get weatherRankingMergeTo => '合并至'; + String get meshtasticDistance => '距離'; @override - String get weatherRankingMergeTown => '乡镇'; + String get meshtasticSnrTrend => '訊號趨勢 (SNR)'; @override - String get weatherRankingMergeCounty => '县市'; + String get meshtasticBatteryTrend => '電量趨勢'; @override - String get weatherRankingWind => '风速'; + String get typhoonOverlayMenuTooltip => '颱風圖層選項'; @override - String get weatherRankingGust => '阵风'; + String get mapLayerSatelliteBtdOzone => 'ひまわり 對流層頂'; @override - String get weatherRankingTempExtremes => '温度极值'; + String meshtasticRegionMismatch(String region) { + return '裝置地區為 $region — DPIP 需要 TW'; + } @override - String get weatherRankingExtremeHigh => '今日最高'; + String get notifySectionEarthquake => '地震'; @override - String get weatherRankingExtremeLow => '今日最低'; + String get mapLayerDisasterMap => '防災地圖'; @override - String get weatherRankingExtremeRange => '日温差'; + String get weatherModeFog => '大霧'; @override - String weatherRankingRecordedAt(String time) { - return '记录于 $time'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; } @override - String weatherRankingAnalysisCurrent(String value) { - return '当下 $value°C'; - } + String get mapLayerStyleGrayTooltip => '氣象廳灰階慣例:溫度越低越白'; @override - String weatherRankingAnalysisHigh(String value) { - return '最高 $value'; - } + String get moreAnnouncements => '公告'; @override - String weatherRankingAnalysisLow(String value) { - return '最低 $value'; - } + String get mapLayerSatelliteTransparentNoData => '無資料(陸地) = 透明'; @override - String weatherRankingAnalysisRange(String value) { - return '温差 $value°C'; - } + String get restroomCategoryGovernment => '民眾洽公場所'; @override - String get reportListEmpty => '当前没有地震报告'; + String get typhoonLegendCurrent => '目前中心'; @override - String get reportListEmptyFiltered => '没有符合条件的地震报告'; + String get aedAddress => '地址'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth 公里'; - } + String get mapLayerAed => 'AED'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get changelogTypePrerelease => '公測'; @override - String get reportListDepthUnit => '公里'; + String get reportFilterIntensityInfoModernBody => + '震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。'; @override - String get reportListLocalFelt => '小区域有感'; + String get typhoonOverlayWeatherNone => '無'; @override - String get reportListToday => '今天'; + String get mapLayerStyleGray => '灰階(JMA)'; @override - String get reportListYesterday => '昨天'; + String get weatherModeAuto => '自動'; @override - String reportListDayCount(int count) { - return '$count'; - } + String get typhoonLabelProbCircle => '70%機率圓'; @override - String get reportListEnd => '已到最后一页'; + String get notifyOptAll => '接收全部'; @override - String get reportFilterTitle => '筛选'; + String get displayTheme => '主題'; @override - String get reportFilterSort => '排序方式'; + String get mapLayerSatelliteB07 => 'ひまわり 短波紅外(B07)'; @override - String get reportFilterSortTime => '时间'; + String get typhoonLabelDirection => '過去移動方向'; @override - String get reportFilterSortIntensity => '震度'; + String get regionManageTitle => '常用地區'; @override - String get reportFilterSortMagnitude => '规模'; + String get typhoonLegendCone => '預測圓錐'; @override - String get reportFilterSortDepth => '深度'; + String get moreCwaEew => '中央氣象署強震即時警報'; @override - String get reportFilterOrderDesc => '降序'; + String get onboardingPermsTitle => '權限授權'; @override - String get reportFilterOrderAsc => '升序'; + String get mapLayerStyleJma => '雲頂強調(JMA)'; @override - String get reportFilterIntensity => '震度'; + String get rainInterval10m => '10 分'; @override - String get reportFilterIntensityInfoTitle => '震度新制与旧制'; + String weatherRankingAnalysisLow(String value) { + return '最低 $value'; + } @override - String get reportFilterIntensityInfoIntro => - '中央气象署自 2020 年 1 月 1 日(台北时间)起改用新制震度。'; + String get meshtasticConnectAnyway => '仍要連線'; @override - String get reportFilterIntensityInfoLegacyTitle => '旧制(2020 以前)'; + String reportListDayCount(int count) { + return '$count'; + } @override - String get reportFilterIntensityInfoLegacyBody => '震度仅 0–7,没有 5弱/5强/6弱/6强。'; + String get mapLayerSatelliteB06 => 'ひまわり 近紅外(B06)'; @override - String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; + String get mapLayerSatelliteTransparentReflectance => '低反射率/夜間 = 透明,顯示底圖'; @override - String get reportFilterIntensityInfoModernBody => - '震度为 0–4、5弱、5强、6弱、6强、7。筛选滑杆依新制;列表中较早的地震会以旧制标示显示。'; + String chartHourLabel(int hour) { + return '$hour時'; + } @override - String get reportFilterMagnitude => '规模'; + String get mapLayerShelter => '避難收容場所'; @override - String get reportFilterDepth => '深度'; + String get typhoonOverlayProbabilityTooltip => '顯示侵襲機率(會隱藏預測圓錐)'; @override - String reportFilterDepthKm(String depth) { - return '$depth 公里'; - } + String get mapLayerSatelliteNdwi => 'ひまわり 水體指數'; @override - String get reportFilterDate => '日期'; + String get disasterMapOverlayShelterTooltip => '顯示避難收容場所'; @override - String get reportFilterDatePick => '选择日期'; + String get mapNavHumidity => '濕度'; @override - String get reportFilterDateStartNote => '开始日:当日 00:00(台北时间)'; + String get reportDetailSortByIntensity => '依震度排序'; @override - String get reportFilterDateEndNote => '结束日:当日 24:00(台北时间)'; + String get homeRainTrendNoData => '無資料'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get mapLayerCategoryRadar => '雷達'; @override - String get reportFilterLocation => '地点'; + String get meshtasticShortName => '簡稱'; @override - String get reportFilterLocationHint => '例如:花莲、东部海域'; + String get mapLayerSatelliteAirmass => 'ひまわり 氣團'; @override - String get reportFilterAny => '不限'; + String get typhoonTrackDetail => '路徑詳情'; @override - String get reportFilterApply => '应用'; + String get dataSectionWeather => '氣象'; @override - String get reportFilterReset => '重置'; + String get aedHoursWeekday => '平日開放時間'; @override - String get reportListSearch => '查询'; + String get homeActiveEventsTitle => '生效中事件'; @override - String get reportDetailTitle => '地震报告'; + String weatherRankingAnalysisHigh(String value) { + return '最高 $value'; + } @override - String reportDetailNumbered(String number) { - return '编号 $number 显著有感地震'; - } + String get faq => '常見問題'; @override - String get reportDetailLocalFelt => '小区域有感地震'; + String get typhoonHistoryLive => '即時'; @override - String get reportDetailInfo => '详细信息'; + String eewSerial(int serial) { + return '第 $serial 報'; + } @override - String get reportDetailOriginTime => '发震时间'; + String get reportFilterSort => '排序方式'; @override - String get reportDetailEpicenter => '震中坐标'; + String get meshtasticRegionConfirm => + '要將這台裝置切換為 TW 地區嗎?裝置會重新啟動並短暫斷線,上面的其他頻道也會一起改變。'; @override - String get reportDetailMagnitude => '地震规模'; + String get dataEarthquakeSubtitle => '地震報告'; @override - String get reportDetailDepth => '震源深度'; + String get typhoonNoActive => '目前無颱風'; @override - String get reportDetailAreaIntensity => '各地震度'; + String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/雲相(B11)'; @override - String get reportDetailLocalIntensity => '所在地的震度'; + String get navEvents => '事件'; @override - String get reportDetailLocalIntensityUnavailable => '没有震度信息'; + String get onboardingTermsTitle => '服務條款'; @override - String get reportDetailSortByIntensity => '依震度排序'; + String get mapTownLabels => '鄉鎮名稱'; @override - String get reportDetailSortByCounty => '依县市排序'; + String get notifySetFailed => '設定失敗,請稍後再試。'; @override - String get reportDetailImage => '地震报告图'; + String get meshtasticDisconnect => '斷線'; @override - String get reportDetailImageUnavailable => '报告图尚未提供'; + String get meshtasticUndecoded => '無法解密'; @override - String get reportDetailOpenReport => '报告页面'; + String get notifyAnnouncement => '公告'; @override - String get reportDetailReplay => '重播'; + String get onboardingIntroTitle => '歡迎使用 DPIP'; @override - String get navMore => '更多'; + String get regionCurrentUnavailable => '無法取得所在地位置資訊'; @override - String get appLogs => '应用日志'; + String get languageSystem => '系統預設'; @override - String get changelogTitle => '更新日志'; + String get skyTimeSunset => '日落'; @override - String get changelogEmpty => '目前没有更新日志'; + String get mapLayerSatelliteDust => 'ひまわり 沙塵'; @override - String get changelogTypePrerelease => '公测'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get changelogTypeStable => '正式'; + String get regionEdit => '修改'; @override - String get changelogCurrentVersion => '当前版本'; + String get weatherDynamicState => '天氣動態狀態'; @override - String get changelogVersionDetails => '版本信息'; + String get mapPlaceholderDisabled => '地圖(暫時停用)'; @override - String get changelogBodyEmpty => '此版本没有说明。'; + String get moonNow => '現在'; @override - String get mapPlaceholderDisabled => '地图(暂时禁用)'; + String get moonSectionAppearance => '外觀'; @override - String get moreSectionRegion => '地区'; + String get moonSectionRiseSet => '月出月沒'; @override - String get moreSectionNotify => '通知'; + String get moonSectionUpcoming => '接下來'; @override - String get moreSectionDisplay => '显示'; + String get moonSectionCalendar => '月曆'; @override - String get regionManageTitle => '常用地区'; + String get moonDistance => '距離'; @override - String get regionAddButton => '添加地区'; + String get moonKilometres => '公里'; @override - String get regionEmpty => '尚未添加常用地区'; + String get moonApparentSize => '視直徑'; @override - String get regionSelectTitle => '选择地区'; + String get moonRise => '月出'; @override - String regionSelectCount(int count, int max) { - return '已选 $count/$max'; - } + String get moonSet => '月沒'; @override - String regionSelectFull(int max) { - return '最多只能选择 $max 个地区'; - } + String get moonNextNewMoon => '下次新月'; @override - String get regionEdit => '修改'; + String get moonAlwaysUp => '整日在地平線上'; @override - String get moreSectionAdvanced => '高级'; + String get moonNoEvent => '當日無'; @override - String get moreDeveloper => '调试信息'; + String get sunTitle => '太陽'; @override - String get experimentalFeatures => '实验性功能'; + String get sunSubtitle => '日出日沒、曙暮光與節氣'; @override - String get moreSectionLinks => '相关链接'; + String get sunSectionDaylight => '日照'; @override - String get moreCwaEew => '中央气象署地震预警'; + String get sunSectionTwilight => '曙暮光'; @override - String get moreTremReport => 'TREM 检测报告'; + String get sunSectionLight => '光線'; @override - String get moreServerStatus => '服务器状态'; + String get sunSectionSundial => '日晷'; @override - String get moreAnnouncements => '公告'; + String get sunSectionTerms => '節氣'; @override - String get moreDiscord => 'Discord 社区'; + String get sunRise => '日出'; @override - String get moreNotifyLog => 'DPIP 通知发送记录'; + String get sunSet => '日沒'; @override - String get moreLinkOpenFailed => '无法打开链接'; + String get sunNoon => '正午'; @override - String get weatherDynamicState => '天气动画'; + String get sunDayLength => '白晝長度'; @override - String get weatherDynamicStateSubtitle => '覆盖首页背景天气'; + String get sunTwilightCivil => '民用'; @override - String get weatherModeAuto => '自动'; + String get sunTwilightNautical => '航海'; @override - String get weatherModeClear => '晴天'; + String get sunTwilightAstronomical => '天文'; @override - String get weatherModeRain => '雨天'; + String get sunGoldenHourMorning => '晨間黃金時刻'; @override - String get weatherModeFog => '大雾'; + String get sunGoldenHourEvening => '昏間黃金時刻'; @override - String get weatherModeThunderstorm => '雷雨'; + String get sunBlueHour => '藍調時刻'; @override - String get commonLoading => '加载中…'; + String get sunEquationOfTime => '均時差'; @override - String get commonRetry => '重试'; + String get sunMinutes => '分'; @override - String get commonError => '出错了'; + String get solarTermNext => '下一個節氣'; @override - String get commonFetchFailed => '无法获取数据,请稍后重试'; + String get planetsTitle => '行星'; @override - String get commonEmpty => '暂无内容'; + String get planetsSubtitle => '今晚在哪、有多亮'; @override - String get feedConnecting => '连接中…'; + String get planetsSectionTonight => '此刻'; @override - String get feedStale => '数据可能已过期'; + String get planetUp => '地平線上'; @override - String get feedOffline => '连接中断'; + String get planetDown => '地平線下'; @override - String get eewTitle => '地震预警'; + String get planetInGlare => '太近太陽'; @override - String get eewNone => '当前没有地震预警'; + String get planetMagnitude => '亮度'; @override - String eewSummary(String magnitude, String depth) { - return '震级 $magnitude·深度 $depth 公里'; - } + String get planetElongation => '距日距角'; @override - String get regionNationwide => '全国'; + String get planetSky => '時段'; @override - String get regionCurrent => '当前位置'; + String get planetEvening => '昏星'; @override - String get regionCurrentUnavailable => '无法获取所在地位置信息'; + String get planetMorning => '晨星'; @override - String get weatherPrecipitation => '降水量'; + String get planetDistance => '距離'; @override - String get weatherHumidity => '湿度'; + String get planetAu => '天文單位'; @override - String weatherDataTime(String station, String time) { - return '$station ∙ 资料时间 $time'; - } + String get planetAltitude => '仰角'; @override - String get homeViewOnMap => '前往地图察看'; + String get planetMercury => '水星'; @override - String get homeForecastTitle => '24小时预报'; + String get planetVenus => '金星'; @override - String homeForecastHighLow(String high, String low) { - return '高 $high° · 低 $low°'; - } + String get planetMars => '火星'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get planetJupiter => '木星'; @override - String homeForecastFeelsLike(String temp) { - return '体感 $temp°'; - } + String get planetSaturn => '土星'; @override - String homeForecastHumidity(String value) { - return '湿度 $value%'; - } + String get planetUranus => '天王星'; @override - String homeForecastWind(String direction, String level) { - return '$direction · $level 级'; - } + String get planetNeptune => '海王星'; @override - String get homeForecastUnavailable => '选择乡镇后可查看预报'; + String get solarTermVernalEquinox => '春分'; @override - String get homeForecastEmpty => '目前没有预报数据'; + String get solarTermPureBrightness => '清明'; @override - String get homeActiveEventsTitle => '生效中事件'; + String get solarTermGrainRain => '穀雨'; @override - String get homeActiveEventsEmpty => '目前没有生效中的事件'; + String get solarTermStartOfSummer => '立夏'; @override - String get homeRainTrendTitle => '近 1 小时降水趋势'; + String get solarTermGrainFull => '小滿'; @override - String homeRainTrendMinute(int minute) { - return '$minute分'; - } + String get solarTermGrainInEar => '芒種'; @override - String homeRainTrendUpdated(String time) { - return '更新 $time'; - } + String get solarTermSummerSolstice => '夏至'; @override - String get homeRainTrendNoData => '无资料'; + String get solarTermMinorHeat => '小暑'; @override - String get homeRainTrendScattered => '可能会有零星降雨'; + String get solarTermMajorHeat => '大暑'; @override - String get homeRainTrendLightSustained => '未来 1 小时会有持续小雨'; + String get solarTermStartOfAutumn => '立秋'; @override - String homeRainTrendLightStopping(int minutes) { - return '预计 $minutes 分钟后停止下小雨'; - } + String get solarTermEndOfHeat => '處暑'; @override - String get homeRainTrendHeavySustained => '未来 1 小时会有持续大雨'; + String get solarTermWhiteDew => '白露'; @override - String homeRainTrendHeavyStopping(int minutes) { - return '预计 $minutes 分钟后停止下大雨'; - } + String get solarTermAutumnalEquinox => '秋分'; @override - String get mapLayers => '图层'; + String get solarTermColdDew => '寒露'; @override - String get mapLayerOrderTitle => '调整图层顺序'; + String get solarTermFrostDescent => '霜降'; @override - String get mapLayerOrderReset => '恢复默认顺序'; + String get solarTermStartOfWinter => '立冬'; @override - String get mapLayerRadar => '雷达合成回波图'; + String get solarTermMinorSnow => '小雪'; @override - String get mapLayerSatellite => 'ひまわり 红外线(B13)'; + String get solarTermMajorSnow => '大雪'; @override - String get mapLayerSatelliteB01 => 'ひまわり 可见光-蓝(B01)'; + String get solarTermWinterSolstice => '冬至'; @override - String get mapLayerSatelliteB02 => 'ひまわり 可见光-绿(B02)'; + String get solarTermMinorCold => '小寒'; @override - String get mapLayerSatelliteB03 => 'ひまわり 可见光-红(B03)'; + String get solarTermMajorCold => '大寒'; @override - String get mapLayerSatelliteB04 => 'ひまわり 近红外(B04)'; + String get solarTermStartOfSpring => '立春'; @override - String get mapLayerSatelliteB05 => 'ひまわり 近红外(B05)'; + String get solarTermRainWater => '雨水'; @override - String get mapLayerSatelliteB06 => 'ひまわり 近红外(B06)'; + String get solarTermAwakeningOfInsects => '驚蟄'; @override - String get mapLayerSatelliteB07 => 'ひまわり 短波红外(B07)'; + String get tonightTitle => '今夜'; @override - String get mapLayerSatelliteB08 => 'ひまわり 上层水气(B08)'; + String get tonightSubtitle => '現在看得到什麼、什麼時候'; @override - String get mapLayerSatelliteB09 => 'ひまわり 中层水气(B09)'; + String get tonightSectionDark => '觀測窗口'; @override - String get mapLayerSatelliteB10 => 'ひまわり 低层水气(B10)'; + String get tonightAstronomicalNight => '天文夜'; @override - String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/云相(B11)'; + String get tonightNeverDark => '整夜不全暗'; @override - String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; + String get tonightDarkWindow => '暗窗'; @override - String get mapLayerSatelliteB13 => 'ひまわり 红外线(B13)'; + String get tonightMoonAllNight => '月亮整夜在天上'; @override - String get mapLayerSatelliteB14 => 'ひまわり 长波红外线(B14)'; + String get tonightDarkTotal => '總暗時'; @override - String get mapLayerSatelliteB15 => 'ひまわり 长波红外线(B15)'; + String get tonightMoonlight => '月光'; @override - String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; + String get tonightSectionShowers => '流星雨'; @override - String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; + String get tonightRadiantDown => '輻射點不升起'; @override - String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; + String get tonightPerHour => '顆/時'; @override - String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + String get tonightSectionSatellites => '衛星過境'; @override - String get mapLayerSatelliteDust => 'ひまわり 沙尘'; + String get tonightSectionTargets => '此刻可觀測目標'; @override - String get mapLayerSatelliteAirmass => 'ひまわり 气团'; + String get showerQuadrantids => '象限儀座'; @override - String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜间微物理'; + String get showerLyrids => '天琴座'; @override - String get mapLayerSatelliteWatervapor => 'ひまわり 水气'; + String get showerEtaAquariids => '寶瓶座η'; @override - String get mapLayerSatelliteBtdSplit => 'ひまわり 分割视窗'; + String get showerDeltaAquariids => '寶瓶座δ'; @override - String get mapLayerSatelliteBtdFog => 'ひまわり 夜间雾'; + String get showerPerseids => '英仙座'; @override - String get mapLayerSatelliteBtdWvirw => 'ひまわり 过冲云顶'; + String get showerOrionids => '獵戶座'; @override - String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/云相'; + String get showerSouthernTaurids => '金牛座南'; @override - String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷云/云高'; + String get showerLeonids => '獅子座'; @override - String get mapLayerSatelliteBtdOzone => 'ひまわり 对流层顶'; + String get showerGeminids => '雙子座'; @override - String get mapLayerSatelliteCloudtop => 'ひまわり 云顶温度'; + String get showerUrsids => '小熊座'; @override - String get mapLayerSatelliteCloudmask => 'ひまわり 云遮罩'; + String get deepSkyOpenCluster => '疏散星團'; @override - String get mapLayerSatelliteSst => 'ひまわり 海表温度'; + String get deepSkyGlobularCluster => '球狀星團'; @override - String get mapLayerSatelliteNdvi => 'ひまわり 植被指数'; + String get deepSkySpiralGalaxy => '螺旋星系'; @override - String get mapLayerSatelliteNdwi => 'ひまわり 水体指数'; + String get deepSkyEllipticalGalaxy => '橢圓星系'; @override - String get mapLayerSatelliteMndwi => 'ひまわり 改良水体指数'; + String get deepSkyIrregularGalaxy => '不規則星系'; @override - String get mapLayerSatelliteGlobalOutline => '国界'; + String get deepSkyPlanetaryNebula => '行星狀星雲'; @override - String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; + String get deepSkySupernovaRemnant => '超新星遺跡'; @override - String get mapLayerSatelliteCloudClear => '晴空'; + String get deepSkyEmissionNebula => '發射星雲'; @override - String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; + String get deepSkyReflectionNebula => '反射星雲'; @override - String get mapLayerSatelliteCloudProbablyCloudy => '可能有云'; + String get deepSkyAsterism => '星群'; @override - String get mapLayerSatelliteCloudCloudy => '有云'; + String get almanacTitle => '曆法'; @override - String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,显示底图'; + String get almanacSubtitle => '農曆日期與未來的日月食'; @override - String get mapLayerSatelliteTransparentReflectance => '低反射率/夜间 = 透明,显示底图'; + String get almanacSectionToday => '今日'; @override - String get mapLayerSatelliteTransparentZero => '零差值 = 透明(无信号)'; + String get almanacGregorian => '西曆'; @override - String get mapLayerSatelliteTransparentNight => '夜间 = 透明,显示底图'; + String get almanacLunar => '農曆'; @override - String get mapLayerSatelliteTransparentNoData => '无资料(陆地) = 透明'; + String get almanacYear => '歲次'; @override - String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(无植被)'; + String get almanacMonthLength => '月大小'; @override - String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(无水体)'; + String get almanacLongMonth => '三十日'; @override - String get mapLayerSatelliteTransparentClear => '晴空 = 透明,显示底图'; + String get almanacShortMonth => '二十九日'; @override - String get mapLayerStyleSection => '显示样式'; + String get almanacLeapPrefix => '閏'; @override - String get mapLayerStyleTooltip => '显示样式'; + String get almanacSectionLunarEclipses => '月食'; @override - String get mapLayerStyleGray => '灰度(JMA)'; + String get almanacSectionSolarEclipses => '日食'; @override - String get mapLayerStyleGrayTooltip => '气象厅灰度惯例:温度越低越白'; + String get almanacNoSolarEclipse => '範圍內無'; @override - String get mapLayerStyleJma => '云顶强调(JMA)'; + String get eclipseTotal => '全食'; @override - String get mapLayerStyleJmaTooltip => '灰阶为底,−40 °C 以下上色,凸显云顶高度'; + String get eclipsePartial => '偏食'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get eclipseAnnular => '環食'; @override - String get mapLayerStyleBdTooltip => 'Dvorak BD 曲线——热带气旋强度分析的阶梯灰度'; + String get eclipsePenumbral => '半影食'; @override - String get mapLayerQpesums => '未来 1 小时降水预报'; + String get zodiacRat => '鼠'; @override - String get mapLayerLightning => '闪电'; + String get zodiacOx => '牛'; @override - String lightningLegendCg(int minutes) { - return '对地 · $minutes 分钟内'; - } + String get zodiacTiger => '虎'; @override - String lightningLegendCc(int minutes) { - return '云间 · $minutes 分钟内'; - } + String get zodiacRabbit => '兔'; @override - String get mapTimelineNow => '现在'; + String get zodiacDragon => '龍'; @override - String get mapTimelinePast => '历史'; + String get zodiacSnake => '蛇'; @override - String get mapTimelineFuture => '未来'; + String get zodiacHorse => '馬'; @override - String get mapTimelineObserved => '观测'; + String get zodiacGoat => '羊'; @override - String get mapTimelineForecast => '预报'; + String get zodiacMonkey => '猴'; @override - String mapTimelineDataTime(String time) { - return '资料时间 $time'; - } + String get zodiacRooster => '雞'; @override - String get notifySettingsMenu => '通知设置'; + String get zodiacDog => '狗'; @override - String get notifyTitle => '通知'; + String get zodiacPig => '豬'; @override - String get notifyUnavailable => '推送通知尚未就绪,请稍后再试。'; + String get tideTitle => '潮汐'; @override - String get notifySetFailed => '设置失败,请稍后再试。'; + String get tideSubtitle => '大潮、小潮與月球引力'; @override - String get notifySectionEew => '地震预警'; + String get tideDisclaimer => '僅為天文引潮力,非港口潮汐表。水位請參考氣象署公布之潮汐預報。'; @override - String get notifySectionEarthquake => '地震'; + String get tideSectionNow => '此刻'; @override - String get notifySectionWeather => '天气'; + String get tidePhase => '週期'; @override - String get notifySectionTsunami => '海啸'; + String get tideSpring => '大潮'; @override - String get notifySectionOther => '其他'; + String get tideNeap => '小潮'; @override - String get notifyEew => '紧急地震预警'; + String get tideMiddling => '中潮'; @override - String get notifyMonitor => '强震监视器'; + String get tideLunarDistanceFactor => '月球引力'; @override - String get notifyReport => '地震报告'; + String get tideEquilibrium => '平衡潮高'; @override - String get notifyIntensity => '震度速报'; + String get tideMetres => '公尺'; @override - String get notifyThunderstorm => '雷雨预警'; + String get tidePerigeanSpring => '下次近地點大潮'; @override - String get notifyAdvisory => '气象预警'; + String get tideSectionTurningPoints => '轉折點'; @override - String get notifyEvacuation => '防灾信息'; + String get tideHigh => '高'; @override - String get notifyTsunami => '海啸信息'; + String get tideLow => '低'; @override - String get notifyAnnouncement => '公告'; + String get skyChartTitle => '星圖'; @override - String get notifyOptOff => '关闭'; + String get skyChartSubtitle => '頭頂上肉眼可見的天空'; @override - String get notifyOptAll => '接收全部'; + String get skyChartNorth => '北'; @override - String get notifyOptLocalIntensity4 => '本地震度4以上'; + String get skyChartEast => '東'; @override - String get notifyOptLocalIntensity1 => '本地震度1以上'; + String get skyChartSouth => '南'; @override - String get notifyOptWeatherLocal => '仅接收当前位置'; + String get skyChartWest => '西'; @override - String get notifyOptTsunamiWarning => '仅接收海啸警报'; + String tonightElementAge(int days) { + return '軌道資料 $days 天前'; + } @override - String get notifyOptTsunamiAll => '海啸消息、海啸警报'; + String almanacLunarDate(String leap, int month, int day) { + return '$leap$month 月 $day 日'; + } @override - String get onboardingNext => '下一步'; + String get tonightNoShowers => '目前無流星雨'; @override - String get onboardingBack => '上一步'; + String get tonightNoPasses => '48 小時內無可見過境'; @override - String get onboardingScrollHint => '向下滚动以继续'; + String get tonightSatellitesUnavailable => '無法讀取軌道資料'; @override - String get onboardingIntroTitle => '欢迎使用 DPIP'; + String get tonightNoTargets => '無足夠高度的目標'; @override - String get onboardingIntroBody => - 'DPIP 是与你并肩的防灾伙伴,整合地震预警、地震报告、天气与各类灾害信息,在关键时刻即时通知你。\n\n• 地震:地震预警、震度速报与详细报告\n• 天气:实时雷雨消息与气象预警\n• 海啸与防灾信息\n\n接下来,我们会请你阅读服务条款,并授权几项权限,让 DPIP 能实时守护你。'; + String get skyChartUnavailable => '無法讀取星表'; +} - @override - String get onboardingTermsTitle => '服务条款'; +/// The translations for Chinese, using the Han script (`zh_Hans`). +class AppLocalizationsZhHans extends AppLocalizationsZh { + AppLocalizationsZhHans() : super('zh_Hans'); @override - String get onboardingTermsBody => - '使用 DPIP 前,请详细阅读以下注意事项:\n\n• 任何信息均应以中央气象署(CWA)发布的内容为准。\n\n• 受网络状态、服务器状态、应用程序状态、上游数据来源状态等因素影响,存在收不到信息的可能,我们会尽力避免此类情况,但不保证一定不会发生。\n\n• 强烈震动有可能比通知更早抵达您所在的位置。\n\n• 地震预警为快速计算的结果,可能存在较大误差,请理解并谨慎使用。\n\n• 任何未获官方认可的行为均可能承担法律风险,请务必遵守相关规定。\n\n此外,为提供本地化预警,本服务会在前台及后台收集并上传您的大致位置与设备推送标识符,仅用于决定应向您推送哪些预警。\n\n点击下方“同意并继续”即表示您已阅读、理解并同意上述事项。'; + String typhoonValueLat(String lat) { + return '北纬 $lat 度'; + } @override - String get onboardingTermsAgree => '我已阅读并同意服务条款'; + String get onboardingSkipBody => + '未授权定位与通知,DPIP 将无法实时通知你所在地的地震与灾害。你仍可稍后在设置中开启。'; @override - String get onboardingAgreeContinue => '同意并继续'; + String get rainInterval24h => '24 时'; @override - String get onboardingPermsTitle => '权限授权'; + String homeRainTrendHeavyStopping(int minutes) { + return '预计 $minutes 分钟后停止下大雨'; + } @override - String get onboardingPermsBody => '为了在灾害发生的第一时间通知你,请授权以下权限。你可以随时在系统设置中更改。'; + String get mapTimelineObserved => '观测'; @override - String get onboardingPermNotify => '通知'; + String get regionSelectTitle => '选择地区'; @override - String get onboardingPermNotifyDesc => '在地震、天气与灾害发生时,即时推送预警通知。'; + String get skyTimeNoon => '正午'; @override - String get onboardingPermCritical => '重要警告'; + String get radarCountyOutlineSubtitle => '让县市界线在雷达回波下仍然清楚。'; @override - String get onboardingPermCriticalDesc => '让危及生命的地震预警,即使在静音或勿扰模式下也能发出声响。'; + String get dpmFilterSectionRestroomType => '厕所类型'; @override - String get onboardingPermLocation => '定位'; + String get mapLayerSatelliteB03 => 'ひまわり 可见光-红(B03)'; @override - String get onboardingPermLocationDesc => '根据你所在的位置推送本地预警。'; + String get reportFilterIntensity => '震度'; @override - String get onboardingPermBackground => '后台定位'; + String get mapLayerLightning => '闪电'; @override - String get onboardingPermBackgroundDesc => '选择“始终允许”,关闭应用后也能向你推送本地预警。'; + String get restroomTypeMale => '男厕所'; @override - String get onboardingPermBattery => '电池优化白名单'; + String get meshtasticLastReceived => '最近接收'; @override - String get onboardingPermBatteryDesc => '允许 DPIP 在后台持续运行,避免预警延迟或漏收。'; + String get reportDetailSortByCounty => '依县市排序'; @override - String get onboardingGrant => '授权'; + String get homeRainTrendScattered => '可能会有零星降雨'; @override - String get onboardingGranted => '已授权'; + String get meshtasticUptime => '运行时间'; @override - String get onboardingStart => '开始使用'; + String get weatherRankingTempExtremes => '温度极值'; @override - String get language => '语言'; + String get themeLight => '浅色'; @override - String get languageSettings => '语言设置'; + String get mapTerrainReliefHint => '在底图上显示立体地形阴影'; @override - String get languageSystem => '系统默认'; + String get meshtasticEmptyMessage => '(空白讯息)'; @override - String get locationBannerServiceOff => '定位服务已关闭,无法向你所在的区域推送本地预警。'; + String get moreSectionRegion => '地区'; @override - String get locationBannerPermission => '尚未授予定位权限,无法向你所在的区域推送本地预警。'; + String get dpmDisasterEarthquake => '震灾'; @override - String get locationBannerFix => '打开设置'; + String get mapLayerSatellite => 'ひまわり 红外线(B13)'; @override - String get notifyBannerDisabled => '通知已关闭,将收不到灾害警报。'; + String get aedHoursSaturday => '周六开放时间'; @override - String get onboardingSkipTitle => '尚未完成授权'; + String get dpmDisasterSlope => '坡地灾害'; @override - String get onboardingSkipBody => - '未授权定位与通知,DPIP 将无法实时通知你所在地的地震与灾害。你仍可稍后在设置中开启。'; + String get moonPhaseNew => '新月'; @override - String get onboardingSkipStay => '返回授权'; + String get notifySectionEew => '地震预警'; @override - String get onboardingSkipLeave => '仍要跳过'; + String get mapResetNorth => '回到正北'; @override - String get moreYoutube => 'YouTube'; + String get rainInterval2d => '2 日'; @override - String get moreGithub => 'ExpTech GitHub'; + String get mapTownLabelsHint => '放大时显示乡镇名称'; @override - String get moreSourceCode => '源代码'; + String get commonCancel => '取消'; @override - String get moreSectionApp => '获取 App'; + String get notifyOptTsunamiWarning => '仅接收海啸警报'; @override - String get moreGooglePlay => 'Google Play'; + String get mapLayerSatelliteBtdFog => 'ひまわり 夜间雾'; @override - String get moreAppStore => 'App Store'; + String get moreSectionAdvanced => '高级'; @override - String get displaySettings => '显示设置'; + String get weatherRankingExtremeRange => '日温差'; @override - String get defaultMapLayerSettings => '地图默认图层'; + String get notifySettingsMenu => '通知设置'; @override - String get defaultMapLayerSubtitle => '打开地图标签页时显示此图层,底部导航栏图标与文字会一并更新。'; + String get typhoonHistoryTitle => '资料时间'; @override - String get mapNavRadar => '雷达'; + String mapAppDefault(String app) { + return '$app(默认)'; + } @override - String get mapNavQpesums => '预报'; + String get trendRange24h => '24 小时'; @override - String get mapNavSatellite => '卫星'; + String get mapLayerStyleJmaTooltip => '灰阶为底,−40 °C 以下上色,凸显云顶高度'; @override - String get mapNavLightning => '闪电'; + String weatherRankingRecordedAt(String time) { + return '记录于 $time'; + } @override - String get mapNavTyphoon => '台风'; + String get mapLayerRain => '雨量'; @override - String get mapNavEarthquake => '地震'; + String get mapLayerQpesums => '未来 1 小时降水预报'; @override - String get mapNavTemperature => '温度'; + String get mapOverlaySectionMap => '地图'; @override - String get mapNavHumidity => '湿度'; + String get mapTerrainRelief => '地形立体感'; @override - String get mapNavPressure => '气压'; + String get eewMaxIntensity => '最大震度'; @override - String get mapNavWind => '风向'; + String get mapLegendCollapse => '收起图例'; @override - String get mapNavRain => '雨量'; + String get changelogTitle => '更新日志'; @override - String get mapNavDisaster => '防灾'; + String get reportFilterOrderDesc => '降序'; @override - String get displayTheme => '主题'; + String get meshtasticExcludeMqttSubtitle => '经互联网桥接、并非无线电听到的节点'; @override - String get themeSystem => '跟随系统'; + String get reportFilterIntensityInfoTitle => '震度新制与旧制'; @override - String get themeLight => '浅色'; + String get mapLayerTyphoon => '台风'; @override - String get themeDark => '深色'; + String get radarOverlayMenuTooltip => '雷达图层选项'; @override - String get moreSectionAbout => '关于'; + String get mapMyLocation => '我的位置'; @override - String get termsOfService => '服务条款'; + String get meshtasticNodes => '節點'; @override - String get faq => '常见问题'; + String get meshtasticSend => '傳送'; @override - String get openSourceLicenses => '开源许可'; + String get typhoonOverlayStormL7Tooltip => '七级风风场 + 平均圆(紫)'; @override - String get sponsorTitle => '支持 DPIP'; + String get aedType => '场所类型'; @override - String get sponsorIntro => - 'DPIP 致力于提供实时防灾信息,没有广告或其他盈利模式。您的支持能帮助我们维持服务器运行并持续开发。'; + String get termsOfService => '服务条款'; @override - String get sponsorSubscriptions => '订阅制'; + String get typhoonLegendCircle25 => '十级风暴风圈'; @override - String get sponsorRecommended => '推荐'; + String get sponsorTitle => '支持 DPIP'; @override - String get sponsorOneTime => '单次支持'; + String get mapNavSatellite => '卫星'; @override - String sponsorPerMonth(String price) { - return '$price / 月'; + String homeRainTrendUpdated(String time) { + return '更新 $time'; } @override - String get sponsorRestore => '恢复购买'; + String get onboardingNext => '下一步'; @override - String get sponsorTerms => '使用条款'; + String get weatherRankingMergeTown => '乡镇'; @override - String get sponsorPrivacy => '隐私政策'; + String get mapLayerMonitor => '强震监视器'; @override - String get sponsorRestoring => '正在恢复购买…'; + String get moreYoutube => 'YouTube'; @override - String get sponsorRestoreUnavailable => '无法连接到商店,请稍后再试'; + String get sponsorSubscriptions => '订阅制'; @override - String get commonClose => '关闭'; + String typhoonValueLon(String lon) { + return '东经 $lon 度'; + } @override - String get mapLayerTemperature => '温度'; + String get skyTime => '天空时间'; @override - String get trendRange24h => '24 小时'; + String get weatherModeCloudy => '多云'; @override - String get trendRange7d => '7 天'; + String get skyTimeDusk => '暮色'; @override - String get trendNoData => '没有趋势数据'; + String get meshtasticFirmware => '固件'; @override - String trendCumulativeTotal(String total) { - return '累计 $total mm'; - } + String get reportFilterDateEndNote => '结束日:当日 24:00(台北时间)'; @override - String chartHourLabel(int hour) { - return '$hour时'; - } + String get reportFilterSortMagnitude => '规模'; @override - String get mapLayerHumidity => '湿度'; + String get meshtasticSilent => '已静默'; @override - String get mapLayerPressure => '气压'; + String get mapLayerCategoryEarthquake => '地震'; @override - String get mapLayerWind => '风向'; + String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; @override - String get mapLayerRain => '雨量'; + String get typhoonLegendPast => '实际路径'; @override - String get rainIntervalMenu => '累积时段'; + String get restroomCategoryOther => '其他'; @override - String get rainIntervalNow => '今日'; + String homeForecastHighLow(String high, String low) { + return '高 $high° · 低 $low°'; + } @override - String get rainInterval10m => '10 分'; + String get locationBannerFix => '打开设置'; @override - String get rainInterval1h => '1 时'; + String get mapLegendExpand => '图例'; @override - String get rainInterval3h => '3 时'; + String get eewNone => '当前没有地震预警'; @override - String get rainInterval6h => '6 时'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get rainInterval12h => '12 时'; + String get notifyOptTsunamiAll => '海啸消息、海啸警报'; @override - String get rainInterval24h => '24 时'; + String get meshtasticLayerOptions => '节点选项'; @override - String get rainInterval2d => '2 日'; + String get onboardingAgreeContinue => '同意并继续'; @override - String get rainInterval3d => '3 日'; + String get commonRetry => '重试'; @override - String get mapLayerTyphoon => '台风'; + String get meshtasticNodeId => '节点 ID'; @override - String get typhoonNoActive => '目前无台风'; + String reportDetailNumbered(String number) { + return '编号 $number 显著有感地震'; + } @override - String get typhoonWind => '风速'; + String get typhoonOverlayStormBandSubtitle => '含平均圆'; @override - String get typhoonGust => '阵风'; + String get disasterMapOverlayRestroomTooltip => '显示公厕'; @override - String get typhoonPressure => '气压'; + String get weatherRankingTitle => '观测排行'; @override - String get typhoonMotion => '移动'; + String get homeRainTrendHeavySustained => '未来 1 小时会有持续大雨'; @override - String get typhoonLabelPosition => '中心位置'; + String get notifySectionTsunami => '海啸'; @override - String get typhoonLabelDirection => '过去移动方向'; + String get restroomCategoryPark => '公园'; @override - String get typhoonLabelSpeed => '过去移动时速'; + String get moreLinkOpenFailed => '无法打开链接'; @override - String get typhoonLabelPressure => '中心气压'; + String get themeDark => '深色'; @override - String get typhoonLabelWind => '近中心最大风速'; + String get sponsorRestore => '恢复购买'; @override - String get typhoonLabelGust => '瞬间最大阵风'; + String get meshtasticChannelWorking => '正在设定 DPIP 频道…'; @override - String get typhoonLabelGaleAvg => '七级风平均暴风半径'; + String get meshtasticRegionSwitch => '切换为 TW'; @override - String get typhoonLabelStormAvg => '十级风平均暴风半径'; + String get meshtasticTraffic => '流量'; @override - String get typhoonLabelProbCircle => '70%概率圆'; + String get mapLayerStyleBdTooltip => 'Dvorak BD 曲线——热带气旋强度分析的阶梯灰度'; @override - String typhoonForecastLead(String hours) { - return '预测 +$hours 小时'; - } + String get disasterMapOverlayAedTooltip => '显示 AED 位置'; @override - String get typhoonLabelNw => '西北侧'; + String get mapLayerHumidity => '湿度'; @override - String get typhoonLabelNe => '东北侧'; + String get mapLayerSatelliteTransparentNight => '夜间 = 透明,显示底图'; @override - String get typhoonLabelSw => '西南侧'; + String get meshtasticScanning => '掃描中…'; @override - String get typhoonLabelSe => '东南侧'; + String regionSelectFull(int max) { + return '最多只能选择 $max 个地区'; + } @override - String typhoonValueLat(String lat) { - return '北纬 $lat 度'; - } + String get meshtasticTitle => 'Meshtastic'; @override - String typhoonValueLon(String lon) { - return '东经 $lon 度'; - } + String get navMore => '更多'; @override - String typhoonValueKm(String n) { - return '$n 公里'; - } + String get meshtasticDpipChannel => 'DPIP 频道'; @override - String typhoonValueHpa(String n) { - return '$n 百帕'; - } + String get disasterMapOverlaySectionLayers => '图层'; @override - String typhoonValueMs(String n) { - return '每秒 $n 公尺'; - } + String get mapLayerSatelliteB05 => 'ひまわり 近红外(B05)'; @override - String typhoonDataTime(String time) { - return '资料时间\n$time'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return 'NE $ne · SE $se · SW $sw · NW $nw km'; } @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get typhoonLabelNe => '东北侧'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get meshtasticCopied => '已复制讯息'; @override - String get mapLayerMonitor => '强震监视器'; + String get reportListEmpty => '当前没有地震报告'; @override - String get mapLayerDisasterMap => '防灾地图'; + String get reportListEnd => '已到最后一页'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; @override - String get disasterMapOverlayMenuTooltip => '防灾地图图层'; + String get typhoonOverlaySectionExtra => '叠加层'; @override - String get disasterMapOverlaySectionLayers => '图层'; + String get eewSWave => '震波'; @override - String get disasterMapOverlayAedTooltip => '显示 AED 位置'; + String get meshtasticBusyTitle => '另一个 App 正在使用这台设备'; @override - String get aedAddress => '地址'; + String get restroomCategoryCultural => '文化育乐活动场所'; @override - String get aedRegion => '县市区域'; + String get typhoonLabelWind => '近中心最大风速'; @override - String get aedCategory => '场所分类'; + String get radarGlobalOutlineHint => '各国国界外框'; @override - String get aedType => '场所类型'; + String get notifyEvacuation => '防灾信息'; @override - String get aedPlaceDesc => '放置位置说明'; + String get typhoonLegendCircle15 => '七级风暴风圈'; @override - String get aedDescription => '备注'; + String get dataSectionAstronomy => '天文'; @override - String get aedHoursWeekday => '平日开放时间'; + String get homeRainTrendLightSustained => '未来 1 小时会有持续小雨'; @override - String get aedHoursSaturday => '周六开放时间'; + String get commonError => '出错了'; @override - String get aedHoursSunday => '周日开放时间'; + String get moonPhaseWaningCrescent => '殘月'; @override - String get aedOpenRemark => '开放时间备注'; + String get meshtasticPower => '电力'; @override - String get aedEmergencyPhone => '紧急联络电话'; + String get mapTimelineNow => '现在'; @override - String get mapLayerRestroom => '公厕'; + String reportFilterRange(String start, String end) { + return '$start – $end'; + } @override - String get mapLayerShelter => '避难收容场所'; + String get reportDetailOpenReport => '报告页面'; @override - String get disasterMapOverlayRestroomTooltip => '显示公厕'; + String get trendRange7d => '7 天'; @override - String get disasterMapOverlayShelterTooltip => '显示避难收容场所'; + String typhoonWarningAreas(String areas) { + return '警戒区域:$areas'; + } @override - String get dpmOpenInMaps => '打开地图'; + String get rainIntervalSection => '统计时间'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get notifyTitle => '通知'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get meshtasticTxPower => '发射功率'; @override - String mapAppDefault(String app) { - return '$app(默认)'; - } + String get restroomCategoryLabel => '类别'; @override - String get mapAppCopyCoordinates => '复制坐标'; + String get sponsorRestoring => '正在恢复购买…'; @override - String get mapAppCoordinatesCopied => '已复制坐标'; + String get sponsorIntro => + 'DPIP 致力于提供实时防灾信息,没有广告或其他盈利模式。您的支持能帮助我们维持服务器运行并持续开发。'; @override - String mapAppOpenFailed(String app) { - return '无法打开 $app'; - } + String get shelterAddressLabel => '地址'; @override - String get mapAppCallFailed => '此设备无法拨打电话'; + String get typhoonLabelStormAvg => '十级风平均暴风半径'; @override - String get mapOverlaySectionReference => '参考图层'; + String get restroomCategoryCommercial => '商业营业场所'; @override - String get mapLayerCategoryEarthquake => '地震'; + String get aedRegion => '县市区域'; @override - String get mapLayerCategoryTyphoon => '台风'; + String homeRainTrendLightStopping(int minutes) { + return '预计 $minutes 分钟后停止下小雨'; + } @override - String get mapLayerCategoryWeather => '气象观测'; + String get reportDetailInfo => '详细信息'; @override - String get mapLayerCategorySatellite => '卫星'; + String get mapNavWind => '风向'; @override - String get mapLayerCategoryRadar => '雷达'; + String get windForecastOverlayMenuTooltip => '风场预报图层选项'; @override - String get mapLayerCategoryLife => '生活'; + String get dataWeatherRankingSubtitle => '即时观测排行'; @override - String get mapLayerCategoryForecast => '数值预报'; + String homeRainTrendMinute(int minute) { + return '$minute分'; + } @override - String get mapOverlaySectionMap => '地图'; + String get rainInterval6h => '6 时'; @override - String get rainIntervalSection => '统计时间'; + String get restroomTypeUnspecified => '未设定'; @override - String get mapTownLabels => '乡镇名称'; + String get typhoonOverlayProbabilityHint => '会隐藏预测圆锥'; @override - String get mapTownLabelsHint => '放大时显示乡镇名称'; + String get mapLayerSatelliteGlobalOutline => '国界'; @override - String get mapTerrainRelief => '地形立体感'; + String get mapNavTemperature => '温度'; @override - String get mapTerrainReliefHint => '在底图上显示立体地形阴影'; + String get typhoonLegendForecastPoint => '预测点'; @override - String get dpmSheetEmpty => '点击地图上的标记查看详情'; + String get reportListYesterday => '昨天'; @override - String get dpmAddress => '地址'; + String get moreSectionLinks => '相关链接'; @override - String get restroomTypeLabel => '厕所类型'; + String get feedOffline => '连接中断'; @override - String get restroomCategoryLabel => '类别'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get restroomGradeLabel => '等级'; + String get moreSectionDisplay => '显示'; @override - String get restroomTypeFemale => '女厕所'; + String get rainInterval3d => '3 日'; @override - String get restroomTypeMale => '男厕所'; + String get defaultMapLayerSubtitle => '打开地图标签页时显示此图层,底部导航栏图标与文字会一并更新。'; @override - String get restroomTypeMixed => '混合厕所'; + String get aedDescription => '备注'; @override - String get restroomTypeAccessible => '无障碍厕所'; + String get typhoonOverlayWeatherRadarTooltip => '最接近台风报文时间的雷达回波'; @override - String get restroomTypeGenderNeutral => '性别友善厕所'; + String get onboardingPermLocationDesc => '根据你所在的位置推送本地预警。'; @override - String get restroomTypeFamily => '亲子厕所'; + String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; @override - String get restroomTypeUnspecified => '未设定'; + String get homeActiveEventsEmpty => '目前没有生效中的事件'; @override - String get restroomCategoryTransport => '交通'; + String get typhoonLabelPosition => '中心位置'; @override - String get restroomCategoryPark => '公园'; + String get weatherRankingBy => '依'; @override - String get restroomCategoryCommercial => '商业营业场所'; + String get typhoonIntensityMild => '轻度台风'; @override - String get restroomCategoryReligious => '宗教礼仪场所'; + String get windForecastGlobalOutlineHint => '各国国界外框'; @override - String get restroomCategoryCultural => '文化育乐活动场所'; + String get rainInterval1h => '1 时'; @override - String get restroomCategoryGovernment => '民众洽公场所'; + String get eewLocalIntensity => '所在地预估'; @override - String get restroomCategoryWelfare => '社福机构、集会场所'; + String get mapLayerRadar => '雷达合成回波图'; @override - String get restroomCategoryTourist => '观光地区及风景区'; + String get restroomCategoryReligious => '宗教礼仪场所'; @override - String get restroomCategoryLeisure => '休闲娱乐场所'; + String get meshtasticRole => '角色'; @override - String get restroomCategoryOther => '其他'; + String get mapLayerSatelliteCloudCloudy => '有云'; @override - String get restroomGradeExcellent => '特优级'; + String get skyTimeSunrise => '日出'; @override - String get restroomGradeGood => '优等级'; + String get meshtasticNoMessages => '尚无讯息'; @override - String get restroomGradeAverage => '普通级'; + String get onboardingPermNotifyDesc => '在地震、天气与灾害发生时,即时推送预警通知。'; @override - String get restroomGradePoor => '不合格'; + String get radarTownOutline => '乡镇界线'; @override - String get shelterAddressLabel => '地址'; + String get mapLayerStyleSection => '显示样式'; @override - String get shelterCapacityLabel => '收容人数'; + String get disasterMapOverlayMenuTooltip => '防灾地图图层'; @override - String shelterCapacityValue(int n) { - return '$n 人'; - } + String get moreGooglePlay => 'Google Play'; @override - String get shelterCategoryLabel => '适用灾害'; + String get meshtasticOnline => '近期听到'; @override - String get shelterIndoorLabel => '室内收容'; + String get typhoonLabelSw => '西南侧'; @override - String get shelterOutdoorLabel => '室外收容'; + String typhoonForecastLead(String hours) { + return '预测 +$hours 小时'; + } @override - String get shelterVulnerableOkLabel => '适合避难弱者安置'; + String get dpmDisasterTsunami => '海啸'; @override - String get dpmYes => '是'; + String get changelogTypeStable => '正式'; @override - String get dpmNo => '否'; + String get mapLayerSatelliteTransparentClear => '晴空 = 透明,显示底图'; @override - String get stationSheetEmpty => '点选任一测站查看观测值'; + String get mapOverlaySectionReference => '参考图层'; @override - String monitorDelay(String value) { - return '延迟 $value s'; - } + String get mapLayerSatelliteB02 => 'ひまわり 可见光-绿(B02)'; @override - String get monitorWaiting => '等待数据…'; + String get reportListLocalFelt => '小区域有感'; @override - String mapLegendUnit(String unit) { - return '单位:$unit'; - } + String get weatherRankingEmpty => '目前没有可排序的观测'; @override - String get typhoonLegendPast => '实际路径'; + String get notifySectionOther => '其他'; @override - String get typhoonIntensityTd => '热带性低气压'; + String weatherRankingMeta(String time, int count) { + return '资料时间:$time\n共 $count 观测点'; + } @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get onboardingTermsAgree => '我已阅读并同意服务条款'; @override - String typhoonPickerTd(String no) { - return '热带性低气压 TD $no'; - } + String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(无植被)'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get notifyOptLocalIntensity4 => '本地震度4以上'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get eewArrived => '已抵达'; @override - String get typhoonIntensityMild => '轻度台风'; + String get meshtasticNoDevices => '找不到 Meshtastic 裝置'; @override - String get typhoonIntensityModerate => '中度台风'; + String get mapLayerCategoryLife => '生活'; @override - String get typhoonIntensityIntense => '强烈台风'; + String get reportFilterSortIntensity => '震度'; @override - String get typhoonLegendForecast => '预测路径'; + String get typhoonMotion => '移动'; @override - String get typhoonLegendForecastPoint => '预测点'; + String get meshtasticStateDisconnected => '未連線'; @override - String get typhoonLegendCurrent => '目前中心'; + String get typhoonIntensityIntense => '强烈台风'; @override - String get typhoonLegendCone => '预测圆锥'; + String get mapLayerOrderTitle => '调整图层顺序'; @override - String get mapLegendExpand => '图例'; + String get dpmYes => '是'; @override - String get mapLegendCollapse => '收起图例'; + String get meshtasticNoHistory => '历史纪录还不够'; @override - String get mapMyLocation => '我的位置'; + String get reportDetailLocalIntensityUnavailable => '没有震度信息'; @override - String get mapResetNorth => '回到正北'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get typhoonLegendCircle15 => '七级风暴风圈'; + String get reportListDepthUnit => '公里'; @override - String get typhoonLegendCircleAvg => '平均圆'; + String get reportFilterDepth => '深度'; @override - String get typhoonLegendCircle25 => '十级风暴风圈'; + String get onboardingScrollHint => '向下滚动以继续'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return 'NE $ne · SE $se · SW $sw · NW $nw km'; - } + String get mapNavQpesums => '预报'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get navMap => '地图'; @override - String get typhoonLegendProbability => '侵袭概率'; + String get notifyAdvisory => '气象预警'; @override - String get typhoonLegendWarningAreas => '警报区域'; + String get reportFilterReset => '重置'; @override - String get typhoonOverlayMenuTooltip => '台风图层选项'; + String get mapLayerSatelliteMndwi => 'ひまわり 改良水体指数'; @override String get typhoonOverlaySectionStorm => '暴风圈'; @override - String get typhoonOverlaySectionExtra => '叠加层'; + String get moonPhaseFull => '滿月'; @override - String get typhoonOverlayStormBandSubtitle => '含平均圆'; + String get moonPhaseWaningGibbous => '虧凸月'; @override - String get typhoonOverlayProbabilityHint => '会隐藏预测圆锥'; + String get weatherDynamicStateSubtitle => '覆盖首页背景天气'; @override - String get typhoonOverlayProbabilityTooltip => '显示侵袭概率(隐藏预测圆锥)'; + String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; @override - String get typhoonOverlayWarningTooltip => '标示发布台风警报的县市'; + String typhoonDataTime(String time) { + return '资料时间\n$time'; + } @override - String get typhoonOverlayStormL7Tooltip => '七级风风场 + 平均圆(紫)'; + String get restroomTypeAccessible => '无障碍厕所'; @override - String get typhoonOverlayStormL10Tooltip => '十级风风场 + 平均圆(黄)'; + String get moreSectionAbout => '关于'; @override - String get typhoonOverlaySectionWeather => '天气底图'; + String get meshtasticSelectDevice => '选择装置'; @override - String get typhoonOverlayWeatherNone => '无'; + String get onboardingIntroBody => + 'DPIP 是与你并肩的防灾伙伴,整合地震预警、地震报告、天气与各类灾害信息,在关键时刻即时通知你。\n\n• 地震:地震预警、震度速报与详细报告\n• 天气:实时雷雨消息与气象预警\n• 海啸与防灾信息\n\n接下来,我们会请你阅读服务条款,并授权几项权限,让 DPIP 能实时守护你。'; @override - String get typhoonOverlayWeatherHint => '对齐报文时间'; + String get shelterCapacityLabel => '收容人数'; @override - String get typhoonOverlayWeatherNoneTooltip => '不显示雷达或红外线底图'; + String get reportDetailImage => '地震报告图'; @override - String get typhoonOverlayWeatherRadarTooltip => '最接近台风报文时间的雷达回波'; + String get meshtasticStateConfiguring => '設定中…'; @override - String get typhoonOverlayWeatherSatelliteTooltip => '最接近台风报文时间的红外线'; + String get typhoonLabelGaleAvg => '七级风平均暴风半径'; @override - String get typhoonWarningTitle => '台风警报'; + String get onboardingPermNotify => '通知'; @override - String typhoonWarningAreas(String areas) { - return '警戒区域:$areas'; - } + String get meshtasticClearMessages => '清除讯息'; @override - String get typhoonTrackDetail => '路径详情'; + String get meshtasticNotifyMessages => '新讯息通知'; @override - String get typhoonHistoryTitle => '资料时间'; + String get defaultMapLayerSettings => '地图默认图层'; @override - String get typhoonHistoryLive => '实时'; + String get moreSectionNotify => '通知'; @override - String get typhoonSatelliteTitle => '卫星云图'; + String get notifyUnavailable => '推送通知尚未就绪,请稍后再试。'; @override - String get typhoonOverlayForecastCallouts => '预测点信息'; + String get mapLayerOrderReset => '恢复默认顺序'; @override - String get typhoonOverlayForecastCalloutsTooltip => '放大时显示预测点详细卡片'; + String get dpmAddress => '地址'; @override - String get dpmFilterSectionRestroom => '场所类型'; + String get weatherRankingMergeCounty => '县市'; @override - String get dpmFilterSectionRestroomType => '厕所类型'; + String get moreSectionApp => '获取 App'; @override - String get dpmFilterSectionShelter => '避难所灾害类型'; + String get reportFilterIntensityInfoLegacyBody => '震度仅 0–7,没有 5弱/5强/6弱/6强。'; @override - String get dpmDisasterFlood => '水灾'; + String get mapLayerSatelliteSst => 'ひまわり 海表温度'; @override - String get dpmDisasterEarthquake => '震灾'; + String get qpesumsOverlayMenuTooltip => '定量降水预报图层选项'; @override - String get dpmDisasterLandslide => '土石流'; + String get mapTimelineFuture => '未来'; @override - String get dpmDisasterTsunami => '海啸'; + String get typhoonLegendCircleAvg => '平均圆'; @override - String get dpmDisasterSlope => '坡地灾害'; + String reportFilterDepthKm(String depth) { + return '$depth 公里'; + } @override - String get dpmDisasterNuclear => '核子事故'; + String get typhoonLabelSe => '东南侧'; @override - String get skyTime => '天空时间'; + String get radarTownOutlineHint => '较细的分区'; @override - String get skyTimeAuto => '自动'; + String eewCountdown(int seconds) { + return '$seconds 秒'; + } @override - String get skyTimeDawn => '黎明'; + String get typhoonLabelGust => '瞬间最大阵风'; @override - String get skyTimeSunrise => '日出'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get skyTimeMorning => '上午'; + String get sponsorTerms => '使用条款'; @override - String get skyTimeNoon => '正午'; + String get restroomTypeGenderNeutral => '性别友善厕所'; @override - String get skyTimeAfternoon => '下午'; + String get notifyThunderstorm => '雷雨预警'; @override String get skyTimeGolden => '黄金时刻'; @override - String get skyTimeSunset => '日落'; + String get moonAge => '月齡'; @override - String get skyTimeDusk => '暮色'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get skyTimeNight => '夜晚'; + String weatherRankingAnalysisCurrent(String value) { + return '当下 $value°C'; + } @override - String get weatherModeCloudy => '多云'; + String get moreGithub => 'ExpTech GitHub'; @override - String get weatherModeOvercast => '阴天'; + String get homeForecastUnavailable => '选择乡镇后可查看预报'; @override - String get weatherModeSnow => '下雪'; + String get mapLayers => '图层'; @override - String get weatherModeSand => '沙尘'; + String get meshtasticHardware => '硬件'; @override - String get radarScanRange => '显示扫描范围'; + String get languageSettings => '语言设置'; @override - String get radarScanRangeSubtitle => '标示四座雷达实际观测到的范围。'; + String get dpmDisasterNuclear => '核子事故'; @override - String get radarScanRangeHint => '框外空白代表未观测'; + String get language => '语言'; @override - String get radarOverlayMenuTooltip => '雷达图层选项'; + String homeForecastFeelsLike(String temp) { + return '体感 $temp°'; + } @override - String get radarCountyOutline => '县市界线'; + String get typhoonOverlayWeatherHint => '对齐报文时间'; @override - String get radarGlobalOutline => '国界'; + String get skyTimeDawn => '黎明'; @override - String get radarGlobalOutlineHint => '各国国界外框'; + String get skyTimeAfternoon => '下午'; @override - String get radarCountyOutlineHint => '画在回波之上'; + String get meshtasticLastHeard => '最后听到'; @override - String get radarCountyOutlineSubtitle => '让县市界线在雷达回波下仍然清楚。'; + String get typhoonWarningTitle => '台风警报'; @override - String get radarTownOutline => '乡镇界线'; + String get moreSourceCode => '源代码'; @override - String get radarTownOutlineHint => '较细的分区'; + String get mapLayerCategoryWeather => '气象观测'; @override - String get radarTownOutlineSubtitle => '让乡镇界线在雷达回波下仍然清楚。'; + String get mapLayerSatelliteB09 => 'ひまわり 中层水气(B09)'; @override - String get qpesumsOverlayMenuTooltip => '定量降水预报图层选项'; + String get windForecastTownOutlineHint => '更细的网格'; @override - String get windForecastOverlayMenuTooltip => '风场预报图层选项'; + String get mapLayerSatelliteCloudmask => 'ひまわり 云遮罩'; @override - String get windForecastCountyOutlineHint => '绘制于风场之上'; + String get mapAppCopyCoordinates => '复制坐标'; @override - String get windForecastGlobalOutlineHint => '各国国界外框'; + String get reportFilterIntensityInfoIntro => + '中央气象署自 2020 年 1 月 1 日(台北时间)起改用新制震度。'; @override - String get windForecastTownOutlineHint => '更细的网格'; + String get mapNavEarthquake => '地震'; @override - String eewSerial(int serial) { - return '第 $serial 报'; - } + String get typhoonGust => '阵风'; @override - String get eewMaxIntensity => '最大震度'; + String get restroomGradeAverage => '普通级'; @override - String get eewLocalIntensity => '所在地预估'; + String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷云/云高'; @override - String get eewSWave => '震波'; + String get onboardingPermBackgroundDesc => '选择“始终允许”,关闭应用后也能向你推送本地预警。'; @override - String get eewArrived => '已抵达'; + String get mapTimelineForecast => '预报'; @override - String eewCountdown(int seconds) { - return '$seconds 秒'; - } -} + String get restroomTypeLabel => '厕所类型'; -/// The translations for Chinese, as used in Hong Kong, using the Han script (`zh_Hant_HK`). -class AppLocalizationsZhHantHk extends AppLocalizationsZh { + @override + String get navEarthquake => '地震'; + + @override + String get typhoonOverlayStormL10Tooltip => '十级风风场 + 平均圆(黄)'; + + @override + String get moonPhaseWaxingGibbous => '盈凸月'; + + @override + String get reportDetailTitle => '地震报告'; + + @override + String get moreTremReport => 'TREM 检测报告'; + + @override + String weatherDataTime(String station, String time) { + return '$station ∙ 资料时间 $time'; + } + + @override + String get meshtasticNoNodes => '尚未听到任何节点'; + + @override + String get meshtasticViaMqtt => '经 MQTT(互联网)'; + + @override + String get radarCountyOutline => '县市界线'; + + @override + String get onboardingGranted => '已授权'; + + @override + String get commonClose => '关闭'; + + @override + String get restroomGradeLabel => '等级'; + + @override + String get rainIntervalNow => '今日'; + + @override + String get changelogCurrentVersion => '当前版本'; + + @override + String get typhoonLabelPressure => '中心气压'; + + @override + String get typhoonOverlayForecastCalloutsTooltip => '放大时显示预测点详细卡片'; + + @override + String get aedOpenRemark => '开放时间备注'; + + @override + String get onboardingPermsBody => '为了在灾害发生的第一时间通知你,请授权以下权限。你可以随时在系统设置中更改。'; + + @override + String get typhoonOverlaySectionWeather => '天气底图'; + + @override + String get notifyOptWeatherLocal => '仅接收当前位置'; + + @override + String get mapNavRain => '雨量'; + + @override + String get moonDays => '天'; + + @override + String mapLegendUnit(String unit) { + return '单位:$unit'; + } + + @override + String get weatherModeClear => '晴天'; + + @override + String get meshtasticRadio => '电台'; + + @override + String get commonEmpty => '暂无内容'; + + @override + String get mapLayerSatelliteB01 => 'ひまわり 可见光-蓝(B01)'; + + @override + String get meshtasticExternalPower => '外部供电'; + + @override + String get moonPhaseLastQuarter => '下弦月'; + + @override + String get reportFilterOrderAsc => '升序'; + + @override + String get reportFilterApply => '应用'; + + @override + String get reportDetailImageUnavailable => '报告图尚未提供'; + + @override + String get weatherRankingHighest => '最高'; + + @override + String get reportDetailReplay => '重播'; + + @override + String get mapLayerRestroom => '公厕'; + + @override + String get restroomCategoryWelfare => '社福机构、集会场所'; + + @override + String get restroomGradeExcellent => '特优级'; + + @override + String get meshtasticLastSent => '最近送出'; + + @override + String get meshtasticName => '名称'; + + @override + String get meshtasticScan => '掃描'; + + @override + String get mapLayerCategoryForecast => '数值预报'; + + @override + String get meshtasticChannelFailed => '无法设定 DPIP 频道'; + + @override + String get themeSystem => '跟随系统'; + + @override + String get mapLayerSatelliteNdvi => 'ひまわり 植被指数'; + + @override + String get typhoonLegendForecast => '预测路径'; + + @override + String typhoonValueHpa(String n) { + return '$n 百帕'; + } + + @override + String get weatherPrecipitation => '降水量'; + + @override + String get moonNextFullMoon => '下次滿月'; + + @override + String get dpmSheetEmpty => '点击地图上的标记查看详情'; + + @override + String get onboardingSkipLeave => '仍要跳过'; + + @override + String get onboardingBack => '上一步'; + + @override + String get aedPlaceDesc => '放置位置说明'; + + @override + String get onboardingSkipTitle => '尚未完成授权'; + + @override + String get restroomTypeFamily => '亲子厕所'; + + @override + String typhoonValueKm(String n) { + return '$n 公里'; + } + + @override + String get typhoonPressure => '气压'; + + @override + String get onboardingPermBattery => '电池优化白名单'; + + @override + String get typhoonLabelNw => '西北侧'; + + @override + String get dpmDisasterFlood => '水灾'; + + @override + String get moonPhaseWaxingCrescent => '眉月'; + + @override + String get restroomCategoryLeisure => '休闲娱乐场所'; + + @override + String get mapLayerTemperature => '温度'; + + @override + String get aedCategory => '场所分类'; + + @override + String get meshtasticChannels => '频道'; + + @override + String get monitorWaiting => '等待数据…'; + + @override + String get typhoonOverlayForecastCallouts => '预测点信息'; + + @override + String get reportDetailEpicenter => '震中坐标'; + + @override + String get meshtasticVoltage => '电压'; + + @override + String get mapLayerMeshtasticSubtitle => '电台听到过的 LoRa 网状网路节点'; + + @override + String get mapLayerWind => '风向'; + + @override + String get reportDetailMagnitude => '地震规模'; + + @override + String get reportDetailAreaIntensity => '各地震度'; + + @override + String get rainInterval12h => '12 时'; + + @override + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } + + @override + String get dpmDisasterLandslide => '土石流'; + + @override + String get notifyMonitor => '强震监视器'; + + @override + String get onboardingStart => '开始使用'; + + @override + String sponsorPerMonth(String price) { + return '$price / 月'; + } + + @override + String get mapLayerPressure => '气压'; + + @override + String get mapLayerSatelliteB04 => 'ひまわり 近红外(B04)'; + + @override + String get mapLayerSatelliteTransparentZero => '零差值 = 透明(无信号)'; + + @override + String get shelterIndoorLabel => '室内收容'; + + @override + String get notifyOptOff => '关闭'; + + @override + String get reportFilterSortTime => '时间'; + + @override + String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; + + @override + String get weatherModeThunderstorm => '雷雨'; + + @override + String get homeViewOnMap => '前往地图察看'; + + @override + String get reportFilterIntensityInfoLegacyTitle => '旧制(2020 以前)'; + + @override + String get typhoonLabelSpeed => '过去移动时速'; + + @override + String mapAppOpenFailed(String app) { + return '无法打开 $app'; + } + + @override + String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; + + @override + String get meshtasticReceived => '已接收'; + + @override + String get weatherRankingExtremeLow => '今日最低'; + + @override + String get mapLayerSatelliteB10 => 'ひまわり 低层水气(B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => '可能有云'; + + @override + String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(无水体)'; + + @override + String get shelterCategoryLabel => '适用灾害'; + + @override + String get meshtasticStateConnecting => '連線中…'; + + @override + String get moonTitle => '月亮'; + + @override + String get weatherRankingGust => '阵风'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => '避难所灾害类型'; + + @override + String get moreServerStatus => '服务器状态'; + + @override + String get notifySectionWeather => '天气'; + + @override + String get meshtasticPreset => '调变预设'; + + @override + String get dataSectionSeismic => '地震'; + + @override + String get changelogBodyEmpty => '此版本没有说明。'; + + @override + String get radarGlobalOutline => '国界'; + + @override + String get notifyEew => '紧急地震预警'; + + @override + String get regionNationwide => '全国'; + + @override + String get moreNotifyLog => 'DPIP 通知发送记录'; + + @override + String get regionCurrent => '当前位置'; + + @override + String get dpmFilterSectionRestroom => '场所类型'; + + @override + String get meshtasticNotConnected => '尚未连线至装置'; + + @override + String get weatherModeSnow => '下雪'; + + @override + String get mapLayerMeshtastic => 'Meshtastic 节点'; + + @override + String get moreDeveloper => '调试信息'; + + @override + String get mapLayerSatelliteB14 => 'ひまわり 长波红外线(B14)'; + + @override + String get meshtasticChannelUse => '频道使用率'; + + @override + String get mapNavLightning => '闪电'; + + @override + String get homeForecastEmpty => '目前没有预报数据'; + + @override + String get sponsorOneTime => '单次支持'; + + @override + String get mapLayerSatelliteBtdSplit => 'ひまわり 分割视窗'; + + @override + String get onboardingPermBackground => '后台定位'; + + @override + String get aedEmergencyPhone => '紧急联络电话'; + + @override + String get dpmOpenInMaps => '打开地图'; + + @override + String get meshtasticNotifyNodes => '新节点通知'; + + @override + String get onboardingPermCriticalDesc => '让危及生命的地震预警,即使在静音或勿扰模式下也能发出声响。'; + + @override + String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,显示底图'; + + @override + String get meshtasticSent => '已送出'; + + @override + String get homeForecastTitle => '24小时预报'; + + @override + String get typhoonLegendWarningAreas => '警报区域'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '已隐藏 $count 个'; + } + + @override + String get notifyOptLocalIntensity1 => '本地震度1以上'; + + @override + String get mapTimelinePast => '历史'; + + @override + String get restroomTypeFemale => '女厕所'; + + @override + String get reportListToday => '今天'; + + @override + String get meshtasticTapNode => '点选节点查看详细信息'; + + @override + String get commonLoading => '加载中…'; + + @override + String get typhoonIntensityModerate => '中度台风'; + + @override + String get typhoonWind => '风速'; + + @override + String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + + @override + String get rainInterval3h => '3 时'; + + @override + String get reportListSearch => '查询'; + + @override + String get mapLayerCategorySatellite => '卫星'; + + @override + String get meshtasticChannelReady => 'DPIP 频道已就绪'; + + @override + String get reportFilterLocation => '地点'; + + @override + String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜间微物理'; + + @override + String get typhoonIntensityTd => '热带性低气压'; + + @override + String get reportFilterDate => '日期'; + + @override + String get sponsorRestoreUnavailable => '无法连接到商店,请稍后再试'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => '尚未添加常用地区'; + + @override + String get onboardingPermBatteryDesc => '允许 DPIP 在后台持续运行,避免预警延迟或漏收。'; + + @override + String get mapNavDisaster => '防灾'; + + @override + String get radarScanRangeSubtitle => '标示四座雷达实际观测到的范围。'; + + @override + String get aedHoursSunday => '周日开放时间'; + + @override + String get reportDetailOriginTime => '发震时间'; + + @override + String get trendNoData => '没有趋势数据'; + + @override + String get onboardingPermLocation => '定位'; + + @override + String get moreDiscord => 'Discord 社区'; + + @override + String get mapNavPressure => '气压'; + + @override + String get mapLayerSatelliteB13 => 'ひまわり 红外线(B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => '目前没有更新日志'; + + @override + String get reportFilterDateStartNote => '开始日:当日 00:00(台北时间)'; + + @override + String get eewTitle => '地震预警'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '已选 $count/$max'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/云相'; + + @override + String get meshtasticStateError => '錯誤'; + + @override + String get weatherModeOvercast => '阴天'; + + @override + String get reportDetailDepth => '震源深度'; + + @override + String get typhoonOverlayWarningTooltip => '标示发布台风警报的县市'; + + @override + String get reportFilterDatePick => '选择日期'; + + @override + String get onboardingSkipStay => '返回授权'; + + @override + String get commonFetchFailed => '无法获取数据,请稍后重试'; + + @override + String get shelterOutdoorLabel => '室外收容'; + + @override + String get meshtasticStateConnected => '已連線'; + + @override + String get mapNavRadar => '雷达'; + + @override + String get mapLayerSatelliteCloudClear => '晴空'; + + @override + String eewSummary(String magnitude, String depth) { + return '震级 $magnitude·深度 $depth 公里'; + } + + @override + String get locationBannerPermission => '尚未授予定位权限,无法向你所在的区域推送本地预警。'; + + @override + String get typhoonOverlayWeatherNoneTooltip => '不显示雷达或红外线底图'; + + @override + String get radarCountyOutlineHint => '画在回波之上'; + + @override + String get windForecastCountyOutlineHint => '绘制于风场之上'; + + @override + String get homeRainTrendTitle => '近 1 小时降水趋势'; + + @override + String get moonPhaseFirstQuarter => '上弦月'; + + @override + String get mapLayerCategoryTyphoon => '台风'; + + @override + String get meshtasticUtilization => '空中工时(24 小时)'; + + @override + String get restroomTypeMixed => '混合厕所'; + + @override + String get restroomGradeGood => '优等级'; + + @override + String get notifyTsunami => '海啸信息'; + + @override + String get navData => '资料'; + + @override + String get mapLayerSatelliteBtdWvirw => 'ひまわり 过冲云顶'; + + @override + String get meshtasticReadingAge => '数值时间'; + + @override + String get mapAppCallFailed => '此设备无法拨打电话'; + + @override + String get reportFilterAny => '不限'; + + @override + String get weatherRankingMergeTo => '合并至'; + + @override + String get notifyIntensity => '震度速报'; + + @override + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; + } + + @override + String get rainIntervalMenu => '累积时段'; + + @override + String get reportDetailLocalFelt => '小区域有感地震'; + + @override + String get meshtasticDevice => '设备'; + + @override + String get onboardingGrant => '授权'; + + @override + String get weatherModeRain => '雨天'; + + @override + String get shelterVulnerableOkLabel => '适合避难弱者安置'; + + @override + String get stationSheetEmpty => '点选任一测站查看观测值'; + + @override + String get typhoonLegendProbability => '侵袭概率'; + + @override + String get reportFilterMagnitude => '规模'; + + @override + String get skyTimeMorning => '上午'; + + @override + String get experimentalFeatures => '实验性功能'; + + @override + String get onboardingTermsBody => + '使用 DPIP 前,请详细阅读以下注意事项:\n\n• 任何信息均应以中央气象署(CWA)发布的内容为准。\n\n• 受网络状态、服务器状态、应用程序状态、上游数据来源状态等因素影响,存在收不到信息的可能,我们会尽力避免此类情况,但不保证一定不会发生。\n\n• 强烈震动有可能比通知更早抵达您所在的位置。\n\n• 地震预警为快速计算的结果,可能存在较大误差,请理解并谨慎使用。\n\n• 任何未获官方认可的行为均可能承担法律风险,请务必遵守相关规定。\n\n此外,为提供本地化预警,本服务会在前台及后台收集并上传您的大致位置与设备推送标识符,仅用于决定应向您推送哪些预警。\n\n点击下方“同意并继续”即表示您已阅读、理解并同意上述事项。'; + + @override + String get reportFilterTitle => '筛选'; + + @override + String get onboardingPermCritical => '重要警告'; + + @override + String trendCumulativeTotal(String total) { + return '累计 $total mm'; + } + + @override + String get languageName => '简体中文'; + + @override + String get reportListEmptyFiltered => '没有符合条件的地震报告'; + + @override + String get meshtasticExcludeMqtt => '隐藏 MQTT 节点'; + + @override + String get mapNavTyphoon => '台风'; + + @override + String get weatherModeSand => '沙尘'; + + @override + String get typhoonSatelliteTitle => '卫星云图'; + + @override + String get notifyReport => '地震报告'; + + @override + String get mapAppCoordinatesCopied => '已复制坐标'; + + @override + String get skyTimeNight => '夜晚'; + + @override + String get sponsorRecommended => '推荐'; + + @override + String get mapLayerSatelliteB15 => 'ひまわり 长波红外线(B15)'; + + @override + String get weatherRankingWind => '风速'; + + @override + String get feedStale => '数据可能已过期'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · $level 级'; + } + + @override + String get navHome => '主页'; + + @override + String get meshtasticRegionLabel => '地区'; + + @override + String get mapLayerSatelliteCloudtop => 'ひまわり 云顶温度'; + + @override + String get moonTimelineCaption => '月相'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth 公里'; + } + + @override + String get openSourceLicenses => '开源许可'; + + @override + String get weatherRankingLowest => '最低'; + + @override + String get reportFilterSortDepth => '深度'; + + @override + String mapTimelineDataTime(String time) { + return '资料时间 $time'; + } + + @override + String get radarScanRange => '显示扫描范围'; + + @override + String get meshtasticHopLimit => '跳数上限'; + + @override + String weatherRankingAnalysisRange(String value) { + return '温差 $value°C'; + } + + @override + String get weatherRankingExtremeHigh => '今日最高'; + + @override + String get changelogVersionDetails => '版本信息'; + + @override + String get sponsorPrivacy => '隐私政策'; + + @override + String get reportDetailLocalIntensity => '所在地的震度'; + + @override + String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; + + @override + String get meshtasticAirtime => '发射占空比'; + + @override + String shelterCapacityValue(int n) { + return '$n 人'; + } + + @override + String lightningLegendCc(int minutes) { + return '云间 · $minutes 分钟内'; + } + + @override + String get meshtasticSendHint => '要廣播的訊息'; + + @override + String monitorDelay(String value) { + return '延迟 $value s'; + } + + @override + String get dpmNo => '否'; + + @override + String get mapLayerSatelliteB08 => 'ひまわり 上层水气(B08)'; + + @override + String get meshtasticReconnecting => '重新连线中…'; + + @override + String get radarTownOutlineSubtitle => '让乡镇界线在雷达回波下仍然清楚。'; + + @override + String get typhoonOverlayWeatherSatelliteTooltip => '最接近台风报文时间的红外线'; + + @override + String get radarScanRangeHint => '框外空白代表未观测'; + + @override + String typhoonPickerTd(String no) { + return '热带性低气压 TD $no'; + } + + @override + String get mapLayerSatelliteWatervapor => 'ひまわり 水气'; + + @override + String get regionAddButton => '添加地区'; + + @override + String get displaySettings => '显示设置'; + + @override + String get restroomGradePoor => '不合格'; + + @override + String get restroomCategoryTourist => '观光地区及风景区'; + + @override + String get locationBannerServiceOff => '定位服务已关闭,无法向你所在的区域推送本地预警。'; + + @override + String get mapLayerStyleTooltip => '显示样式'; + + @override + String lightningLegendCg(int minutes) { + return '对地 · $minutes 分钟内'; + } + + @override + String get skyTimeAuto => '自动'; + + @override + String get appLogs => '应用日志'; + + @override + String get feedConnecting => '连接中…'; + + @override + String get notifyBannerDisabled => '通知已关闭,将收不到灾害警报。'; + + @override + String get weatherHumidity => '湿度'; + + @override + String typhoonValueMs(String n) { + return '每秒 $n 公尺'; + } + + @override + String homeForecastHumidity(String value) { + return '湿度 $value%'; + } + + @override + String get meshtasticBusyBody => + '请先在另一个 Meshtastic App 中断线。两个 App 同时连同一台设备会互相抢走讯息,导致部分讯息遗失。'; + + @override + String get meshtasticChannelNoSlot => '没有可用的频道空位 — 请先在设备上空出一个'; + + @override + String get restroomCategoryTransport => '交通'; + + @override + String get reportFilterLocationHint => '例如:花莲、东部海域'; + + @override + String get moonSubtitle => '月相與亮度 — 完全本地計算'; + + @override + String get meshtasticBattery => '电量'; + + @override + String get meshtasticDistance => '距离'; + + @override + String get meshtasticSnrTrend => '信号趋势 (SNR)'; + + @override + String get meshtasticBatteryTrend => '电量趋势'; + + @override + String get typhoonOverlayMenuTooltip => '台风图层选项'; + + @override + String get mapLayerSatelliteBtdOzone => 'ひまわり 对流层顶'; + + @override + String meshtasticRegionMismatch(String region) { + return '设备地区为 $region — DPIP 需要 TW'; + } + + @override + String get notifySectionEarthquake => '地震'; + + @override + String get mapLayerDisasterMap => '防灾地图'; + + @override + String get weatherModeFog => '大雾'; + + @override + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } + + @override + String get mapLayerStyleGrayTooltip => '气象厅灰度惯例:温度越低越白'; + + @override + String get moreAnnouncements => '公告'; + + @override + String get mapLayerSatelliteTransparentNoData => '无资料(陆地) = 透明'; + + @override + String get restroomCategoryGovernment => '民众洽公场所'; + + @override + String get typhoonLegendCurrent => '目前中心'; + + @override + String get aedAddress => '地址'; + + @override + String get mapLayerAed => 'AED'; + + @override + String get changelogTypePrerelease => '公测'; + + @override + String get reportFilterIntensityInfoModernBody => + '震度为 0–4、5弱、5强、6弱、6强、7。筛选滑杆依新制;列表中较早的地震会以旧制标示显示。'; + + @override + String get typhoonOverlayWeatherNone => '无'; + + @override + String get mapLayerStyleGray => '灰度(JMA)'; + + @override + String get weatherModeAuto => '自动'; + + @override + String get typhoonLabelProbCircle => '70%概率圆'; + + @override + String get notifyOptAll => '接收全部'; + + @override + String get displayTheme => '主题'; + + @override + String get mapLayerSatelliteB07 => 'ひまわり 短波红外(B07)'; + + @override + String get typhoonLabelDirection => '过去移动方向'; + + @override + String get regionManageTitle => '常用地区'; + + @override + String get typhoonLegendCone => '预测圆锥'; + + @override + String get moreCwaEew => '中央气象署地震预警'; + + @override + String get onboardingPermsTitle => '权限授权'; + + @override + String get mapLayerStyleJma => '云顶强调(JMA)'; + + @override + String get rainInterval10m => '10 分'; + + @override + String weatherRankingAnalysisLow(String value) { + return '最低 $value'; + } + + @override + String get meshtasticConnectAnyway => '仍要连线'; + + @override + String reportListDayCount(int count) { + return '$count'; + } + + @override + String get mapLayerSatelliteB06 => 'ひまわり 近红外(B06)'; + + @override + String get mapLayerSatelliteTransparentReflectance => '低反射率/夜间 = 透明,显示底图'; + + @override + String chartHourLabel(int hour) { + return '$hour时'; + } + + @override + String get mapLayerShelter => '避难收容场所'; + + @override + String get typhoonOverlayProbabilityTooltip => '显示侵袭概率(隐藏预测圆锥)'; + + @override + String get mapLayerSatelliteNdwi => 'ひまわり 水体指数'; + + @override + String get disasterMapOverlayShelterTooltip => '显示避难收容场所'; + + @override + String get mapNavHumidity => '湿度'; + + @override + String get reportDetailSortByIntensity => '依震度排序'; + + @override + String get homeRainTrendNoData => '无资料'; + + @override + String get mapLayerCategoryRadar => '雷达'; + + @override + String get meshtasticShortName => '简称'; + + @override + String get mapLayerSatelliteAirmass => 'ひまわり 气团'; + + @override + String get typhoonTrackDetail => '路径详情'; + + @override + String get dataSectionWeather => '气象'; + + @override + String get aedHoursWeekday => '平日开放时间'; + + @override + String get homeActiveEventsTitle => '生效中事件'; + + @override + String weatherRankingAnalysisHigh(String value) { + return '最高 $value'; + } + + @override + String get faq => '常见问题'; + + @override + String get typhoonHistoryLive => '实时'; + + @override + String eewSerial(int serial) { + return '第 $serial 报'; + } + + @override + String get reportFilterSort => '排序方式'; + + @override + String get meshtasticRegionConfirm => + '要将这台设备切换为 TW 地区吗?设备会重新启动并短暂断线,上面的其他频道也会一起改变。'; + + @override + String get dataEarthquakeSubtitle => '地震报告'; + + @override + String get typhoonNoActive => '目前无台风'; + + @override + String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/云相(B11)'; + + @override + String get navEvents => '事件'; + + @override + String get onboardingTermsTitle => '服务条款'; + + @override + String get mapTownLabels => '乡镇名称'; + + @override + String get notifySetFailed => '设置失败,请稍后再试。'; + + @override + String get meshtasticDisconnect => '斷線'; + + @override + String get meshtasticUndecoded => '无法解密'; + + @override + String get notifyAnnouncement => '公告'; + + @override + String get onboardingIntroTitle => '欢迎使用 DPIP'; + + @override + String get regionCurrentUnavailable => '无法获取所在地位置信息'; + + @override + String get languageSystem => '系统默认'; + + @override + String get skyTimeSunset => '日落'; + + @override + String get mapLayerSatelliteDust => 'ひまわり 沙尘'; + + @override + String get mapAppAppleMaps => 'Apple Maps'; + + @override + String get regionEdit => '修改'; + + @override + String get weatherDynamicState => '天气动画'; + + @override + String get mapPlaceholderDisabled => '地图(暂时禁用)'; + + @override + String get moonNow => '现在'; + + @override + String get moonSectionAppearance => '外观'; + + @override + String get moonSectionRiseSet => '月出月落'; + + @override + String get moonSectionUpcoming => '接下来'; + + @override + String get moonSectionCalendar => '月历'; + + @override + String get moonDistance => '距离'; + + @override + String get moonKilometres => '公里'; + + @override + String get moonApparentSize => '视直径'; + + @override + String get moonRise => '月出'; + + @override + String get moonSet => '月落'; + + @override + String get moonNextNewMoon => '下次新月'; + + @override + String get moonAlwaysUp => '整日在地平线上'; + + @override + String get moonNoEvent => '当日无'; + + @override + String get sunTitle => '太阳'; + + @override + String get sunSubtitle => '日出日落、曙暮光与节气'; + + @override + String get sunSectionDaylight => '日照'; + + @override + String get sunSectionTwilight => '曙暮光'; + + @override + String get sunSectionLight => '光线'; + + @override + String get sunSectionSundial => '日晷'; + + @override + String get sunSectionTerms => '节气'; + + @override + String get sunRise => '日出'; + + @override + String get sunSet => '日落'; + + @override + String get sunNoon => '正午'; + + @override + String get sunDayLength => '白昼长度'; + + @override + String get sunTwilightCivil => '民用'; + + @override + String get sunTwilightNautical => '航海'; + + @override + String get sunTwilightAstronomical => '天文'; + + @override + String get sunGoldenHourMorning => '晨间黄金时刻'; + + @override + String get sunGoldenHourEvening => '昏间黄金时刻'; + + @override + String get sunBlueHour => '蓝调时刻'; + + @override + String get sunEquationOfTime => '均时差'; + + @override + String get sunMinutes => '分'; + + @override + String get solarTermNext => '下一个节气'; + + @override + String get planetsTitle => '行星'; + + @override + String get planetsSubtitle => '今晚在哪、有多亮'; + + @override + String get planetsSectionTonight => '此刻'; + + @override + String get planetUp => '地平线上'; + + @override + String get planetDown => '地平线下'; + + @override + String get planetInGlare => '太近太阳'; + + @override + String get planetMagnitude => '亮度'; + + @override + String get planetElongation => '距日距角'; + + @override + String get planetSky => '时段'; + + @override + String get planetEvening => '昏星'; + + @override + String get planetMorning => '晨星'; + + @override + String get planetDistance => '距离'; + + @override + String get planetAu => '天文单位'; + + @override + String get planetAltitude => '仰角'; + + @override + String get planetMercury => '水星'; + + @override + String get planetVenus => '金星'; + + @override + String get planetMars => '火星'; + + @override + String get planetJupiter => '木星'; + + @override + String get planetSaturn => '土星'; + + @override + String get planetUranus => '天王星'; + + @override + String get planetNeptune => '海王星'; + + @override + String get solarTermVernalEquinox => '春分'; + + @override + String get solarTermPureBrightness => '清明'; + + @override + String get solarTermGrainRain => '谷雨'; + + @override + String get solarTermStartOfSummer => '立夏'; + + @override + String get solarTermGrainFull => '小满'; + + @override + String get solarTermGrainInEar => '芒种'; + + @override + String get solarTermSummerSolstice => '夏至'; + + @override + String get solarTermMinorHeat => '小暑'; + + @override + String get solarTermMajorHeat => '大暑'; + + @override + String get solarTermStartOfAutumn => '立秋'; + + @override + String get solarTermEndOfHeat => '处暑'; + + @override + String get solarTermWhiteDew => '白露'; + + @override + String get solarTermAutumnalEquinox => '秋分'; + + @override + String get solarTermColdDew => '寒露'; + + @override + String get solarTermFrostDescent => '霜降'; + + @override + String get solarTermStartOfWinter => '立冬'; + + @override + String get solarTermMinorSnow => '小雪'; + + @override + String get solarTermMajorSnow => '大雪'; + + @override + String get solarTermWinterSolstice => '冬至'; + + @override + String get solarTermMinorCold => '小寒'; + + @override + String get solarTermMajorCold => '大寒'; + + @override + String get solarTermStartOfSpring => '立春'; + + @override + String get solarTermRainWater => '雨水'; + + @override + String get solarTermAwakeningOfInsects => '惊蛰'; + + @override + String get tonightTitle => '今夜'; + + @override + String get tonightSubtitle => '现在看得到什麼、什麼时候'; + + @override + String get tonightSectionDark => '观测窗口'; + + @override + String get tonightAstronomicalNight => '天文夜'; + + @override + String get tonightNeverDark => '整夜不全暗'; + + @override + String get tonightDarkWindow => '暗窗'; + + @override + String get tonightMoonAllNight => '月亮整夜在天上'; + + @override + String get tonightDarkTotal => '總暗时'; + + @override + String get tonightMoonlight => '月光'; + + @override + String get tonightSectionShowers => '流星雨'; + + @override + String get tonightRadiantDown => '輻射点不升起'; + + @override + String get tonightPerHour => '颗/时'; + + @override + String get tonightSectionSatellites => '卫星过境'; + + @override + String get tonightSectionTargets => '此刻可观测目標'; + + @override + String get showerQuadrantids => '象限儀座'; + + @override + String get showerLyrids => '天琴座'; + + @override + String get showerEtaAquariids => '寶瓶座η'; + + @override + String get showerDeltaAquariids => '寶瓶座δ'; + + @override + String get showerPerseids => '英仙座'; + + @override + String get showerOrionids => '獵戶座'; + + @override + String get showerSouthernTaurids => '金牛座南'; + + @override + String get showerLeonids => '獅子座'; + + @override + String get showerGeminids => '雙子座'; + + @override + String get showerUrsids => '小熊座'; + + @override + String get deepSkyOpenCluster => '疏散星团'; + + @override + String get deepSkyGlobularCluster => '球狀星团'; + + @override + String get deepSkySpiralGalaxy => '螺旋星系'; + + @override + String get deepSkyEllipticalGalaxy => '椭圆星系'; + + @override + String get deepSkyIrregularGalaxy => '不規则星系'; + + @override + String get deepSkyPlanetaryNebula => '行星狀星云'; + + @override + String get deepSkySupernovaRemnant => '超新星遗迹'; + + @override + String get deepSkyEmissionNebula => '发射星云'; + + @override + String get deepSkyReflectionNebula => '反射星云'; + + @override + String get deepSkyAsterism => '星群'; + + @override + String get almanacTitle => '历法'; + + @override + String get almanacSubtitle => '农历日期与未来的日月食'; + + @override + String get almanacSectionToday => '今日'; + + @override + String get almanacGregorian => '西历'; + + @override + String get almanacLunar => '农历'; + + @override + String get almanacYear => '岁次'; + + @override + String get almanacMonthLength => '月大小'; + + @override + String get almanacLongMonth => '三十日'; + + @override + String get almanacShortMonth => '二十九日'; + + @override + String get almanacLeapPrefix => '闰'; + + @override + String get almanacSectionLunarEclipses => '月食'; + + @override + String get almanacSectionSolarEclipses => '日食'; + + @override + String get almanacNoSolarEclipse => '范围內無'; + + @override + String get eclipseTotal => '全食'; + + @override + String get eclipsePartial => '偏食'; + + @override + String get eclipseAnnular => '环食'; + + @override + String get eclipsePenumbral => '半影食'; + + @override + String get zodiacRat => '鼠'; + + @override + String get zodiacOx => '牛'; + + @override + String get zodiacTiger => '虎'; + + @override + String get zodiacRabbit => '兔'; + + @override + String get zodiacDragon => '龙'; + + @override + String get zodiacSnake => '蛇'; + + @override + String get zodiacHorse => '马'; + + @override + String get zodiacGoat => '羊'; + + @override + String get zodiacMonkey => '猴'; + + @override + String get zodiacRooster => '鸡'; + + @override + String get zodiacDog => '狗'; + + @override + String get zodiacPig => '猪'; + + @override + String get tideTitle => '潮汐'; + + @override + String get tideSubtitle => '大潮、小潮与月球引力'; + + @override + String get tideDisclaimer => '仅为天文引潮力,非港口潮汐表。水位請参考气象署公布之潮汐预报。'; + + @override + String get tideSectionNow => '此刻'; + + @override + String get tidePhase => '周期'; + + @override + String get tideSpring => '大潮'; + + @override + String get tideNeap => '小潮'; + + @override + String get tideMiddling => '中潮'; + + @override + String get tideLunarDistanceFactor => '月球引力'; + + @override + String get tideEquilibrium => '平衡潮高'; + + @override + String get tideMetres => '公尺'; + + @override + String get tidePerigeanSpring => '下次近地点大潮'; + + @override + String get tideSectionTurningPoints => '转折点'; + + @override + String get tideHigh => '高'; + + @override + String get tideLow => '低'; + + @override + String get skyChartTitle => '星图'; + + @override + String get skyChartSubtitle => '头頂上肉眼可见的天空'; + + @override + String get skyChartNorth => '北'; + + @override + String get skyChartEast => '东'; + + @override + String get skyChartSouth => '南'; + + @override + String get skyChartWest => '西'; + + @override + String tonightElementAge(int days) { + return '轨道数据 $days 天前'; + } + + @override + String almanacLunarDate(String leap, int month, int day) { + return '$leap$month 月 $day 日'; + } + + @override + String get tonightNoShowers => '目前无流星雨'; + + @override + String get tonightNoPasses => '48 小时内无可见过境'; + + @override + String get tonightSatellitesUnavailable => '无法读取轨道数据'; + + @override + String get tonightNoTargets => '无足够高度的目标'; + + @override + String get skyChartUnavailable => '无法读取星表'; +} + +/// The translations for Chinese, as used in Hong Kong, using the Han script (`zh_Hant_HK`). +class AppLocalizationsZhHantHk extends AppLocalizationsZh { AppLocalizationsZhHantHk() : super('zh_Hant_HK'); @override - String get languageName => '繁體中文(香港)'; + String typhoonValueLat(String lat) { + return '北緯 $lat 度'; + } + + @override + String get onboardingSkipBody => + '未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。'; + + @override + String get rainInterval24h => '24 時'; + + @override + String homeRainTrendHeavyStopping(int minutes) { + return '預計 $minutes 分鐘後停止下大雨'; + } + + @override + String get mapTimelineObserved => '觀測'; + + @override + String get regionSelectTitle => '選擇地區'; + + @override + String get skyTimeNoon => '正午'; + + @override + String get radarCountyOutlineSubtitle => '讓縣市界線在雷達回波下仍然清楚。'; + + @override + String get dpmFilterSectionRestroomType => '廁所類型'; + + @override + String get mapLayerSatelliteB03 => 'ひまわり 可見光-紅(B03)'; + + @override + String get reportFilterIntensity => '震度'; + + @override + String get mapLayerLightning => '閃電'; + + @override + String get restroomTypeMale => '男廁所'; + + @override + String get meshtasticLastReceived => '最近接收'; + + @override + String get reportDetailSortByCounty => '依縣市排序'; + + @override + String get homeRainTrendScattered => '可能會有零星降雨'; + + @override + String get meshtasticUptime => '運行時間'; + + @override + String get weatherRankingTempExtremes => '溫度極值'; + + @override + String get themeLight => '淺色'; + + @override + String get mapTerrainReliefHint => '在底圖上顯示立體地形陰影'; + + @override + String get meshtasticEmptyMessage => '(空白訊息)'; + + @override + String get moreSectionRegion => '地區'; + + @override + String get dpmDisasterEarthquake => '震災'; + + @override + String get mapLayerSatellite => 'ひまわり 紅外線(B13)'; + + @override + String get aedHoursSaturday => '週六開放時間'; + + @override + String get dpmDisasterSlope => '坡地災害'; + + @override + String get moonPhaseNew => '新月'; + + @override + String get notifySectionEew => '地震速報'; + + @override + String get mapResetNorth => '回到北方'; + + @override + String get rainInterval2d => '2 日'; + + @override + String get mapTownLabelsHint => '放大時顯示鄉鎮名稱'; + + @override + String get commonCancel => '取消'; + + @override + String get notifyOptTsunamiWarning => '只接收海嘯警報'; + + @override + String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; + + @override + String get moreSectionAdvanced => '進階'; + + @override + String get weatherRankingExtremeRange => '日溫差'; + + @override + String get notifySettingsMenu => '通知設定'; + + @override + String get typhoonHistoryTitle => '資料時間'; + + @override + String mapAppDefault(String app) { + return '$app(預設)'; + } + + @override + String get trendRange24h => '24 小時'; + + @override + String get mapLayerStyleJmaTooltip => '灰階為底,−40 °C 以下上色,凸顯雲頂高度'; + + @override + String weatherRankingRecordedAt(String time) { + return '記錄於 $time'; + } + + @override + String get mapLayerRain => '雨量'; + + @override + String get mapLayerQpesums => '未來 1 小時降水預報'; + + @override + String get mapOverlaySectionMap => '地圖'; + + @override + String get mapTerrainRelief => '地形立體感'; + + @override + String get eewMaxIntensity => '最大震度'; + + @override + String get mapLegendCollapse => '收合圖例'; + + @override + String get changelogTitle => '更新日誌'; + + @override + String get reportFilterOrderDesc => '降序'; + + @override + String get meshtasticExcludeMqttSubtitle => '經網際網路橋接、並非無線電聽到的節點'; + + @override + String get reportFilterIntensityInfoTitle => '震度新制與舊制'; + + @override + String get mapLayerTyphoon => '颱風'; + + @override + String get radarOverlayMenuTooltip => '雷達圖層選項'; + + @override + String get mapMyLocation => '我的位置'; + + @override + String get meshtasticNodes => '節點'; + + @override + String get meshtasticSend => '傳送'; + + @override + String get typhoonOverlayStormL7Tooltip => '七級暴風圈+平均圓(紫色)'; + + @override + String get aedType => '場所類型'; + + @override + String get termsOfService => '服務條款'; + + @override + String get typhoonLegendCircle25 => '十級風暴風圈'; + + @override + String get sponsorTitle => '支持 DPIP'; + + @override + String get mapNavSatellite => '衛星'; + + @override + String homeRainTrendUpdated(String time) { + return '更新 $time'; + } + + @override + String get onboardingNext => '下一步'; + + @override + String get weatherRankingMergeTown => '鄉鎮'; + + @override + String get mapLayerMonitor => '強震監視器'; + + @override + String get moreYoutube => 'YouTube'; + + @override + String get sponsorSubscriptions => '訂閱制'; + + @override + String typhoonValueLon(String lon) { + return '東經 $lon 度'; + } + + @override + String get skyTime => '天空時間'; + + @override + String get weatherModeCloudy => '多雲'; + + @override + String get skyTimeDusk => '暮色'; + + @override + String get meshtasticFirmware => '韌體'; + + @override + String get reportFilterDateEndNote => '結束日:當日 24:00(台北時間)'; + + @override + String get reportFilterSortMagnitude => '規模'; + + @override + String get meshtasticSilent => '已靜默'; + + @override + String get mapLayerCategoryEarthquake => '地震'; + + @override + String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; + + @override + String get typhoonLegendPast => '實際路徑'; + + @override + String get restroomCategoryOther => '其他'; + + @override + String homeForecastHighLow(String high, String low) { + return '高 $high° · 低 $low°'; + } + + @override + String get locationBannerFix => '開啟設定'; + + @override + String get mapLegendExpand => '圖例'; + + @override + String get eewNone => '目前沒有地震速報'; + + @override + String typhoonTyNo(String no) { + return 'TY $no'; + } + + @override + String get notifyOptTsunamiAll => '海嘯消息、海嘯警報'; + + @override + String get meshtasticLayerOptions => '節點選項'; + + @override + String get onboardingAgreeContinue => '同意並繼續'; + + @override + String get commonRetry => '重試'; + + @override + String get meshtasticNodeId => '節點 ID'; + + @override + String reportDetailNumbered(String number) { + return '編號 $number 顯著有感地震'; + } + + @override + String get typhoonOverlayStormBandSubtitle => '含平均圓'; + + @override + String get disasterMapOverlayRestroomTooltip => '顯示公廁'; + + @override + String get weatherRankingTitle => '觀測排行'; + + @override + String get homeRainTrendHeavySustained => '未來 1 小時會有持續大雨'; + + @override + String get notifySectionTsunami => '海嘯'; + + @override + String get restroomCategoryPark => '公園'; + + @override + String get moreLinkOpenFailed => '無法開啟連結'; + + @override + String get themeDark => '深色'; + + @override + String get sponsorRestore => '恢復購買'; + + @override + String get meshtasticChannelWorking => '正在設定 DPIP 頻道…'; + + @override + String get meshtasticRegionSwitch => '切換為 TW'; + + @override + String get meshtasticTraffic => '流量'; + + @override + String get mapLayerStyleBdTooltip => 'Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階'; + + @override + String get disasterMapOverlayAedTooltip => '顯示 AED 位置'; + + @override + String get mapLayerHumidity => '濕度'; + + @override + String get mapLayerSatelliteTransparentNight => '夜間 = 透明,顯示底圖'; + + @override + String get meshtasticScanning => '掃描中…'; + + @override + String regionSelectFull(int max) { + return '最多只能選擇 $max 個地區'; + } + + @override + String get meshtasticTitle => 'Meshtastic'; + + @override + String get navMore => '更多'; + + @override + String get meshtasticDpipChannel => 'DPIP 頻道'; + + @override + String get disasterMapOverlaySectionLayers => '圖層'; + + @override + String get mapLayerSatelliteB05 => 'ひまわり 近紅外(B05)'; + + @override + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return '東北 $ne · 東南 $se · 西南 $sw · 西北 $nw km'; + } + + @override + String get typhoonLabelNe => '東北側'; + + @override + String get meshtasticCopied => '已複製訊息'; + + @override + String get reportListEmpty => '目前沒有地震報告'; + + @override + String get reportListEnd => '已到最後一頁'; + + @override + String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; + + @override + String get typhoonOverlaySectionExtra => '覆蓋層'; + + @override + String get eewSWave => '震波'; + + @override + String get meshtasticBusyTitle => '另一個 App 正在使用這台裝置'; + + @override + String get restroomCategoryCultural => '文化育樂活動場所'; + + @override + String get typhoonLabelWind => '近中心最大風速'; + + @override + String get radarGlobalOutlineHint => '各國國界外框'; + + @override + String get notifyEvacuation => '防災資訊'; + + @override + String get typhoonLegendCircle15 => '七級風暴風圈'; + + @override + String get dataSectionAstronomy => '天文'; + + @override + String get homeRainTrendLightSustained => '未來 1 小時會有持續小雨'; + + @override + String get commonError => '發生錯誤'; + + @override + String get moonPhaseWaningCrescent => '殘月'; + + @override + String get meshtasticPower => '電力'; + + @override + String get mapTimelineNow => '現在'; + + @override + String reportFilterRange(String start, String end) { + return '$start – $end'; + } + + @override + String get reportDetailOpenReport => '報告頁面'; + + @override + String get trendRange7d => '7 天'; + + @override + String typhoonWarningAreas(String areas) { + return '警戒區域:$areas'; + } + + @override + String get rainIntervalSection => '統計時間'; + + @override + String get notifyTitle => '通知'; + + @override + String get meshtasticTxPower => '發射功率'; + + @override + String get restroomCategoryLabel => '類別'; + + @override + String get sponsorRestoring => '正在恢復購買…'; + + @override + String get sponsorIntro => + 'DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。'; + + @override + String get shelterAddressLabel => '地址'; + + @override + String get typhoonLabelStormAvg => '十級風平均暴風半徑'; + + @override + String get restroomCategoryCommercial => '商業營業場所'; + + @override + String get aedRegion => '縣市區域'; + + @override + String homeRainTrendLightStopping(int minutes) { + return '預計 $minutes 分鐘後停止下小雨'; + } + + @override + String get reportDetailInfo => '詳細資訊'; + + @override + String get mapNavWind => '風向'; + + @override + String get windForecastOverlayMenuTooltip => '風場預報圖層選項'; + + @override + String get dataWeatherRankingSubtitle => '即時觀測排行'; + + @override + String homeRainTrendMinute(int minute) { + return '$minute分'; + } + + @override + String get rainInterval6h => '6 時'; + + @override + String get restroomTypeUnspecified => '未設定'; + + @override + String get typhoonOverlayProbabilityHint => '會隱藏預測圓錐'; + + @override + String get mapLayerSatelliteGlobalOutline => '國界'; + + @override + String get mapNavTemperature => '溫度'; + + @override + String get typhoonLegendForecastPoint => '預測點'; + + @override + String get reportListYesterday => '昨天'; + + @override + String get moreSectionLinks => '相關連結'; + + @override + String get feedOffline => '連接中斷'; + + @override + String get mapLayerStyleBd => 'Dvorak BD'; + + @override + String get moreSectionDisplay => '顯示'; + + @override + String get rainInterval3d => '3 日'; + + @override + String get defaultMapLayerSubtitle => '開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。'; + + @override + String get aedDescription => '備註'; + + @override + String get typhoonOverlayWeatherRadarTooltip => '雷達回波(對齊颱風報文時間)'; + + @override + String get onboardingPermLocationDesc => '依你所在位置推送本地警報。'; + + @override + String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; + + @override + String get homeActiveEventsEmpty => '目前沒有生效中的事件'; + + @override + String get typhoonLabelPosition => '中心位置'; + + @override + String get weatherRankingBy => '依'; + + @override + String get typhoonIntensityMild => '輕度颱風'; + + @override + String get windForecastGlobalOutlineHint => '各國國界外框'; + + @override + String get rainInterval1h => '1 時'; + + @override + String get eewLocalIntensity => '所在地預估'; + + @override + String get mapLayerRadar => '雷達合成回波圖'; + + @override + String get restroomCategoryReligious => '宗教禮儀場所'; + + @override + String get meshtasticRole => '角色'; + + @override + String get mapLayerSatelliteCloudCloudy => '有雲'; + + @override + String get skyTimeSunrise => '日出'; + + @override + String get meshtasticNoMessages => '尚無訊息'; + + @override + String get onboardingPermNotifyDesc => '在地震、天氣與災害發生時,即時傳遞警報通知。'; + + @override + String get radarTownOutline => '鄉鎮界線'; + + @override + String get mapLayerStyleSection => '顯示樣式'; + + @override + String get disasterMapOverlayMenuTooltip => '防災地圖圖層'; + + @override + String get moreGooglePlay => 'Google Play'; + + @override + String get meshtasticOnline => '近期聽到'; + + @override + String get typhoonLabelSw => '西南側'; + + @override + String typhoonForecastLead(String hours) { + return '預測 +$hours 小時'; + } + + @override + String get dpmDisasterTsunami => '海嘯'; + + @override + String get changelogTypeStable => '正式'; + + @override + String get mapLayerSatelliteTransparentClear => '晴空 = 透明,顯示底圖'; + + @override + String get mapOverlaySectionReference => '參考圖層'; + + @override + String get mapLayerSatelliteB02 => 'ひまわり 可見光-綠(B02)'; + + @override + String get reportListLocalFelt => '小區域有感'; + + @override + String get weatherRankingEmpty => '目前沒有可排序的觀測'; + + @override + String get notifySectionOther => '其他'; + + @override + String weatherRankingMeta(String time, int count) { + return '資料時間:$time\n共 $count 觀測點'; + } + + @override + String get onboardingTermsAgree => '我已閱讀並同意服務條款'; + + @override + String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(無植被)'; + + @override + String get notifyOptLocalIntensity4 => '所在地震度4以上'; + + @override + String get eewArrived => '已抵達'; + + @override + String get meshtasticNoDevices => '找不到 Meshtastic 裝置'; + + @override + String get mapLayerCategoryLife => '生活'; + + @override + String get reportFilterSortIntensity => '震度'; + + @override + String get typhoonMotion => '移動'; + + @override + String get meshtasticStateDisconnected => '未連線'; + + @override + String get typhoonIntensityIntense => '強烈颱風'; + + @override + String get mapLayerOrderTitle => '調整圖層順序'; + + @override + String get dpmYes => '是'; + + @override + String get meshtasticNoHistory => '歷史紀錄還不夠'; + + @override + String get reportDetailLocalIntensityUnavailable => '沒有震度訊息'; + + @override + String get mapLayerWindForecastGfs => 'GFS'; + + @override + String get reportListDepthUnit => '公里'; + + @override + String get reportFilterDepth => '深度'; + + @override + String get onboardingScrollHint => '向下捲動以繼續'; + + @override + String get mapNavQpesums => '預報'; + + @override + String get navMap => '地圖'; + + @override + String get notifyAdvisory => '天氣警告及特報'; + + @override + String get reportFilterReset => '重設'; + + @override + String get mapLayerSatelliteMndwi => 'ひまわり 改良水體指數'; + + @override + String get typhoonOverlaySectionStorm => '暴風圈'; + + @override + String get moonPhaseFull => '滿月'; + + @override + String get moonPhaseWaningGibbous => '虧凸月'; + + @override + String get weatherDynamicStateSubtitle => '覆寫主頁背景天氣'; + + @override + String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; + + @override + String typhoonDataTime(String time) { + return '資料時間\n$time'; + } + + @override + String get restroomTypeAccessible => '無障礙廁所'; + + @override + String get moreSectionAbout => '關於'; + + @override + String get meshtasticSelectDevice => '選擇裝置'; + + @override + String get onboardingIntroBody => + 'DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷暴即時訊息、天氣警告及特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。'; + + @override + String get shelterCapacityLabel => '收容人數'; + + @override + String get reportDetailImage => '地震報告圖'; + + @override + String get meshtasticStateConfiguring => '設定中…'; + + @override + String get typhoonLabelGaleAvg => '七級風平均暴風半徑'; + + @override + String get onboardingPermNotify => '通知'; + + @override + String get meshtasticClearMessages => '清除訊息'; + + @override + String get meshtasticNotifyMessages => '新訊息通知'; + + @override + String get defaultMapLayerSettings => '地圖預設圖層'; + + @override + String get moreSectionNotify => '通知'; + + @override + String get notifyUnavailable => '推送尚未就緒,請稍後再試。'; + + @override + String get mapLayerOrderReset => '回復預設順序'; + + @override + String get dpmAddress => '地址'; + + @override + String get weatherRankingMergeCounty => '縣市'; + + @override + String get moreSectionApp => '取得 App'; + + @override + String get reportFilterIntensityInfoLegacyBody => '震度僅 0–7,沒有 5弱/5強/6弱/6強。'; + + @override + String get mapLayerSatelliteSst => 'ひまわり 海表溫度'; + + @override + String get qpesumsOverlayMenuTooltip => '定量降水預報圖層選項'; + + @override + String get mapTimelineFuture => '未來'; + + @override + String get typhoonLegendCircleAvg => '平均圓'; + + @override + String reportFilterDepthKm(String depth) { + return '$depth 公里'; + } + + @override + String get typhoonLabelSe => '東南側'; + + @override + String get radarTownOutlineHint => '較細的分區'; + + @override + String eewCountdown(int seconds) { + return '$seconds 秒'; + } + + @override + String get typhoonLabelGust => '瞬間最大陣風'; + + @override + String get mapAppGoogleMaps => 'Google Maps'; + + @override + String get sponsorTerms => '使用條款'; + + @override + String get restroomTypeGenderNeutral => '性別友善廁所'; + + @override + String get notifyThunderstorm => '雷暴即時訊息'; + + @override + String get skyTimeGolden => '黃金時刻'; + + @override + String get moonAge => '月齡'; + + @override + String get meshtasticRadioSettings => 'LoRa'; + + @override + String weatherRankingAnalysisCurrent(String value) { + return '當下 $value°C'; + } + + @override + String get moreGithub => 'ExpTech GitHub'; + + @override + String get homeForecastUnavailable => '選擇地區後可查看預報'; + + @override + String get mapLayers => '圖層'; + + @override + String get meshtasticHardware => '硬體'; + + @override + String get languageSettings => '語言設定'; + + @override + String get dpmDisasterNuclear => '核子事故'; + + @override + String get language => '語言'; + + @override + String homeForecastFeelsLike(String temp) { + return '體感 $temp°'; + } + + @override + String get typhoonOverlayWeatherHint => '對齊報文時間'; + + @override + String get skyTimeDawn => '黎明'; + + @override + String get skyTimeAfternoon => '下午'; + + @override + String get meshtasticLastHeard => '最後聽到'; + + @override + String get typhoonWarningTitle => '颱風警報'; + + @override + String get moreSourceCode => '原始碼'; + + @override + String get mapLayerCategoryWeather => '氣象觀測'; + + @override + String get mapLayerSatelliteB09 => 'ひまわり 中層水氣(B09)'; + + @override + String get windForecastTownOutlineHint => '更細的網格'; + + @override + String get mapLayerSatelliteCloudmask => 'ひまわり 雲遮罩'; + + @override + String get mapAppCopyCoordinates => '複製座標'; + + @override + String get reportFilterIntensityInfoIntro => + '中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。'; + + @override + String get mapNavEarthquake => '地震'; + + @override + String get typhoonGust => '陣風'; + + @override + String get restroomGradeAverage => '普通級'; + + @override + String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷雲/雲高'; + + @override + String get onboardingPermBackgroundDesc => '選擇「一律允許」,關閉 App 也能推送本地警報。'; + + @override + String get mapTimelineForecast => '預報'; + + @override + String get restroomTypeLabel => '廁所類型'; + + @override + String get navEarthquake => '地震'; + + @override + String get typhoonOverlayStormL10Tooltip => '十級暴風圈+平均圓(黃色)'; + + @override + String get moonPhaseWaxingGibbous => '盈凸月'; + + @override + String get reportDetailTitle => '地震報告'; + + @override + String get moreTremReport => 'TREM 偵測報告'; + + @override + String weatherDataTime(String station, String time) { + return '$station ∙ 資料時間 $time'; + } + + @override + String get meshtasticNoNodes => '尚未聽到任何節點'; + + @override + String get meshtasticViaMqtt => '經 MQTT(網際網路)'; + + @override + String get radarCountyOutline => '縣市界線'; + + @override + String get onboardingGranted => '已授權'; + + @override + String get commonClose => '關閉'; + + @override + String get restroomGradeLabel => '等級'; + + @override + String get rainIntervalNow => '今日'; + + @override + String get changelogCurrentVersion => '目前版本'; + + @override + String get typhoonLabelPressure => '中心氣壓'; + + @override + String get typhoonOverlayForecastCalloutsTooltip => '放大時顯示預測點詳細卡片'; + + @override + String get aedOpenRemark => '開放時間備註'; + + @override + String get onboardingPermsBody => '為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中更改。'; + + @override + String get typhoonOverlaySectionWeather => '天氣底圖'; + + @override + String get notifyOptWeatherLocal => '接收所在地'; + + @override + String get mapNavRain => '雨量'; + + @override + String get moonDays => '天'; + + @override + String mapLegendUnit(String unit) { + return '單位:$unit'; + } + + @override + String get weatherModeClear => '晴天'; + + @override + String get meshtasticRadio => '電台'; + + @override + String get commonEmpty => '沒有資料'; + + @override + String get mapLayerSatelliteB01 => 'ひまわり 可見光-藍(B01)'; + + @override + String get meshtasticExternalPower => '外部供電'; + + @override + String get moonPhaseLastQuarter => '下弦月'; + + @override + String get reportFilterOrderAsc => '升序'; + + @override + String get reportFilterApply => '套用'; + + @override + String get reportDetailImageUnavailable => '報告圖尚未提供'; + + @override + String get weatherRankingHighest => '最高'; + + @override + String get reportDetailReplay => '重播'; + + @override + String get mapLayerRestroom => '公廁'; + + @override + String get restroomCategoryWelfare => '社福機構、集會場所'; + + @override + String get restroomGradeExcellent => '特優級'; + + @override + String get meshtasticLastSent => '最近送出'; + + @override + String get meshtasticName => '名稱'; + + @override + String get meshtasticScan => '掃描'; + + @override + String get mapLayerCategoryForecast => '數值預報'; + + @override + String get meshtasticChannelFailed => '無法設定 DPIP 頻道'; + + @override + String get themeSystem => '跟隨系統'; + + @override + String get mapLayerSatelliteNdvi => 'ひまわり 植生指數'; + + @override + String get typhoonLegendForecast => '預測路徑'; + + @override + String typhoonValueHpa(String n) { + return '$n 百帕'; + } + + @override + String get weatherPrecipitation => '降水量'; + + @override + String get moonNextFullMoon => '下次滿月'; + + @override + String get dpmSheetEmpty => '點選地圖上的標記查看詳情'; + + @override + String get onboardingSkipLeave => '仍要略過'; + + @override + String get onboardingBack => '上一步'; + + @override + String get aedPlaceDesc => '放置位置說明'; + + @override + String get onboardingSkipTitle => '尚未完成授權'; + + @override + String get restroomTypeFamily => '親子廁所'; + + @override + String typhoonValueKm(String n) { + return '$n 公里'; + } + + @override + String get typhoonPressure => '氣壓'; + + @override + String get onboardingPermBattery => '省電白名單'; + + @override + String get typhoonLabelNw => '西北側'; + + @override + String get dpmDisasterFlood => '水災'; + + @override + String get moonPhaseWaxingCrescent => '眉月'; + + @override + String get restroomCategoryLeisure => '休閒娛樂場所'; + + @override + String get mapLayerTemperature => '溫度'; + + @override + String get aedCategory => '場所分類'; + + @override + String get meshtasticChannels => '頻道'; + + @override + String get monitorWaiting => '等待資料…'; + + @override + String get typhoonOverlayForecastCallouts => '預測點資訊'; + + @override + String get reportDetailEpicenter => '震央座標'; + + @override + String get meshtasticVoltage => '電壓'; + + @override + String get mapLayerMeshtasticSubtitle => '電台聽到過的 LoRa 網狀網路節點'; + + @override + String get mapLayerWind => '風向'; + + @override + String get reportDetailMagnitude => '地震規模'; + + @override + String get reportDetailAreaIntensity => '各地震度'; + + @override + String get rainInterval12h => '12 時'; + + @override + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } + + @override + String get dpmDisasterLandslide => '土石流'; + + @override + String get notifyMonitor => '強震監視器'; + + @override + String get onboardingStart => '開始使用'; + + @override + String sponsorPerMonth(String price) { + return '$price / 月'; + } + + @override + String get mapLayerPressure => '氣壓'; + + @override + String get mapLayerSatelliteB04 => 'ひまわり 近紅外(B04)'; + + @override + String get mapLayerSatelliteTransparentZero => '零差值 = 透明(無訊號)'; + + @override + String get shelterIndoorLabel => '室內收容'; + + @override + String get notifyOptOff => '關閉'; + + @override + String get reportFilterSortTime => '時間'; + + @override + String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; + + @override + String get weatherModeThunderstorm => '雷暴'; + + @override + String get homeViewOnMap => '前往地圖察看'; + + @override + String get reportFilterIntensityInfoLegacyTitle => '舊制(2020 以前)'; + + @override + String get typhoonLabelSpeed => '過去移動時速'; + + @override + String mapAppOpenFailed(String app) { + return '無法開啟 $app'; + } + + @override + String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; + + @override + String get meshtasticReceived => '已接收'; + + @override + String get weatherRankingExtremeLow => '今日最低'; + + @override + String get mapLayerSatelliteB10 => 'ひまわり 低層水氣(B10)'; + + @override + String get mapLayerSatelliteCloudProbablyCloudy => '可能有雲'; + + @override + String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(無水體)'; + + @override + String get shelterCategoryLabel => '適用災害'; + + @override + String get meshtasticStateConnecting => '連線中…'; + + @override + String get moonTitle => '月亮'; + + @override + String get weatherRankingGust => '陣風'; + + @override + String get moreAppStore => 'App Store'; + + @override + String get dpmFilterSectionShelter => '避難所災害類型'; + + @override + String get moreServerStatus => '伺服器狀態'; + + @override + String get notifySectionWeather => '天氣'; + + @override + String get meshtasticPreset => '調變預設'; + + @override + String get dataSectionSeismic => '地震'; + + @override + String get changelogBodyEmpty => '此版本沒有說明。'; + + @override + String get radarGlobalOutline => '國界'; + + @override + String get notifyEew => '緊急地震速報'; + + @override + String get regionNationwide => '全國'; + + @override + String get moreNotifyLog => 'DPIP 通知發送記錄'; + + @override + String get regionCurrent => '所在地'; + + @override + String get dpmFilterSectionRestroom => '場所類型'; + + @override + String get meshtasticNotConnected => '尚未連線至裝置'; + + @override + String get weatherModeSnow => '下雪'; + + @override + String get mapLayerMeshtastic => 'Meshtastic 節點'; + + @override + String get moreDeveloper => '偵錯資訊'; + + @override + String get mapLayerSatelliteB14 => 'ひまわり 長波紅外線(B14)'; + + @override + String get meshtasticChannelUse => '頻道使用率'; + + @override + String get mapNavLightning => '閃電'; + + @override + String get homeForecastEmpty => '目前沒有預報資料'; + + @override + String get sponsorOneTime => '單次支援'; + + @override + String get mapLayerSatelliteBtdSplit => 'ひまわり 分割視窗'; + + @override + String get onboardingPermBackground => '背景定位'; + + @override + String get aedEmergencyPhone => '緊急聯絡電話'; + + @override + String get dpmOpenInMaps => '開啟地圖'; + + @override + String get meshtasticNotifyNodes => '新節點通知'; + + @override + String get onboardingPermCriticalDesc => '讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。'; + + @override + String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,顯示底圖'; + + @override + String get meshtasticSent => '已送出'; + + @override + String get homeForecastTitle => '24小時預報'; + + @override + String get typhoonLegendWarningAreas => '警報區域'; + + @override + String meshtasticExcludeMqttHidden(int count) { + return '已隱藏 $count 個'; + } + + @override + String get notifyOptLocalIntensity1 => '所在地震度1以上'; + + @override + String get mapTimelinePast => '歷史'; + + @override + String get restroomTypeFemale => '女廁所'; + + @override + String get reportListToday => '今天'; + + @override + String get meshtasticTapNode => '點選節點查看詳細資訊'; + + @override + String get commonLoading => '載入中…'; + + @override + String get typhoonIntensityModerate => '中度颱風'; + + @override + String get typhoonWind => '風速'; + + @override + String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + + @override + String get rainInterval3h => '3 時'; + + @override + String get reportListSearch => '查詢'; + + @override + String get mapLayerCategorySatellite => '衛星'; + + @override + String get meshtasticChannelReady => 'DPIP 頻道已就緒'; + + @override + String get reportFilterLocation => '地點'; + + @override + String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; + + @override + String get typhoonIntensityTd => '熱帶性低氣壓'; + + @override + String get reportFilterDate => '日期'; + + @override + String get sponsorRestoreUnavailable => '無法連線至商店,請稍後再試'; + + @override + String homeForecastPop(String pop) { + return '$pop%'; + } + + @override + String get regionEmpty => '尚未新增常用地區'; + + @override + String get onboardingPermBatteryDesc => '允許 DPIP 在背景持續運作,避免警報延遲或漏收。'; + + @override + String get mapNavDisaster => '防災'; + + @override + String get radarScanRangeSubtitle => '標示四座雷達實際觀測到的範圍。'; + + @override + String get aedHoursSunday => '週日開放時間'; + + @override + String get reportDetailOriginTime => '發震時間'; + + @override + String get trendNoData => '沒有趨勢資料'; + + @override + String get onboardingPermLocation => '定位'; + + @override + String get moreDiscord => 'Discord 社群'; + + @override + String get mapNavPressure => '氣壓'; + + @override + String get mapLayerSatelliteB13 => 'ひまわり 紅外線(B13)'; + + @override + String typhoonTdNo(String no) { + return 'TD $no'; + } + + @override + String get changelogEmpty => '目前沒有更新日誌'; + + @override + String get reportFilterDateStartNote => '開始日:當日 00:00(台北時間)'; + + @override + String get eewTitle => '地震速報'; + + @override + String get mapLayerWindForecastEcmwf => 'ECMWF'; + + @override + String regionSelectCount(int count, int max) { + return '已選 $count/$max'; + } + + @override + String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/雲相'; + + @override + String get meshtasticStateError => '錯誤'; + + @override + String get weatherModeOvercast => '陰天'; @override - String get navHome => '主頁'; + String get reportDetailDepth => '震源深度'; @override - String get navEvents => '事件'; + String get typhoonOverlayWarningTooltip => '標示警報區域縣市'; @override - String get navMap => '地圖'; + String get reportFilterDatePick => '選擇日期'; + + @override + String get onboardingSkipStay => '返回授權'; + + @override + String get commonFetchFailed => '無法獲取資料,請稍後重試'; + + @override + String get shelterOutdoorLabel => '室外收容'; + + @override + String get meshtasticStateConnected => '已連線'; + + @override + String get mapNavRadar => '雷達'; + + @override + String get mapLayerSatelliteCloudClear => '晴空'; + + @override + String eewSummary(String magnitude, String depth) { + return '規模 $magnitude・深度 $depth 公里'; + } + + @override + String get locationBannerPermission => '尚未授權定位,無法針對你的所在地推送警報。'; + + @override + String get typhoonOverlayWeatherNoneTooltip => '不疊雷達或紅外線'; + + @override + String get radarCountyOutlineHint => '畫在回波之上'; + + @override + String get windForecastCountyOutlineHint => '繪製於風場之上'; + + @override + String get homeRainTrendTitle => '近 1 小時降水趨勢'; + + @override + String get moonPhaseFirstQuarter => '上弦月'; + + @override + String get mapLayerCategoryTyphoon => '颱風'; + + @override + String get meshtasticUtilization => '空中工時(24 小時)'; + + @override + String get restroomTypeMixed => '混合廁所'; + + @override + String get restroomGradeGood => '優等級'; + + @override + String get notifyTsunami => '海嘯資訊'; @override String get navData => '資料'; @override - String get navEarthquake => '地震'; + String get mapLayerSatelliteBtdWvirw => 'ひまわり 過衝雲頂'; @override - String get dataSectionSeismic => '地震'; + String get meshtasticReadingAge => '數值時間'; @override - String get dataEarthquakeSubtitle => '地震報告'; + String get mapAppCallFailed => '此裝置無法撥打電話'; @override - String get dataSectionWeather => '氣象'; + String get reportFilterAny => '不限'; @override - String get dataWeatherRankingSubtitle => '即時觀測排行'; + String get weatherRankingMergeTo => '合併至'; @override - String get weatherRankingTitle => '觀測排行'; + String get notifyIntensity => '震度速報'; @override - String weatherRankingMeta(String time, int count) { - return '資料時間:$time\n共 $count 觀測點'; + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; } @override - String get weatherRankingEmpty => '目前沒有可排序的觀測'; + String get rainIntervalMenu => '累積時段'; @override - String get weatherRankingBy => '依'; + String get reportDetailLocalFelt => '小區域有感地震'; @override - String get weatherRankingHighest => '最高'; + String get meshtasticDevice => '裝置'; @override - String get weatherRankingLowest => '最低'; + String get onboardingGrant => '授權'; @override - String get weatherRankingMergeTo => '合併至'; + String get weatherModeRain => '雨天'; @override - String get weatherRankingMergeTown => '鄉鎮'; + String get shelterVulnerableOkLabel => '適合避難弱者安置'; @override - String get weatherRankingMergeCounty => '縣市'; + String get stationSheetEmpty => '點選任一測站查看觀測值'; @override - String get weatherRankingWind => '風速'; + String get typhoonLegendProbability => '侵襲機率'; @override - String get weatherRankingGust => '陣風'; + String get reportFilterMagnitude => '規模'; @override - String get weatherRankingTempExtremes => '溫度極值'; + String get skyTimeMorning => '上午'; @override - String get weatherRankingExtremeHigh => '今日最高'; + String get experimentalFeatures => '實驗性功能'; @override - String get weatherRankingExtremeLow => '今日最低'; + String get onboardingTermsBody => + '使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網絡狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機會比通知早抵達用戶所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供本地化警報,本服務會在前景及背景收集並上傳您的概略位置與裝置推送識別碼,僅用於決定應向您推送之警報。\n\n點按下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。'; @override - String get weatherRankingExtremeRange => '日溫差'; + String get reportFilterTitle => '篩選'; @override - String weatherRankingRecordedAt(String time) { - return '記錄於 $time'; + String get onboardingPermCritical => '重大通知'; + + @override + String trendCumulativeTotal(String total) { + return '累計 $total mm'; } @override - String weatherRankingAnalysisCurrent(String value) { - return '當下 $value°C'; + String get languageName => '繁體中文(香港)'; + + @override + String get reportListEmptyFiltered => '沒有符合條件的地震報告'; + + @override + String get meshtasticExcludeMqtt => '隱藏 MQTT 節點'; + + @override + String get mapNavTyphoon => '颱風'; + + @override + String get weatherModeSand => '沙塵'; + + @override + String get typhoonSatelliteTitle => '衛星雲圖'; + + @override + String get notifyReport => '地震報告'; + + @override + String get mapAppCoordinatesCopied => '已複製座標'; + + @override + String get skyTimeNight => '夜晚'; + + @override + String get sponsorRecommended => '推薦'; + + @override + String get mapLayerSatelliteB15 => 'ひまわり 長波紅外線(B15)'; + + @override + String get weatherRankingWind => '風速'; + + @override + String get feedStale => '資料可能已過期'; + + @override + String homeForecastWind(String direction, String level) { + return '$direction · $level 級'; } @override - String weatherRankingAnalysisHigh(String value) { - return '最高 $value'; + String get navHome => '主頁'; + + @override + String get meshtasticRegionLabel => '地區'; + + @override + String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂溫度'; + + @override + String get moonTimelineCaption => '月相'; + + @override + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth 公里'; } @override - String weatherRankingAnalysisLow(String value) { - return '最低 $value'; + String get openSourceLicenses => '引用套件'; + + @override + String get weatherRankingLowest => '最低'; + + @override + String get reportFilterSortDepth => '深度'; + + @override + String mapTimelineDataTime(String time) { + return '資料時間 $time'; } + @override + String get radarScanRange => '顯示掃描範圍'; + + @override + String get meshtasticHopLimit => '跳數上限'; + @override String weatherRankingAnalysisRange(String value) { return '溫差 $value°C'; } @override - String get reportListEmpty => '目前沒有地震報告'; + String get weatherRankingExtremeHigh => '今日最高'; @override - String get reportListEmptyFiltered => '沒有符合條件的地震報告'; + String get changelogVersionDetails => '版本資訊'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth 公里'; + String get sponsorPrivacy => '私隱權政策'; + + @override + String get reportDetailLocalIntensity => '所在地的震度'; + + @override + String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; + + @override + String get meshtasticAirtime => '發射佔空比'; + + @override + String shelterCapacityValue(int n) { + return '$n 人'; } @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; + String lightningLegendCc(int minutes) { + return '雲間 · $minutes 分內'; } @override - String get reportListDepthUnit => '公里'; + String get meshtasticSendHint => '要廣播的訊息'; @override - String get reportListLocalFelt => '小區域有感'; + String monitorDelay(String value) { + return '延遲 $value s'; + } @override - String get reportListToday => '今天'; + String get dpmNo => '否'; @override - String get reportListYesterday => '昨天'; + String get mapLayerSatelliteB08 => 'ひまわり 上層水氣(B08)'; @override - String reportListDayCount(int count) { - return '$count'; + String get meshtasticReconnecting => '重新連線中…'; + + @override + String get radarTownOutlineSubtitle => '讓鄉鎮界線在雷達回波下仍然清楚。'; + + @override + String get typhoonOverlayWeatherSatelliteTooltip => '紅外線(對齊颱風報文時間)'; + + @override + String get radarScanRangeHint => '框外空白代表未觀測'; + + @override + String typhoonPickerTd(String no) { + return '熱帶性低氣壓 TD $no'; } @override - String get reportListEnd => '已到最後一頁'; + String get mapLayerSatelliteWatervapor => 'ひまわり 水氣'; + + @override + String get regionAddButton => '新增地區'; + + @override + String get displaySettings => '顯示設定'; + + @override + String get restroomGradePoor => '不合格'; + + @override + String get restroomCategoryTourist => '觀光地區及風景區'; + + @override + String get locationBannerServiceOff => '定位服務已關閉,無法針對你的所在地推送警報。'; + + @override + String get mapLayerStyleTooltip => '顯示樣式'; + + @override + String lightningLegendCg(int minutes) { + return '對地 · $minutes 分內'; + } @override - String get reportFilterTitle => '篩選'; + String get skyTimeAuto => '自動'; @override - String get reportFilterSort => '排序方式'; + String get appLogs => 'App 日誌'; @override - String get reportFilterSortTime => '時間'; + String get feedConnecting => '連接中…'; @override - String get reportFilterSortIntensity => '震度'; + String get notifyBannerDisabled => '通知已關閉,將收不到災害警報。'; @override - String get reportFilterSortMagnitude => '規模'; + String get weatherHumidity => '濕度'; @override - String get reportFilterSortDepth => '深度'; + String typhoonValueMs(String n) { + return '每秒 $n 公尺'; + } @override - String get reportFilterOrderDesc => '降序'; + String homeForecastHumidity(String value) { + return '濕度 $value%'; + } @override - String get reportFilterOrderAsc => '升序'; + String get meshtasticBusyBody => + '請先在另一個 Meshtastic App 中斷線。兩個 App 同時連同一台裝置會互相搶走訊息,導致部分訊息遺失。'; @override - String get reportFilterIntensity => '震度'; + String get meshtasticChannelNoSlot => '沒有可用的頻道空位 — 請先在裝置上空出一個'; @override - String get reportFilterIntensityInfoTitle => '震度新制與舊制'; + String get restroomCategoryTransport => '交通'; @override - String get reportFilterIntensityInfoIntro => - '中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。'; + String get reportFilterLocationHint => '例如:花蓮、東部海域'; @override - String get reportFilterIntensityInfoLegacyTitle => '舊制(2020 以前)'; + String get moonSubtitle => '月相與亮度 — 完全本地計算'; @override - String get reportFilterIntensityInfoLegacyBody => '震度僅 0–7,沒有 5弱/5強/6弱/6強。'; + String get meshtasticBattery => '電量'; @override - String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; + String get meshtasticDistance => '距離'; @override - String get reportFilterIntensityInfoModernBody => - '震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。'; + String get meshtasticSnrTrend => '訊號趨勢 (SNR)'; @override - String get reportFilterMagnitude => '規模'; + String get meshtasticBatteryTrend => '電量趨勢'; @override - String get reportFilterDepth => '深度'; + String get typhoonOverlayMenuTooltip => '颱風圖層選項'; @override - String reportFilterDepthKm(String depth) { - return '$depth 公里'; - } + String get mapLayerSatelliteBtdOzone => 'ひまわり 對流層頂'; @override - String get reportFilterDate => '日期'; + String meshtasticRegionMismatch(String region) { + return '裝置地區為 $region — DPIP 需要 TW'; + } @override - String get reportFilterDatePick => '選擇日期'; + String get notifySectionEarthquake => '地震'; @override - String get reportFilterDateStartNote => '開始日:當日 00:00(台北時間)'; + String get mapLayerDisasterMap => '防災地圖'; @override - String get reportFilterDateEndNote => '結束日:當日 24:00(台北時間)'; + String get weatherModeFog => '大霧'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; } @override - String get reportFilterLocation => '地點'; - - @override - String get reportFilterLocationHint => '例如:花蓮、東部海域'; + String get mapLayerStyleGrayTooltip => '氣象廳灰階慣例:溫度越低越白'; @override - String get reportFilterAny => '不限'; + String get moreAnnouncements => '公告'; @override - String get reportFilterApply => '套用'; + String get mapLayerSatelliteTransparentNoData => '無資料(陸地) = 透明'; @override - String get reportFilterReset => '重設'; + String get restroomCategoryGovernment => '民眾洽公場所'; @override - String get reportListSearch => '查詢'; + String get typhoonLegendCurrent => '目前中心'; @override - String get reportDetailTitle => '地震報告'; + String get aedAddress => '地址'; @override - String reportDetailNumbered(String number) { - return '編號 $number 顯著有感地震'; - } + String get mapLayerAed => 'AED'; @override - String get reportDetailLocalFelt => '小區域有感地震'; + String get changelogTypePrerelease => '公測'; @override - String get reportDetailInfo => '詳細資訊'; + String get reportFilterIntensityInfoModernBody => + '震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。'; @override - String get reportDetailOriginTime => '發震時間'; + String get typhoonOverlayWeatherNone => '無'; @override - String get reportDetailEpicenter => '震央座標'; + String get mapLayerStyleGray => '灰階(JMA)'; @override - String get reportDetailMagnitude => '地震規模'; + String get weatherModeAuto => '自動'; @override - String get reportDetailDepth => '震源深度'; + String get typhoonLabelProbCircle => '70%機率圓'; @override - String get reportDetailAreaIntensity => '各地震度'; + String get notifyOptAll => '接收全部'; @override - String get reportDetailLocalIntensity => '所在地的震度'; + String get displayTheme => '主題'; @override - String get reportDetailLocalIntensityUnavailable => '沒有震度訊息'; + String get mapLayerSatelliteB07 => 'ひまわり 短波紅外(B07)'; @override - String get reportDetailSortByIntensity => '依震度排序'; + String get typhoonLabelDirection => '過去移動方向'; @override - String get reportDetailSortByCounty => '依縣市排序'; + String get regionManageTitle => '常用地區'; @override - String get reportDetailImage => '地震報告圖'; + String get typhoonLegendCone => '預測圓錐'; @override - String get reportDetailImageUnavailable => '報告圖尚未提供'; + String get moreCwaEew => '中央氣象署強震即時警報'; @override - String get reportDetailOpenReport => '報告頁面'; + String get onboardingPermsTitle => '權限授權'; @override - String get reportDetailReplay => '重播'; + String get mapLayerStyleJma => '雲頂強調(JMA)'; @override - String get navMore => '更多'; + String get rainInterval10m => '10 分'; @override - String get appLogs => 'App 日誌'; + String weatherRankingAnalysisLow(String value) { + return '最低 $value'; + } @override - String get changelogTitle => '更新日誌'; + String get meshtasticConnectAnyway => '仍要連線'; @override - String get changelogEmpty => '目前沒有更新日誌'; + String reportListDayCount(int count) { + return '$count'; + } @override - String get changelogTypePrerelease => '公測'; + String get mapLayerSatelliteB06 => 'ひまわり 近紅外(B06)'; @override - String get changelogTypeStable => '正式'; + String get mapLayerSatelliteTransparentReflectance => '低反射率/夜間 = 透明,顯示底圖'; @override - String get changelogCurrentVersion => '目前版本'; + String chartHourLabel(int hour) { + return '$hour時'; + } @override - String get changelogVersionDetails => '版本資訊'; + String get mapLayerShelter => '避難收容場所'; @override - String get changelogBodyEmpty => '此版本沒有說明。'; + String get typhoonOverlayProbabilityTooltip => '顯示侵襲機率(會隱藏預測圓錐)'; @override - String get mapPlaceholderDisabled => '地圖(暫時停用)'; + String get mapLayerSatelliteNdwi => 'ひまわり 水體指數'; @override - String get moreSectionRegion => '地區'; + String get disasterMapOverlayShelterTooltip => '顯示避難收容場所'; @override - String get moreSectionNotify => '通知'; + String get mapNavHumidity => '濕度'; @override - String get moreSectionDisplay => '顯示'; + String get reportDetailSortByIntensity => '依震度排序'; @override - String get regionManageTitle => '常用地區'; + String get homeRainTrendNoData => '無資料'; @override - String get regionAddButton => '新增地區'; + String get mapLayerCategoryRadar => '雷達'; @override - String get regionEmpty => '尚未新增常用地區'; + String get meshtasticShortName => '簡稱'; @override - String get regionSelectTitle => '選擇地區'; + String get mapLayerSatelliteAirmass => 'ひまわり 氣團'; @override - String regionSelectCount(int count, int max) { - return '已選 $count/$max'; - } + String get typhoonTrackDetail => '路徑詳情'; @override - String regionSelectFull(int max) { - return '最多只能選擇 $max 個地區'; - } + String get dataSectionWeather => '氣象'; @override - String get regionEdit => '修改'; + String get aedHoursWeekday => '平日開放時間'; @override - String get moreSectionAdvanced => '進階'; + String get homeActiveEventsTitle => '生效中事件'; @override - String get moreDeveloper => '偵錯資訊'; + String weatherRankingAnalysisHigh(String value) { + return '最高 $value'; + } @override - String get experimentalFeatures => '實驗性功能'; + String get faq => '常見問題'; @override - String get moreSectionLinks => '相關連結'; + String get typhoonHistoryLive => '即時'; @override - String get moreCwaEew => '中央氣象署強震即時警報'; + String eewSerial(int serial) { + return '第 $serial 報'; + } @override - String get moreTremReport => 'TREM 偵測報告'; + String get reportFilterSort => '排序方式'; @override - String get moreServerStatus => '伺服器狀態'; + String get meshtasticRegionConfirm => + '要將這台裝置切換為 TW 地區嗎?裝置會重新啟動並短暫斷線,上面的其他頻道也會一起改變。'; @override - String get moreAnnouncements => '公告'; + String get dataEarthquakeSubtitle => '地震報告'; @override - String get moreDiscord => 'Discord 社群'; + String get typhoonNoActive => '目前無颱風'; @override - String get moreNotifyLog => 'DPIP 通知發送記錄'; + String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/雲相(B11)'; @override - String get moreLinkOpenFailed => '無法開啟連結'; + String get navEvents => '事件'; @override - String get weatherDynamicState => '天氣動態狀態'; + String get onboardingTermsTitle => '服務條款'; @override - String get weatherDynamicStateSubtitle => '覆寫主頁背景天氣'; + String get mapTownLabels => '鄉鎮名稱'; @override - String get weatherModeAuto => '自動'; + String get notifySetFailed => '設定失敗,請稍後再試。'; @override - String get weatherModeClear => '晴天'; + String get meshtasticDisconnect => '斷線'; @override - String get weatherModeRain => '雨天'; + String get meshtasticUndecoded => '無法解密'; @override - String get weatherModeFog => '大霧'; + String get notifyAnnouncement => '公告'; @override - String get weatherModeThunderstorm => '雷暴'; + String get onboardingIntroTitle => '歡迎使用 DPIP'; @override - String get commonLoading => '載入中…'; + String get regionCurrentUnavailable => '無法取得所在地位置資訊'; @override - String get commonRetry => '重試'; + String get languageSystem => '系統預設'; @override - String get commonError => '發生錯誤'; + String get skyTimeSunset => '日落'; @override - String get commonFetchFailed => '無法獲取資料,請稍後重試'; + String get mapLayerSatelliteDust => 'ひまわり 沙塵'; @override - String get commonEmpty => '沒有資料'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get feedConnecting => '連接中…'; + String get regionEdit => '修改'; @override - String get feedStale => '資料可能已過期'; + String get weatherDynamicState => '天氣動態狀態'; @override - String get feedOffline => '連接中斷'; + String get mapPlaceholderDisabled => '地圖(暫時停用)'; @override - String get eewTitle => '地震速報'; + String get moonNow => '現在'; @override - String get eewNone => '目前沒有地震速報'; + String get moonSectionAppearance => '外觀'; @override - String eewSummary(String magnitude, String depth) { - return '規模 $magnitude・深度 $depth 公里'; - } + String get moonSectionRiseSet => '月出月落'; @override - String get regionNationwide => '全國'; + String get moonSectionUpcoming => '接下來'; @override - String get regionCurrent => '所在地'; + String get moonSectionCalendar => '月曆'; @override - String get regionCurrentUnavailable => '無法取得所在地位置資訊'; + String get moonDistance => '距離'; @override - String get weatherPrecipitation => '降水量'; + String get moonKilometres => '公里'; @override - String get weatherHumidity => '濕度'; + String get moonApparentSize => '視直徑'; @override - String weatherDataTime(String station, String time) { - return '$station ∙ 資料時間 $time'; - } + String get moonRise => '月出'; @override - String get homeViewOnMap => '前往地圖察看'; + String get moonSet => '月落'; @override - String get homeForecastTitle => '24小時預報'; + String get moonNextNewMoon => '下次新月'; @override - String homeForecastHighLow(String high, String low) { - return '高 $high° · 低 $low°'; - } + String get moonAlwaysUp => '整日在地平線上'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get moonNoEvent => '當日無'; @override - String homeForecastFeelsLike(String temp) { - return '體感 $temp°'; - } + String get sunTitle => '太陽'; @override - String homeForecastHumidity(String value) { - return '濕度 $value%'; - } + String get sunSubtitle => '日出日沒、曙暮光與節氣'; @override - String homeForecastWind(String direction, String level) { - return '$direction · $level 級'; - } + String get sunSectionDaylight => '日照'; @override - String get homeForecastUnavailable => '選擇地區後可查看預報'; + String get sunSectionTwilight => '曙暮光'; @override - String get homeForecastEmpty => '目前沒有預報資料'; + String get sunSectionLight => '光線'; @override - String get homeActiveEventsTitle => '生效中事件'; + String get sunSectionSundial => '日晷'; @override - String get homeActiveEventsEmpty => '目前沒有生效中的事件'; + String get sunSectionTerms => '節氣'; @override - String get homeRainTrendTitle => '近 1 小時降水趨勢'; + String get sunRise => '日出'; @override - String homeRainTrendMinute(int minute) { - return '$minute分'; - } + String get sunSet => '日沒'; @override - String homeRainTrendUpdated(String time) { - return '更新 $time'; - } + String get sunNoon => '正午'; @override - String get homeRainTrendNoData => '無資料'; + String get sunDayLength => '白晝長度'; @override - String get homeRainTrendScattered => '可能會有零星降雨'; + String get sunTwilightCivil => '民用'; @override - String get homeRainTrendLightSustained => '未來 1 小時會有持續小雨'; + String get sunTwilightNautical => '航海'; @override - String homeRainTrendLightStopping(int minutes) { - return '預計 $minutes 分鐘後停止下小雨'; - } + String get sunTwilightAstronomical => '天文'; @override - String get homeRainTrendHeavySustained => '未來 1 小時會有持續大雨'; + String get sunGoldenHourMorning => '晨間黃金時刻'; @override - String homeRainTrendHeavyStopping(int minutes) { - return '預計 $minutes 分鐘後停止下大雨'; - } + String get sunGoldenHourEvening => '昏間黃金時刻'; @override - String get mapLayers => '圖層'; + String get sunBlueHour => '藍調時刻'; @override - String get mapLayerOrderTitle => '調整圖層順序'; + String get sunEquationOfTime => '均時差'; @override - String get mapLayerOrderReset => '回復預設順序'; + String get sunMinutes => '分'; @override - String get mapLayerRadar => '雷達合成回波圖'; + String get solarTermNext => '下一個節氣'; @override - String get mapLayerSatellite => 'ひまわり 紅外線(B13)'; + String get planetsTitle => '行星'; @override - String get mapLayerSatelliteB01 => 'ひまわり 可見光-藍(B01)'; + String get planetsSubtitle => '今晚在哪、有多亮'; @override - String get mapLayerSatelliteB02 => 'ひまわり 可見光-綠(B02)'; + String get planetsSectionTonight => '此刻'; @override - String get mapLayerSatelliteB03 => 'ひまわり 可見光-紅(B03)'; + String get planetUp => '地平線上'; @override - String get mapLayerSatelliteB04 => 'ひまわり 近紅外(B04)'; + String get planetDown => '地平線下'; @override - String get mapLayerSatelliteB05 => 'ひまわり 近紅外(B05)'; + String get planetInGlare => '太近太陽'; @override - String get mapLayerSatelliteB06 => 'ひまわり 近紅外(B06)'; + String get planetMagnitude => '亮度'; @override - String get mapLayerSatelliteB07 => 'ひまわり 短波紅外(B07)'; + String get planetElongation => '距日距角'; @override - String get mapLayerSatelliteB08 => 'ひまわり 上層水氣(B08)'; + String get planetSky => '時段'; @override - String get mapLayerSatelliteB09 => 'ひまわり 中層水氣(B09)'; + String get planetEvening => '昏星'; @override - String get mapLayerSatelliteB10 => 'ひまわり 低層水氣(B10)'; + String get planetMorning => '晨星'; @override - String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/雲相(B11)'; + String get planetDistance => '距離'; @override - String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; + String get planetAu => '天文單位'; @override - String get mapLayerSatelliteB13 => 'ひまわり 紅外線(B13)'; + String get planetAltitude => '仰角'; @override - String get mapLayerSatelliteB14 => 'ひまわり 長波紅外線(B14)'; + String get planetMercury => '水星'; @override - String get mapLayerSatelliteB15 => 'ひまわり 長波紅外線(B15)'; + String get planetVenus => '金星'; @override - String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; + String get planetMars => '火星'; @override - String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; + String get planetJupiter => '木星'; @override - String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; + String get planetSaturn => '土星'; @override - String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + String get planetUranus => '天王星'; @override - String get mapLayerSatelliteDust => 'ひまわり 沙塵'; + String get planetNeptune => '海王星'; @override - String get mapLayerSatelliteAirmass => 'ひまわり 氣團'; + String get solarTermVernalEquinox => '春分'; @override - String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; + String get solarTermPureBrightness => '清明'; @override - String get mapLayerSatelliteWatervapor => 'ひまわり 水氣'; + String get solarTermGrainRain => '穀雨'; @override - String get mapLayerSatelliteBtdSplit => 'ひまわり 分割視窗'; + String get solarTermStartOfSummer => '立夏'; @override - String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; + String get solarTermGrainFull => '小滿'; @override - String get mapLayerSatelliteBtdWvirw => 'ひまわり 過衝雲頂'; + String get solarTermGrainInEar => '芒種'; @override - String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/雲相'; + String get solarTermSummerSolstice => '夏至'; @override - String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷雲/雲高'; + String get solarTermMinorHeat => '小暑'; @override - String get mapLayerSatelliteBtdOzone => 'ひまわり 對流層頂'; + String get solarTermMajorHeat => '大暑'; @override - String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂溫度'; + String get solarTermStartOfAutumn => '立秋'; @override - String get mapLayerSatelliteCloudmask => 'ひまわり 雲遮罩'; + String get solarTermEndOfHeat => '處暑'; @override - String get mapLayerSatelliteSst => 'ひまわり 海表溫度'; + String get solarTermWhiteDew => '白露'; @override - String get mapLayerSatelliteNdvi => 'ひまわり 植生指數'; + String get solarTermAutumnalEquinox => '秋分'; @override - String get mapLayerSatelliteNdwi => 'ひまわり 水體指數'; + String get solarTermColdDew => '寒露'; @override - String get mapLayerSatelliteMndwi => 'ひまわり 改良水體指數'; + String get solarTermFrostDescent => '霜降'; @override - String get mapLayerSatelliteGlobalOutline => '國界'; + String get solarTermStartOfWinter => '立冬'; @override - String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; + String get solarTermMinorSnow => '小雪'; @override - String get mapLayerSatelliteCloudClear => '晴空'; + String get solarTermMajorSnow => '大雪'; @override - String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; + String get solarTermWinterSolstice => '冬至'; @override - String get mapLayerSatelliteCloudProbablyCloudy => '可能有雲'; + String get solarTermMinorCold => '小寒'; @override - String get mapLayerSatelliteCloudCloudy => '有雲'; + String get solarTermMajorCold => '大寒'; @override - String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,顯示底圖'; + String get solarTermStartOfSpring => '立春'; @override - String get mapLayerSatelliteTransparentReflectance => '低反射率/夜間 = 透明,顯示底圖'; + String get solarTermRainWater => '雨水'; @override - String get mapLayerSatelliteTransparentZero => '零差值 = 透明(無訊號)'; + String get solarTermAwakeningOfInsects => '驚蟄'; @override - String get mapLayerSatelliteTransparentNight => '夜間 = 透明,顯示底圖'; + String get tonightTitle => '今夜'; @override - String get mapLayerSatelliteTransparentNoData => '無資料(陸地) = 透明'; + String get tonightSubtitle => '現在看得到什麼、什麼時候'; @override - String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(無植被)'; + String get tonightSectionDark => '觀測窗口'; @override - String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(無水體)'; + String get tonightAstronomicalNight => '天文夜'; @override - String get mapLayerSatelliteTransparentClear => '晴空 = 透明,顯示底圖'; + String get tonightNeverDark => '整夜不全暗'; @override - String get mapLayerStyleSection => '顯示樣式'; + String get tonightDarkWindow => '暗窗'; @override - String get mapLayerStyleTooltip => '顯示樣式'; + String get tonightMoonAllNight => '月亮整夜在天上'; @override - String get mapLayerStyleGray => '灰階(JMA)'; + String get tonightDarkTotal => '總暗時'; @override - String get mapLayerStyleGrayTooltip => '氣象廳灰階慣例:溫度越低越白'; + String get tonightMoonlight => '月光'; @override - String get mapLayerStyleJma => '雲頂強調(JMA)'; + String get tonightSectionShowers => '流星雨'; @override - String get mapLayerStyleJmaTooltip => '灰階為底,−40 °C 以下上色,凸顯雲頂高度'; + String get tonightRadiantDown => '輻射點不升起'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get tonightPerHour => '顆/時'; @override - String get mapLayerStyleBdTooltip => 'Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階'; + String get tonightSectionSatellites => '衛星過境'; @override - String get mapLayerQpesums => '未來 1 小時降水預報'; + String get tonightSectionTargets => '此刻可觀測目標'; @override - String get mapLayerLightning => '閃電'; + String get showerQuadrantids => '象限儀座'; @override - String lightningLegendCg(int minutes) { - return '對地 · $minutes 分內'; - } + String get showerLyrids => '天琴座'; @override - String lightningLegendCc(int minutes) { - return '雲間 · $minutes 分內'; - } + String get showerEtaAquariids => '寶瓶座η'; @override - String get mapTimelineNow => '現在'; + String get showerDeltaAquariids => '寶瓶座δ'; @override - String get mapTimelinePast => '歷史'; + String get showerPerseids => '英仙座'; @override - String get mapTimelineFuture => '未來'; + String get showerOrionids => '獵戶座'; @override - String get mapTimelineObserved => '觀測'; + String get showerSouthernTaurids => '金牛座南'; @override - String get mapTimelineForecast => '預報'; + String get showerLeonids => '獅子座'; @override - String mapTimelineDataTime(String time) { - return '資料時間 $time'; - } + String get showerGeminids => '雙子座'; @override - String get notifySettingsMenu => '通知設定'; + String get showerUrsids => '小熊座'; @override - String get notifyTitle => '通知'; + String get deepSkyOpenCluster => '疏散星團'; @override - String get notifyUnavailable => '推送尚未就緒,請稍後再試。'; + String get deepSkyGlobularCluster => '球狀星團'; @override - String get notifySetFailed => '設定失敗,請稍後再試。'; + String get deepSkySpiralGalaxy => '螺旋星系'; @override - String get notifySectionEew => '地震速報'; + String get deepSkyEllipticalGalaxy => '橢圓星系'; @override - String get notifySectionEarthquake => '地震'; + String get deepSkyIrregularGalaxy => '不規則星系'; @override - String get notifySectionWeather => '天氣'; + String get deepSkyPlanetaryNebula => '行星狀星雲'; @override - String get notifySectionTsunami => '海嘯'; + String get deepSkySupernovaRemnant => '超新星遺跡'; @override - String get notifySectionOther => '其他'; + String get deepSkyEmissionNebula => '發射星雲'; @override - String get notifyEew => '緊急地震速報'; + String get deepSkyReflectionNebula => '反射星雲'; @override - String get notifyMonitor => '強震監視器'; + String get deepSkyAsterism => '星群'; @override - String get notifyReport => '地震報告'; + String get almanacTitle => '曆法'; @override - String get notifyIntensity => '震度速報'; + String get almanacSubtitle => '農曆日期與未來的日月食'; @override - String get notifyThunderstorm => '雷暴即時訊息'; + String get almanacSectionToday => '今日'; @override - String get notifyAdvisory => '天氣警告及特報'; + String get almanacGregorian => '西曆'; @override - String get notifyEvacuation => '防災資訊'; + String get almanacLunar => '農曆'; @override - String get notifyTsunami => '海嘯資訊'; + String get almanacYear => '歲次'; @override - String get notifyAnnouncement => '公告'; + String get almanacMonthLength => '月大小'; @override - String get notifyOptOff => '關閉'; + String get almanacLongMonth => '三十日'; @override - String get notifyOptAll => '接收全部'; + String get almanacShortMonth => '二十九日'; @override - String get notifyOptLocalIntensity4 => '所在地震度4以上'; + String get almanacLeapPrefix => '閏'; @override - String get notifyOptLocalIntensity1 => '所在地震度1以上'; + String get almanacSectionLunarEclipses => '月食'; @override - String get notifyOptWeatherLocal => '接收所在地'; + String get almanacSectionSolarEclipses => '日食'; @override - String get notifyOptTsunamiWarning => '只接收海嘯警報'; + String get almanacNoSolarEclipse => '範圍內無'; @override - String get notifyOptTsunamiAll => '海嘯消息、海嘯警報'; + String get eclipseTotal => '全食'; @override - String get onboardingNext => '下一步'; + String get eclipsePartial => '偏食'; @override - String get onboardingBack => '上一步'; + String get eclipseAnnular => '環食'; @override - String get onboardingScrollHint => '向下捲動以繼續'; + String get eclipsePenumbral => '半影食'; @override - String get onboardingIntroTitle => '歡迎使用 DPIP'; + String get zodiacRat => '鼠'; @override - String get onboardingIntroBody => - 'DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷暴即時訊息、天氣警告及特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。'; + String get zodiacOx => '牛'; @override - String get onboardingTermsTitle => '服務條款'; + String get zodiacTiger => '虎'; @override - String get onboardingTermsBody => - '使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網絡狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機會比通知早抵達用戶所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供本地化警報,本服務會在前景及背景收集並上傳您的概略位置與裝置推送識別碼,僅用於決定應向您推送之警報。\n\n點按下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。'; + String get zodiacRabbit => '兔'; @override - String get onboardingTermsAgree => '我已閱讀並同意服務條款'; + String get zodiacDragon => '龍'; @override - String get onboardingAgreeContinue => '同意並繼續'; + String get zodiacSnake => '蛇'; @override - String get onboardingPermsTitle => '權限授權'; + String get zodiacHorse => '馬'; @override - String get onboardingPermsBody => '為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中更改。'; + String get zodiacGoat => '羊'; @override - String get onboardingPermNotify => '通知'; + String get zodiacMonkey => '猴'; @override - String get onboardingPermNotifyDesc => '在地震、天氣與災害發生時,即時傳遞警報通知。'; + String get zodiacRooster => '雞'; @override - String get onboardingPermCritical => '重大通知'; + String get zodiacDog => '狗'; @override - String get onboardingPermCriticalDesc => '讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。'; + String get zodiacPig => '豬'; @override - String get onboardingPermLocation => '定位'; + String get tideTitle => '潮汐'; @override - String get onboardingPermLocationDesc => '依你所在位置推送本地警報。'; + String get tideSubtitle => '大潮、小潮與月球引力'; @override - String get onboardingPermBackground => '背景定位'; + String get tideDisclaimer => '僅為天文引潮力,非港口潮汐表。水位請參考氣象署公布之潮汐預報。'; @override - String get onboardingPermBackgroundDesc => '選擇「一律允許」,關閉 App 也能推送本地警報。'; + String get tideSectionNow => '此刻'; @override - String get onboardingPermBattery => '省電白名單'; + String get tidePhase => '週期'; @override - String get onboardingPermBatteryDesc => '允許 DPIP 在背景持續運作,避免警報延遲或漏收。'; + String get tideSpring => '大潮'; @override - String get onboardingGrant => '授權'; + String get tideNeap => '小潮'; @override - String get onboardingGranted => '已授權'; + String get tideMiddling => '中潮'; @override - String get onboardingStart => '開始使用'; + String get tideLunarDistanceFactor => '月球引力'; @override - String get language => '語言'; + String get tideEquilibrium => '平衡潮高'; @override - String get languageSettings => '語言設定'; + String get tideMetres => '公尺'; @override - String get languageSystem => '系統預設'; + String get tidePerigeanSpring => '下次近地點大潮'; @override - String get locationBannerServiceOff => '定位服務已關閉,無法針對你的所在地推送警報。'; + String get tideSectionTurningPoints => '轉折點'; @override - String get locationBannerPermission => '尚未授權定位,無法針對你的所在地推送警報。'; + String get tideHigh => '高'; @override - String get locationBannerFix => '開啟設定'; + String get tideLow => '低'; @override - String get notifyBannerDisabled => '通知已關閉,將收不到災害警報。'; + String get skyChartTitle => '星圖'; @override - String get onboardingSkipTitle => '尚未完成授權'; + String get skyChartSubtitle => '頭頂上肉眼可見的天空'; @override - String get onboardingSkipBody => - '未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。'; + String get skyChartNorth => '北'; @override - String get onboardingSkipStay => '返回授權'; + String get skyChartEast => '東'; @override - String get onboardingSkipLeave => '仍要略過'; + String get skyChartSouth => '南'; @override - String get moreYoutube => 'YouTube'; + String get skyChartWest => '西'; @override - String get moreGithub => 'ExpTech GitHub'; + String tonightElementAge(int days) { + return '軌道資料 $days 天前'; + } @override - String get moreSourceCode => '原始碼'; + String almanacLunarDate(String leap, int month, int day) { + return '$leap$month 月 $day 日'; + } @override - String get moreSectionApp => '取得 App'; + String get tonightNoShowers => '目前無流星雨'; @override - String get moreGooglePlay => 'Google Play'; + String get tonightNoPasses => '48 小時內無可見過境'; @override - String get moreAppStore => 'App Store'; + String get tonightSatellitesUnavailable => '無法讀取軌道資料'; @override - String get displaySettings => '顯示設定'; + String get tonightNoTargets => '無足夠高度的目標'; @override - String get defaultMapLayerSettings => '地圖預設圖層'; + String get skyChartUnavailable => '無法讀取星表'; +} + +/// The translations for Chinese, as used in Taiwan (`zh_TW`). +class AppLocalizationsZhTw extends AppLocalizationsZh { + AppLocalizationsZhTw() : super('zh_TW'); @override - String get defaultMapLayerSubtitle => '開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。'; + String typhoonValueLat(String lat) { + return '北緯 $lat 度'; + } @override - String get mapNavRadar => '雷達'; + String get onboardingSkipBody => + '未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。'; @override - String get mapNavQpesums => '預報'; + String get rainInterval24h => '24 時'; @override - String get mapNavSatellite => '衛星'; + String homeRainTrendHeavyStopping(int minutes) { + return '預計 $minutes 分鐘後停止下大雨'; + } @override - String get mapNavLightning => '閃電'; + String get mapTimelineObserved => '觀測'; @override - String get mapNavTyphoon => '颱風'; + String get regionSelectTitle => '選擇地區'; @override - String get mapNavEarthquake => '地震'; + String get skyTimeNoon => '正午'; @override - String get mapNavTemperature => '溫度'; + String get radarCountyOutlineSubtitle => '讓縣市界線在雷達回波下仍然清楚。'; @override - String get mapNavHumidity => '濕度'; + String get dpmFilterSectionRestroomType => '廁所類型'; @override - String get mapNavPressure => '氣壓'; + String get mapLayerSatelliteB03 => 'ひまわり 可見光-紅(B03)'; @override - String get mapNavWind => '風向'; + String get reportFilterIntensity => '震度'; @override - String get mapNavRain => '雨量'; + String get mapLayerLightning => '閃電'; @override - String get mapNavDisaster => '防災'; + String get restroomTypeMale => '男廁所'; @override - String get displayTheme => '主題'; + String get meshtasticLastReceived => '最近接收'; @override - String get themeSystem => '跟隨系統'; + String get reportDetailSortByCounty => '依縣市排序'; @override - String get themeLight => '淺色'; + String get homeRainTrendScattered => '可能會有零星降雨'; @override - String get themeDark => '深色'; + String get meshtasticUptime => '運行時間'; @override - String get moreSectionAbout => '關於'; + String get weatherRankingTempExtremes => '溫度極值'; @override - String get termsOfService => '服務條款'; + String get themeLight => '淺色'; @override - String get faq => '常見問題'; + String get mapTerrainReliefHint => '在底圖上顯示立體地形陰影'; @override - String get openSourceLicenses => '引用套件'; + String get meshtasticEmptyMessage => '(空白訊息)'; @override - String get sponsorTitle => '支持 DPIP'; + String get moreSectionRegion => '地區'; @override - String get sponsorIntro => - 'DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。'; + String get dpmDisasterEarthquake => '震災'; @override - String get sponsorSubscriptions => '訂閱制'; + String get mapLayerSatellite => 'ひまわり 紅外線(B13)'; @override - String get sponsorRecommended => '推薦'; + String get aedHoursSaturday => '週六開放時間'; @override - String get sponsorOneTime => '單次支援'; + String get dpmDisasterSlope => '坡地災害'; @override - String sponsorPerMonth(String price) { - return '$price / 月'; - } + String get moonPhaseNew => '新月'; @override - String get sponsorRestore => '恢復購買'; + String get notifySectionEew => '地震速報'; @override - String get sponsorTerms => '使用條款'; + String get mapResetNorth => '回到北方'; @override - String get sponsorPrivacy => '私隱權政策'; + String get rainInterval2d => '2 日'; @override - String get sponsorRestoring => '正在恢復購買…'; + String get mapTownLabelsHint => '放大時顯示鄉鎮名稱'; @override - String get sponsorRestoreUnavailable => '無法連線至商店,請稍後再試'; + String get commonCancel => '取消'; @override - String get commonClose => '關閉'; + String get notifyOptTsunamiWarning => '只接收海嘯警報'; @override - String get mapLayerTemperature => '溫度'; + String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; @override - String get trendRange24h => '24 小時'; + String get moreSectionAdvanced => '進階'; @override - String get trendRange7d => '7 天'; + String get weatherRankingExtremeRange => '日溫差'; @override - String get trendNoData => '沒有趨勢資料'; + String get notifySettingsMenu => '通知設定'; @override - String trendCumulativeTotal(String total) { - return '累計 $total mm'; - } + String get typhoonHistoryTitle => '資料時間'; @override - String chartHourLabel(int hour) { - return '$hour時'; + String mapAppDefault(String app) { + return '$app(預設)'; } @override - String get mapLayerHumidity => '濕度'; + String get trendRange24h => '24 小時'; @override - String get mapLayerPressure => '氣壓'; + String get mapLayerStyleJmaTooltip => '灰階為底,−40 °C 以下上色,凸顯雲頂高度'; @override - String get mapLayerWind => '風向'; + String weatherRankingRecordedAt(String time) { + return '記錄於 $time'; + } @override String get mapLayerRain => '雨量'; @override - String get rainIntervalMenu => '累積時段'; + String get mapLayerQpesums => '未來 1 小時降水預報'; @override - String get rainIntervalNow => '今日'; + String get mapOverlaySectionMap => '地圖'; @override - String get rainInterval10m => '10 分'; + String get mapTerrainRelief => '地形立體感'; @override - String get rainInterval1h => '1 時'; + String get eewMaxIntensity => '最大震度'; @override - String get rainInterval3h => '3 時'; + String get mapLegendCollapse => '收合圖例'; + + @override + String get changelogTitle => '更新日誌'; @override - String get rainInterval6h => '6 時'; + String get reportFilterOrderDesc => '降序'; @override - String get rainInterval12h => '12 時'; + String get meshtasticExcludeMqttSubtitle => '經網際網路橋接、並非無線電聽到的節點'; @override - String get rainInterval24h => '24 時'; + String get reportFilterIntensityInfoTitle => '震度新制與舊制'; @override - String get rainInterval2d => '2 日'; + String get mapLayerTyphoon => '颱風'; @override - String get rainInterval3d => '3 日'; + String get radarOverlayMenuTooltip => '雷達圖層選項'; @override - String get mapLayerTyphoon => '颱風'; + String get mapMyLocation => '我的位置'; @override - String get typhoonNoActive => '目前無颱風'; + String get meshtasticNodes => '節點'; @override - String get typhoonWind => '風速'; + String get meshtasticSend => '傳送'; @override - String get typhoonGust => '陣風'; + String get typhoonOverlayStormL7Tooltip => '七級暴風圈+平均圓(紫色)'; @override - String get typhoonPressure => '氣壓'; + String get aedType => '場所類型'; @override - String get typhoonMotion => '移動'; + String get termsOfService => '服務條款'; @override - String get typhoonLabelPosition => '中心位置'; + String get typhoonLegendCircle25 => '十級風暴風圈'; @override - String get typhoonLabelDirection => '過去移動方向'; + String get sponsorTitle => '支持 DPIP'; @override - String get typhoonLabelSpeed => '過去移動時速'; + String get mapNavSatellite => '衛星'; @override - String get typhoonLabelPressure => '中心氣壓'; + String homeRainTrendUpdated(String time) { + return '更新 $time'; + } @override - String get typhoonLabelWind => '近中心最大風速'; + String get onboardingNext => '下一步'; @override - String get typhoonLabelGust => '瞬間最大陣風'; + String get weatherRankingMergeTown => '鄉鎮'; @override - String get typhoonLabelGaleAvg => '七級風平均暴風半徑'; + String get mapLayerMonitor => '強震監視器'; @override - String get typhoonLabelStormAvg => '十級風平均暴風半徑'; + String get moreYoutube => 'YouTube'; @override - String get typhoonLabelProbCircle => '70%機率圓'; + String get sponsorSubscriptions => '訂閱制'; @override - String typhoonForecastLead(String hours) { - return '預測 +$hours 小時'; + String typhoonValueLon(String lon) { + return '東經 $lon 度'; } @override - String get typhoonLabelNw => '西北側'; + String get skyTime => '天空時間'; @override - String get typhoonLabelNe => '東北側'; + String get weatherModeCloudy => '多雲'; @override - String get typhoonLabelSw => '西南側'; + String get skyTimeDusk => '暮色'; @override - String get typhoonLabelSe => '東南側'; + String get meshtasticFirmware => '韌體'; @override - String typhoonValueLat(String lat) { - return '北緯 $lat 度'; - } + String get reportFilterDateEndNote => '結束日:當日 24:00(臺北時間)'; @override - String typhoonValueLon(String lon) { - return '東經 $lon 度'; - } + String get reportFilterSortMagnitude => '規模'; @override - String typhoonValueKm(String n) { - return '$n 公里'; - } + String get meshtasticSilent => '已靜默'; @override - String typhoonValueHpa(String n) { - return '$n 百帕'; - } + String get mapLayerCategoryEarthquake => '地震'; @override - String typhoonValueMs(String n) { - return '每秒 $n 公尺'; - } + String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; @override - String typhoonDataTime(String time) { - return '資料時間\n$time'; - } + String get typhoonLegendPast => '實際路徑'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get restroomCategoryOther => '其他'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String homeForecastHighLow(String high, String low) { + return '高 $high° · 低 $low°'; + } @override - String get mapLayerMonitor => '強震監視器'; + String get locationBannerFix => '開啟設定'; @override - String get mapLayerDisasterMap => '防災地圖'; + String get mapLegendExpand => '圖例'; @override - String get mapLayerAed => 'AED'; + String get eewNone => '目前沒有地震速報'; @override - String get disasterMapOverlayMenuTooltip => '防災地圖圖層'; + String typhoonTyNo(String no) { + return 'TY $no'; + } @override - String get disasterMapOverlaySectionLayers => '圖層'; + String get notifyOptTsunamiAll => '海嘯消息、海嘯警報'; @override - String get disasterMapOverlayAedTooltip => '顯示 AED 位置'; + String get meshtasticLayerOptions => '節點選項'; @override - String get aedAddress => '地址'; + String get onboardingAgreeContinue => '同意並繼續'; @override - String get aedRegion => '縣市區域'; + String get commonRetry => '重試'; @override - String get aedCategory => '場所分類'; + String get meshtasticNodeId => '節點 ID'; @override - String get aedType => '場所類型'; + String reportDetailNumbered(String number) { + return '編號 $number 顯著有感地震'; + } @override - String get aedPlaceDesc => '放置位置說明'; + String get typhoonOverlayStormBandSubtitle => '含平均圓'; @override - String get aedDescription => '備註'; + String get disasterMapOverlayRestroomTooltip => '顯示公廁'; @override - String get aedHoursWeekday => '平日開放時間'; + String get weatherRankingTitle => '觀測排行'; @override - String get aedHoursSaturday => '週六開放時間'; + String get homeRainTrendHeavySustained => '未來 1 小時會有持續大雨'; @override - String get aedHoursSunday => '週日開放時間'; + String get notifySectionTsunami => '海嘯'; @override - String get aedOpenRemark => '開放時間備註'; + String get restroomCategoryPark => '公園'; @override - String get aedEmergencyPhone => '緊急聯絡電話'; + String get moreLinkOpenFailed => '無法開啟連結'; @override - String get mapLayerRestroom => '公廁'; + String get themeDark => '深色'; @override - String get mapLayerShelter => '避難收容場所'; + String get sponsorRestore => '恢復購買'; @override - String get disasterMapOverlayRestroomTooltip => '顯示公廁'; + String get meshtasticChannelWorking => '正在設定 DPIP 頻道…'; @override - String get disasterMapOverlayShelterTooltip => '顯示避難收容場所'; + String get meshtasticRegionSwitch => '切換為 TW'; @override - String get dpmOpenInMaps => '開啟地圖'; + String get meshtasticTraffic => '流量'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get mapLayerStyleBdTooltip => 'Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get disasterMapOverlayAedTooltip => '顯示 AED 位置'; @override - String mapAppDefault(String app) { - return '$app(預設)'; - } + String get mapLayerHumidity => '濕度'; @override - String get mapAppCopyCoordinates => '複製座標'; + String get mapLayerSatelliteTransparentNight => '夜間 = 透明,顯示底圖'; @override - String get mapAppCoordinatesCopied => '已複製座標'; + String get meshtasticScanning => '掃描中…'; @override - String mapAppOpenFailed(String app) { - return '無法開啟 $app'; + String regionSelectFull(int max) { + return '最多只能選擇 $max 個地區'; } @override - String get mapAppCallFailed => '此裝置無法撥打電話'; + String get meshtasticTitle => 'Meshtastic'; @override - String get mapOverlaySectionReference => '參考圖層'; + String get navMore => '更多'; @override - String get mapLayerCategoryEarthquake => '地震'; + String get meshtasticDpipChannel => 'DPIP 頻道'; @override - String get mapLayerCategoryTyphoon => '颱風'; + String get disasterMapOverlaySectionLayers => '圖層'; @override - String get mapLayerCategoryWeather => '氣象觀測'; + String get mapLayerSatelliteB05 => 'ひまわり 近紅外(B05)'; @override - String get mapLayerCategorySatellite => '衛星'; + String typhoonStormRadii(String ne, String se, String sw, String nw) { + return '東北 $ne · 東南 $se · 西南 $sw · 西北 $nw km'; + } @override - String get mapLayerCategoryRadar => '雷達'; + String get typhoonLabelNe => '東北側'; @override - String get mapLayerCategoryLife => '生活'; + String get meshtasticCopied => '已複製訊息'; @override - String get mapLayerCategoryForecast => '數值預報'; + String get reportListEmpty => '目前沒有地震報告'; @override - String get mapOverlaySectionMap => '地圖'; + String get reportListEnd => '已到最後一頁'; @override - String get rainIntervalSection => '統計時間'; + String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; @override - String get mapTownLabels => '鄉鎮名稱'; + String get typhoonOverlaySectionExtra => '覆蓋層'; @override - String get mapTownLabelsHint => '放大時顯示鄉鎮名稱'; + String get eewSWave => '震波'; @override - String get mapTerrainRelief => '地形立體感'; + String get meshtasticBusyTitle => '另一個 App 正在使用這台裝置'; @override - String get mapTerrainReliefHint => '在底圖上顯示立體地形陰影'; + String get restroomCategoryCultural => '文化育樂活動場所'; @override - String get dpmSheetEmpty => '點選地圖上的標記查看詳情'; + String get typhoonLabelWind => '近中心最大風速'; @override - String get dpmAddress => '地址'; + String get radarGlobalOutlineHint => '各國國界外框'; @override - String get restroomTypeLabel => '廁所類型'; + String get notifyEvacuation => '防災資訊'; @override - String get restroomCategoryLabel => '類別'; + String get typhoonLegendCircle15 => '七級風暴風圈'; @override - String get restroomGradeLabel => '等級'; + String get dataSectionAstronomy => '天文'; @override - String get restroomTypeFemale => '女廁所'; + String get homeRainTrendLightSustained => '未來 1 小時會有持續小雨'; @override - String get restroomTypeMale => '男廁所'; + String get commonError => '發生錯誤'; @override - String get restroomTypeMixed => '混合廁所'; + String get moonPhaseWaningCrescent => '殘月'; @override - String get restroomTypeAccessible => '無障礙廁所'; + String get meshtasticPower => '電力'; @override - String get restroomTypeGenderNeutral => '性別友善廁所'; + String get mapTimelineNow => '現在'; @override - String get restroomTypeFamily => '親子廁所'; + String reportFilterRange(String start, String end) { + return '$start – $end'; + } @override - String get restroomTypeUnspecified => '未設定'; + String get reportDetailOpenReport => '報告頁面'; @override - String get restroomCategoryTransport => '交通'; + String get trendRange7d => '7 天'; @override - String get restroomCategoryPark => '公園'; + String typhoonWarningAreas(String areas) { + return '警戒區域:$areas'; + } @override - String get restroomCategoryCommercial => '商業營業場所'; + String get rainIntervalSection => '統計時間'; @override - String get restroomCategoryReligious => '宗教禮儀場所'; + String get notifyTitle => '通知'; @override - String get restroomCategoryCultural => '文化育樂活動場所'; + String get meshtasticTxPower => '發射功率'; @override - String get restroomCategoryGovernment => '民眾洽公場所'; + String get restroomCategoryLabel => '類別'; @override - String get restroomCategoryWelfare => '社福機構、集會場所'; + String get sponsorRestoring => '正在恢復購買…'; @override - String get restroomCategoryTourist => '觀光地區及風景區'; + String get sponsorIntro => + 'DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。'; @override - String get restroomCategoryLeisure => '休閒娛樂場所'; + String get shelterAddressLabel => '地址'; @override - String get restroomCategoryOther => '其他'; + String get typhoonLabelStormAvg => '十級風平均暴風半徑'; @override - String get restroomGradeExcellent => '特優級'; + String get restroomCategoryCommercial => '商業營業場所'; @override - String get restroomGradeGood => '優等級'; + String get aedRegion => '縣市區域'; @override - String get restroomGradeAverage => '普通級'; + String homeRainTrendLightStopping(int minutes) { + return '預計 $minutes 分鐘後停止下小雨'; + } @override - String get restroomGradePoor => '不合格'; + String get reportDetailInfo => '詳細資訊'; @override - String get shelterAddressLabel => '地址'; + String get mapNavWind => '風向'; @override - String get shelterCapacityLabel => '收容人數'; + String get windForecastOverlayMenuTooltip => '風場預報圖層選項'; @override - String shelterCapacityValue(int n) { - return '$n 人'; - } + String get dataWeatherRankingSubtitle => '即時觀測排行'; @override - String get shelterCategoryLabel => '適用災害'; + String homeRainTrendMinute(int minute) { + return '$minute分'; + } @override - String get shelterIndoorLabel => '室內收容'; + String get rainInterval6h => '6 時'; @override - String get shelterOutdoorLabel => '室外收容'; + String get restroomTypeUnspecified => '未設定'; @override - String get shelterVulnerableOkLabel => '適合避難弱者安置'; + String get typhoonOverlayProbabilityHint => '會隱藏預測圓錐'; @override - String get dpmYes => '是'; + String get mapLayerSatelliteGlobalOutline => '國界'; @override - String get dpmNo => '否'; + String get mapNavTemperature => '溫度'; @override - String get stationSheetEmpty => '點選任一測站查看觀測值'; + String get typhoonLegendForecastPoint => '預測點'; @override - String monitorDelay(String value) { - return '延遲 $value s'; - } + String get reportListYesterday => '昨天'; @override - String get monitorWaiting => '等待資料…'; + String get moreSectionLinks => '相關連結'; @override - String mapLegendUnit(String unit) { - return '單位:$unit'; - } + String get feedOffline => '連線中斷'; @override - String get typhoonLegendPast => '實際路徑'; + String get mapLayerStyleBd => 'Dvorak BD'; @override - String get typhoonIntensityTd => '熱帶性低氣壓'; + String get moreSectionDisplay => '顯示'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get rainInterval3d => '3 日'; @override - String typhoonPickerTd(String no) { - return '熱帶性低氣壓 TD $no'; - } + String get defaultMapLayerSubtitle => '開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get aedDescription => '備註'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get typhoonOverlayWeatherRadarTooltip => '雷達回波(對齊颱風報文時間)'; @override - String get typhoonIntensityMild => '輕度颱風'; + String get onboardingPermLocationDesc => '依你所在位置推送在地警報。'; @override - String get typhoonIntensityModerate => '中度颱風'; + String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; @override - String get typhoonIntensityIntense => '強烈颱風'; + String get homeActiveEventsEmpty => '目前沒有生效中的事件'; @override - String get typhoonLegendForecast => '預測路徑'; + String get typhoonLabelPosition => '中心位置'; @override - String get typhoonLegendForecastPoint => '預測點'; + String get weatherRankingBy => '依'; @override - String get typhoonLegendCurrent => '目前中心'; + String get typhoonIntensityMild => '輕度颱風'; @override - String get typhoonLegendCone => '預測圓錐'; + String get windForecastGlobalOutlineHint => '各國國界外框'; @override - String get mapLegendExpand => '圖例'; + String get rainInterval1h => '1 時'; @override - String get mapLegendCollapse => '收合圖例'; + String get eewLocalIntensity => '所在地預估'; @override - String get mapMyLocation => '我的位置'; + String get mapLayerRadar => '雷達合成回波圖'; @override - String get mapResetNorth => '回到北方'; + String get restroomCategoryReligious => '宗教禮儀場所'; @override - String get typhoonLegendCircle15 => '七級風暴風圈'; + String get meshtasticRole => '角色'; @override - String get typhoonLegendCircleAvg => '平均圓'; + String get mapLayerSatelliteCloudCloudy => '有雲'; @override - String get typhoonLegendCircle25 => '十級風暴風圈'; + String get skyTimeSunrise => '日出'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return '東北 $ne · 東南 $se · 西南 $sw · 西北 $nw km'; - } + String get meshtasticNoMessages => '尚無訊息'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get onboardingPermNotifyDesc => '在地震、天氣與災害發生時,即時傳遞警報通知。'; @override - String get typhoonLegendProbability => '侵襲機率'; + String get radarTownOutline => '鄉鎮界線'; @override - String get typhoonLegendWarningAreas => '警報區域'; + String get mapLayerStyleSection => '顯示樣式'; @override - String get typhoonOverlayMenuTooltip => '颱風圖層選項'; + String get disasterMapOverlayMenuTooltip => '防災地圖圖層'; @override - String get typhoonOverlaySectionStorm => '暴風圈'; + String get moreGooglePlay => 'Google Play'; @override - String get typhoonOverlaySectionExtra => '覆蓋層'; + String get meshtasticOnline => '近期聽到'; @override - String get typhoonOverlayStormBandSubtitle => '含平均圓'; + String get typhoonLabelSw => '西南側'; @override - String get typhoonOverlayProbabilityHint => '會隱藏預測圓錐'; + String typhoonForecastLead(String hours) { + return '預測 +$hours 小時'; + } @override - String get typhoonOverlayProbabilityTooltip => '顯示侵襲機率(會隱藏預測圓錐)'; + String get dpmDisasterTsunami => '海嘯'; @override - String get typhoonOverlayWarningTooltip => '標示警報區域縣市'; + String get changelogTypeStable => '正式'; @override - String get typhoonOverlayStormL7Tooltip => '七級暴風圈+平均圓(紫色)'; + String get mapLayerSatelliteTransparentClear => '晴空 = 透明,顯示底圖'; @override - String get typhoonOverlayStormL10Tooltip => '十級暴風圈+平均圓(黃色)'; + String get mapOverlaySectionReference => '參考圖層'; @override - String get typhoonOverlaySectionWeather => '天氣底圖'; + String get mapLayerSatelliteB02 => 'ひまわり 可見光-綠(B02)'; @override - String get typhoonOverlayWeatherNone => '無'; + String get reportListLocalFelt => '小區域有感'; @override - String get typhoonOverlayWeatherHint => '對齊報文時間'; + String get weatherRankingEmpty => '目前沒有可排序的觀測'; @override - String get typhoonOverlayWeatherNoneTooltip => '不疊雷達或紅外線'; + String get notifySectionOther => '其他'; @override - String get typhoonOverlayWeatherRadarTooltip => '雷達回波(對齊颱風報文時間)'; + String weatherRankingMeta(String time, int count) { + return '資料時間:$time\n共 $count 觀測點'; + } @override - String get typhoonOverlayWeatherSatelliteTooltip => '紅外線(對齊颱風報文時間)'; + String get onboardingTermsAgree => '我已閱讀並同意服務條款'; @override - String get typhoonWarningTitle => '颱風警報'; + String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(無植被)'; @override - String typhoonWarningAreas(String areas) { - return '警戒區域:$areas'; - } + String get notifyOptLocalIntensity4 => '所在地震度4以上'; @override - String get typhoonTrackDetail => '路徑詳情'; + String get eewArrived => '已抵達'; @override - String get typhoonHistoryTitle => '資料時間'; + String get meshtasticNoDevices => '找不到 Meshtastic 裝置'; @override - String get typhoonHistoryLive => '即時'; + String get mapLayerCategoryLife => '生活'; @override - String get typhoonSatelliteTitle => '衛星雲圖'; + String get reportFilterSortIntensity => '震度'; @override - String get typhoonOverlayForecastCallouts => '預測點資訊'; + String get typhoonMotion => '移動'; @override - String get typhoonOverlayForecastCalloutsTooltip => '放大時顯示預測點詳細卡片'; + String get meshtasticStateDisconnected => '未連線'; @override - String get dpmFilterSectionRestroom => '場所類型'; + String get typhoonIntensityIntense => '強烈颱風'; @override - String get dpmFilterSectionRestroomType => '廁所類型'; + String get mapLayerOrderTitle => '調整圖層順序'; @override - String get dpmFilterSectionShelter => '避難所災害類型'; + String get dpmYes => '是'; @override - String get dpmDisasterFlood => '水災'; + String get meshtasticNoHistory => '歷史紀錄還不夠'; @override - String get dpmDisasterEarthquake => '震災'; + String get reportDetailLocalIntensityUnavailable => '沒有震度訊息'; @override - String get dpmDisasterLandslide => '土石流'; + String get mapLayerWindForecastGfs => 'GFS'; @override - String get dpmDisasterTsunami => '海嘯'; + String get reportListDepthUnit => '公里'; @override - String get dpmDisasterSlope => '坡地災害'; + String get reportFilterDepth => '深度'; @override - String get dpmDisasterNuclear => '核子事故'; + String get onboardingScrollHint => '往下捲動以繼續'; @override - String get skyTime => '天空時間'; + String get mapNavQpesums => '預報'; @override - String get skyTimeAuto => '自動'; + String get navMap => '地圖'; @override - String get skyTimeDawn => '黎明'; + String get notifyAdvisory => '天氣警特報'; @override - String get skyTimeSunrise => '日出'; + String get reportFilterReset => '重設'; @override - String get skyTimeMorning => '上午'; + String get mapLayerSatelliteMndwi => 'ひまわり 改良水體指數'; @override - String get skyTimeNoon => '正午'; + String get typhoonOverlaySectionStorm => '暴風圈'; @override - String get skyTimeAfternoon => '下午'; + String get moonPhaseFull => '滿月'; @override - String get skyTimeGolden => '黃金時刻'; + String get moonPhaseWaningGibbous => '虧凸月'; @override - String get skyTimeSunset => '日落'; + String get weatherDynamicStateSubtitle => '覆寫主頁背景天氣'; @override - String get skyTimeDusk => '暮色'; + String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; @override - String get skyTimeNight => '夜晚'; + String typhoonDataTime(String time) { + return '資料時間\n$time'; + } @override - String get weatherModeCloudy => '多雲'; + String get restroomTypeAccessible => '無障礙廁所'; @override - String get weatherModeOvercast => '陰天'; + String get moreSectionAbout => '關於'; @override - String get weatherModeSnow => '下雪'; + String get meshtasticSelectDevice => '選擇裝置'; @override - String get weatherModeSand => '沙塵'; + String get onboardingIntroBody => + 'DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷雨即時訊息、天氣警特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。'; @override - String get radarScanRange => '顯示掃描範圍'; + String get shelterCapacityLabel => '收容人數'; @override - String get radarScanRangeSubtitle => '標示四座雷達實際觀測到的範圍。'; + String get reportDetailImage => '地震報告圖'; @override - String get radarScanRangeHint => '框外空白代表未觀測'; + String get meshtasticStateConfiguring => '設定中…'; @override - String get radarOverlayMenuTooltip => '雷達圖層選項'; + String get typhoonLabelGaleAvg => '七級風平均暴風半徑'; @override - String get radarCountyOutline => '縣市界線'; + String get onboardingPermNotify => '通知'; @override - String get radarGlobalOutline => '國界'; + String get meshtasticClearMessages => '清除訊息'; @override - String get radarGlobalOutlineHint => '各國國界外框'; + String get meshtasticNotifyMessages => '新訊息通知'; @override - String get radarCountyOutlineHint => '畫在回波之上'; + String get defaultMapLayerSettings => '地圖預設圖層'; @override - String get radarCountyOutlineSubtitle => '讓縣市界線在雷達回波下仍然清楚。'; + String get moreSectionNotify => '通知'; @override - String get radarTownOutline => '鄉鎮界線'; + String get notifyUnavailable => '推播尚未就緒,請稍後再試。'; @override - String get radarTownOutlineHint => '較細的分區'; + String get mapLayerOrderReset => '回復預設順序'; @override - String get radarTownOutlineSubtitle => '讓鄉鎮界線在雷達回波下仍然清楚。'; + String get dpmAddress => '地址'; @override - String get qpesumsOverlayMenuTooltip => '定量降水預報圖層選項'; + String get weatherRankingMergeCounty => '縣市'; @override - String get windForecastOverlayMenuTooltip => '風場預報圖層選項'; + String get moreSectionApp => '取得 App'; @override - String get windForecastCountyOutlineHint => '繪製於風場之上'; + String get reportFilterIntensityInfoLegacyBody => '震度僅 0–7,沒有 5弱/5強/6弱/6強。'; @override - String get windForecastGlobalOutlineHint => '各國國界外框'; + String get mapLayerSatelliteSst => 'ひまわり 海表溫度'; @override - String get windForecastTownOutlineHint => '更細的網格'; + String get qpesumsOverlayMenuTooltip => '定量降水預報圖層選項'; @override - String eewSerial(int serial) { - return '第 $serial 報'; - } + String get mapTimelineFuture => '未來'; @override - String get eewMaxIntensity => '最大震度'; + String get typhoonLegendCircleAvg => '平均圓'; @override - String get eewLocalIntensity => '所在地預估'; + String reportFilterDepthKm(String depth) { + return '$depth 公里'; + } @override - String get eewSWave => '震波'; + String get typhoonLabelSe => '東南側'; @override - String get eewArrived => '已抵達'; + String get radarTownOutlineHint => '較細的分區'; @override String eewCountdown(int seconds) { return '$seconds 秒'; } -} -/// The translations for Chinese, as used in Taiwan (`zh_TW`). -class AppLocalizationsZhTw extends AppLocalizationsZh { - AppLocalizationsZhTw() : super('zh_TW'); + @override + String get typhoonLabelGust => '瞬間最大陣風'; @override - String get languageName => '繁體中文(臺灣)'; + String get mapAppGoogleMaps => 'Google Maps'; @override - String get navHome => '首頁'; + String get sponsorTerms => '使用條款'; @override - String get navEvents => '事件'; + String get restroomTypeGenderNeutral => '性別友善廁所'; @override - String get navMap => '地圖'; + String get notifyThunderstorm => '雷雨即時訊息'; @override - String get navData => '資料'; + String get skyTimeGolden => '黃金時刻'; @override - String get navEarthquake => '地震'; + String get moonAge => '月齡'; @override - String get dataSectionSeismic => '地震'; + String get meshtasticRadioSettings => 'LoRa'; @override - String get dataEarthquakeSubtitle => '地震報告'; + String weatherRankingAnalysisCurrent(String value) { + return '當下 $value°C'; + } @override - String get dataSectionWeather => '氣象'; + String get moreGithub => 'ExpTech GitHub'; @override - String get dataWeatherRankingSubtitle => '即時觀測排行'; + String get homeForecastUnavailable => '選擇鄉鎮後可查看預報'; @override - String get weatherRankingTitle => '觀測排行'; + String get mapLayers => '圖層'; @override - String weatherRankingMeta(String time, int count) { - return '資料時間:$time\n共 $count 觀測點'; - } + String get meshtasticHardware => '硬體'; @override - String get weatherRankingEmpty => '目前沒有可排序的觀測'; + String get languageSettings => '語言設定'; @override - String get weatherRankingBy => '依'; + String get dpmDisasterNuclear => '核子事故'; @override - String get weatherRankingHighest => '最高'; + String get language => '語言'; @override - String get weatherRankingLowest => '最低'; + String homeForecastFeelsLike(String temp) { + return '體感 $temp°'; + } @override - String get weatherRankingMergeTo => '合併至'; + String get typhoonOverlayWeatherHint => '對齊報文時間'; @override - String get weatherRankingMergeTown => '鄉鎮'; + String get skyTimeDawn => '黎明'; @override - String get weatherRankingMergeCounty => '縣市'; + String get skyTimeAfternoon => '下午'; @override - String get weatherRankingWind => '風速'; + String get meshtasticLastHeard => '最後聽到'; @override - String get weatherRankingGust => '陣風'; + String get typhoonWarningTitle => '颱風警報'; @override - String get weatherRankingTempExtremes => '溫度極值'; + String get moreSourceCode => '原始碼'; + + @override + String get mapLayerCategoryWeather => '氣象觀測'; + + @override + String get mapLayerSatelliteB09 => 'ひまわり 中層水氣(B09)'; @override - String get weatherRankingExtremeHigh => '今日最高'; + String get windForecastTownOutlineHint => '更細的網格'; @override - String get weatherRankingExtremeLow => '今日最低'; + String get mapLayerSatelliteCloudmask => 'ひまわり 雲遮罩'; @override - String get weatherRankingExtremeRange => '日溫差'; + String get mapAppCopyCoordinates => '複製座標'; @override - String weatherRankingRecordedAt(String time) { - return '記錄於 $time'; - } + String get reportFilterIntensityInfoIntro => + '中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。'; @override - String weatherRankingAnalysisCurrent(String value) { - return '當下 $value°C'; - } + String get mapNavEarthquake => '地震'; @override - String weatherRankingAnalysisHigh(String value) { - return '最高 $value'; - } + String get typhoonGust => '陣風'; @override - String weatherRankingAnalysisLow(String value) { - return '最低 $value'; - } + String get restroomGradeAverage => '普通級'; @override - String weatherRankingAnalysisRange(String value) { - return '溫差 $value°C'; - } + String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷雲/雲高'; @override - String get reportListEmpty => '目前沒有地震報告'; + String get onboardingPermBackgroundDesc => '選擇「一律允許」,關閉 App 也能推送在地警報。'; @override - String get reportListEmptyFiltered => '沒有符合條件的地震報告'; + String get mapTimelineForecast => '預報'; @override - String reportListMeta(String magnitude, String depth) { - return 'M$magnitude · $depth 公里'; - } + String get restroomTypeLabel => '廁所類型'; @override - String reportListMagnitude(String magnitude) { - return 'M$magnitude'; - } + String get navEarthquake => '地震'; @override - String get reportListDepthUnit => '公里'; + String get typhoonOverlayStormL10Tooltip => '十級暴風圈+平均圓(黃色)'; @override - String get reportListLocalFelt => '小區域有感'; + String get moonPhaseWaxingGibbous => '盈凸月'; @override - String get reportListToday => '今天'; + String get reportDetailTitle => '地震報告'; @override - String get reportListYesterday => '昨天'; + String get moreTremReport => 'TREM 檢知報告'; @override - String reportListDayCount(int count) { - return '$count'; + String weatherDataTime(String station, String time) { + return '$station ∙ 資料時間 $time'; } @override - String get reportListEnd => '已到最後一頁'; + String get meshtasticNoNodes => '尚未聽到任何節點'; @override - String get reportFilterTitle => '篩選'; + String get meshtasticViaMqtt => '經 MQTT(網際網路)'; @override - String get reportFilterSort => '排序方式'; + String get radarCountyOutline => '縣市界線'; @override - String get reportFilterSortTime => '時間'; + String get onboardingGranted => '已授權'; @override - String get reportFilterSortIntensity => '震度'; + String get commonClose => '關閉'; @override - String get reportFilterSortMagnitude => '規模'; + String get restroomGradeLabel => '等級'; @override - String get reportFilterSortDepth => '深度'; + String get rainIntervalNow => '今日'; @override - String get reportFilterOrderDesc => '降序'; + String get changelogCurrentVersion => '目前版本'; @override - String get reportFilterOrderAsc => '升序'; + String get typhoonLabelPressure => '中心氣壓'; @override - String get reportFilterIntensity => '震度'; + String get typhoonOverlayForecastCalloutsTooltip => '放大時顯示預測點詳細卡片'; @override - String get reportFilterIntensityInfoTitle => '震度新制與舊制'; + String get aedOpenRemark => '開放時間備註'; @override - String get reportFilterIntensityInfoIntro => - '中央氣象署自 2020 年 1 月 1 日(臺北時間)起改用新制震度。'; + String get onboardingPermsBody => '為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。'; @override - String get reportFilterIntensityInfoLegacyTitle => '舊制(2020 以前)'; + String get typhoonOverlaySectionWeather => '天氣底圖'; @override - String get reportFilterIntensityInfoLegacyBody => '震度僅 0–7,沒有 5弱/5強/6弱/6強。'; + String get notifyOptWeatherLocal => '接收所在地'; @override - String get reportFilterIntensityInfoModernTitle => '新制(2020 起)'; + String get mapNavRain => '雨量'; @override - String get reportFilterIntensityInfoModernBody => - '震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。'; + String get moonDays => '天'; @override - String get reportFilterMagnitude => '規模'; + String mapLegendUnit(String unit) { + return '單位:$unit'; + } @override - String get reportFilterDepth => '深度'; + String get weatherModeClear => '晴天'; @override - String reportFilterDepthKm(String depth) { - return '$depth 公里'; - } + String get meshtasticRadio => '電台'; @override - String get reportFilterDate => '日期'; + String get commonEmpty => '沒有資料'; @override - String get reportFilterDatePick => '選擇日期'; + String get mapLayerSatelliteB01 => 'ひまわり 可見光-藍(B01)'; @override - String get reportFilterDateStartNote => '開始日:當日 00:00(臺北時間)'; + String get meshtasticExternalPower => '外部供電'; @override - String get reportFilterDateEndNote => '結束日:當日 24:00(臺北時間)'; + String get moonPhaseLastQuarter => '下弦月'; @override - String reportFilterRange(String start, String end) { - return '$start – $end'; - } + String get reportFilterOrderAsc => '升序'; @override - String get reportFilterLocation => '地點'; + String get reportFilterApply => '套用'; @override - String get reportFilterLocationHint => '例如:花蓮、東部海域'; + String get reportDetailImageUnavailable => '報告圖尚未提供'; @override - String get reportFilterAny => '不限'; + String get weatherRankingHighest => '最高'; @override - String get reportFilterApply => '套用'; + String get reportDetailReplay => '重播'; @override - String get reportFilterReset => '重設'; + String get mapLayerRestroom => '公廁'; @override - String get reportListSearch => '查詢'; + String get restroomCategoryWelfare => '社福機構、集會場所'; @override - String get reportDetailTitle => '地震報告'; + String get restroomGradeExcellent => '特優級'; @override - String reportDetailNumbered(String number) { - return '編號 $number 顯著有感地震'; - } + String get meshtasticLastSent => '最近送出'; @override - String get reportDetailLocalFelt => '小區域有感地震'; + String get meshtasticName => '名稱'; @override - String get reportDetailInfo => '詳細資訊'; + String get meshtasticScan => '掃描'; @override - String get reportDetailOriginTime => '發震時間'; + String get mapLayerCategoryForecast => '數值預報'; @override - String get reportDetailEpicenter => '震央座標'; + String get meshtasticChannelFailed => '無法設定 DPIP 頻道'; @override - String get reportDetailMagnitude => '地震規模'; + String get themeSystem => '跟隨系統'; @override - String get reportDetailDepth => '震源深度'; + String get mapLayerSatelliteNdvi => 'ひまわり 植生指數'; @override - String get reportDetailAreaIntensity => '各地震度'; + String get typhoonLegendForecast => '預測路徑'; @override - String get reportDetailLocalIntensity => '所在地的震度'; + String typhoonValueHpa(String n) { + return '$n 百帕'; + } @override - String get reportDetailLocalIntensityUnavailable => '沒有震度訊息'; + String get weatherPrecipitation => '降水量'; @override - String get reportDetailSortByIntensity => '依震度排序'; + String get moonNextFullMoon => '下次滿月'; @override - String get reportDetailSortByCounty => '依縣市排序'; + String get dpmSheetEmpty => '點選地圖上的標記查看詳情'; @override - String get reportDetailImage => '地震報告圖'; + String get onboardingSkipLeave => '仍要略過'; @override - String get reportDetailImageUnavailable => '報告圖尚未提供'; + String get onboardingBack => '上一步'; @override - String get reportDetailOpenReport => '報告頁面'; + String get aedPlaceDesc => '放置位置說明'; @override - String get reportDetailReplay => '重播'; + String get onboardingSkipTitle => '尚未完成授權'; @override - String get navMore => '更多'; + String get restroomTypeFamily => '親子廁所'; @override - String get appLogs => 'App 日誌'; + String typhoonValueKm(String n) { + return '$n 公里'; + } @override - String get changelogTitle => '更新日誌'; + String get typhoonPressure => '氣壓'; @override - String get changelogEmpty => '目前沒有更新日誌'; + String get onboardingPermBattery => '省電白名單'; @override - String get changelogTypePrerelease => '公測'; + String get typhoonLabelNw => '西北側'; @override - String get changelogTypeStable => '正式'; + String get dpmDisasterFlood => '水災'; @override - String get changelogCurrentVersion => '目前版本'; + String get moonPhaseWaxingCrescent => '眉月'; @override - String get changelogVersionDetails => '版本資訊'; + String get restroomCategoryLeisure => '休閒娛樂場所'; @override - String get changelogBodyEmpty => '此版本沒有說明。'; + String get mapLayerTemperature => '溫度'; @override - String get mapPlaceholderDisabled => '地圖(暫時停用)'; + String get aedCategory => '場所分類'; @override - String get moreSectionRegion => '地區'; + String get meshtasticChannels => '頻道'; @override - String get moreSectionNotify => '通知'; + String get monitorWaiting => '等待資料…'; @override - String get moreSectionDisplay => '顯示'; + String get typhoonOverlayForecastCallouts => '預測點資訊'; @override - String get regionManageTitle => '常用地區'; + String get reportDetailEpicenter => '震央座標'; @override - String get regionAddButton => '新增地區'; + String get meshtasticVoltage => '電壓'; @override - String get regionEmpty => '尚未新增常用地區'; + String get mapLayerMeshtasticSubtitle => '電台聽到過的 LoRa 網狀網路節點'; @override - String get regionSelectTitle => '選擇地區'; + String get mapLayerWind => '風向'; @override - String regionSelectCount(int count, int max) { - return '已選 $count/$max'; - } + String get reportDetailMagnitude => '地震規模'; @override - String regionSelectFull(int max) { - return '最多只能選擇 $max 個地區'; - } + String get reportDetailAreaIntensity => '各地震度'; @override - String get regionEdit => '修改'; + String get rainInterval12h => '12 時'; @override - String get moreSectionAdvanced => '進階'; + String reportListMagnitude(String magnitude) { + return 'M$magnitude'; + } @override - String get moreDeveloper => '除錯資訊'; + String get dpmDisasterLandslide => '土石流'; @override - String get experimentalFeatures => '實驗性功能'; + String get notifyMonitor => '強震監視器'; @override - String get moreSectionLinks => '相關連結'; + String get onboardingStart => '開始使用'; @override - String get moreCwaEew => '中央氣象署強震即時警報'; + String sponsorPerMonth(String price) { + return '$price / 月'; + } @override - String get moreTremReport => 'TREM 檢知報告'; + String get mapLayerPressure => '氣壓'; @override - String get moreServerStatus => '伺服器狀態'; + String get mapLayerSatelliteB04 => 'ひまわり 近紅外(B04)'; @override - String get moreAnnouncements => '公告'; + String get mapLayerSatelliteTransparentZero => '零差值 = 透明(無訊號)'; @override - String get moreDiscord => 'Discord 社群'; + String get shelterIndoorLabel => '室內收容'; @override - String get moreNotifyLog => 'DPIP 通知發送記錄'; + String get notifyOptOff => '關閉'; @override - String get moreLinkOpenFailed => '無法開啟連結'; + String get reportFilterSortTime => '時間'; @override - String get weatherDynamicState => '天氣動態狀態'; + String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; @override - String get weatherDynamicStateSubtitle => '覆寫主頁背景天氣'; + String get weatherModeThunderstorm => '雷雨'; @override - String get weatherModeAuto => '自動'; + String get homeViewOnMap => '前往地圖察看'; @override - String get weatherModeClear => '晴天'; + String get reportFilterIntensityInfoLegacyTitle => '舊制(2020 以前)'; @override - String get weatherModeRain => '雨天'; + String get typhoonLabelSpeed => '過去移動時速'; @override - String get weatherModeFog => '大霧'; + String mapAppOpenFailed(String app) { + return '無法開啟 $app'; + } @override - String get weatherModeThunderstorm => '雷雨'; + String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; @override - String get commonLoading => '載入中…'; + String get meshtasticReceived => '已接收'; @override - String get commonRetry => '重試'; + String get weatherRankingExtremeLow => '今日最低'; @override - String get commonError => '發生錯誤'; + String get mapLayerSatelliteB10 => 'ひまわり 低層水氣(B10)'; @override - String get commonFetchFailed => '無法獲取資料,請稍後重試'; + String get mapLayerSatelliteCloudProbablyCloudy => '可能有雲'; @override - String get commonEmpty => '沒有資料'; + String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(無水體)'; @override - String get feedConnecting => '連線中…'; + String get shelterCategoryLabel => '適用災害'; @override - String get feedStale => '資料可能已過期'; + String get meshtasticStateConnecting => '連線中…'; @override - String get feedOffline => '連線中斷'; + String get moonTitle => '月亮'; @override - String get eewTitle => '地震速報'; + String get weatherRankingGust => '陣風'; @override - String get eewNone => '目前沒有地震速報'; + String get moreAppStore => 'App Store'; @override - String eewSummary(String magnitude, String depth) { - return '規模 $magnitude・深度 $depth 公里'; - } + String get dpmFilterSectionShelter => '避難所災害類型'; @override - String get regionNationwide => '全國'; + String get moreServerStatus => '伺服器狀態'; @override - String get regionCurrent => '所在地'; + String get notifySectionWeather => '天氣'; + + @override + String get meshtasticPreset => '調變預設'; @override - String get regionCurrentUnavailable => '無法取得所在地位置資訊'; + String get dataSectionSeismic => '地震'; @override - String get weatherPrecipitation => '降水量'; + String get changelogBodyEmpty => '此版本沒有說明。'; @override - String get weatherHumidity => '濕度'; + String get radarGlobalOutline => '國界'; @override - String weatherDataTime(String station, String time) { - return '$station ∙ 資料時間 $time'; - } + String get notifyEew => '緊急地震速報'; @override - String get homeViewOnMap => '前往地圖察看'; + String get regionNationwide => '全國'; @override - String get homeForecastTitle => '24小時預報'; + String get moreNotifyLog => 'DPIP 通知發送記錄'; @override - String homeForecastHighLow(String high, String low) { - return '高 $high° · 低 $low°'; - } + String get regionCurrent => '所在地'; @override - String homeForecastPop(String pop) { - return '$pop%'; - } + String get dpmFilterSectionRestroom => '場所類型'; @override - String homeForecastFeelsLike(String temp) { - return '體感 $temp°'; - } + String get meshtasticNotConnected => '尚未連線至裝置'; @override - String homeForecastHumidity(String value) { - return '濕度 $value%'; - } + String get weatherModeSnow => '下雪'; @override - String homeForecastWind(String direction, String level) { - return '$direction · $level 級'; - } + String get mapLayerMeshtastic => 'Meshtastic 節點'; @override - String get homeForecastUnavailable => '選擇鄉鎮後可查看預報'; + String get moreDeveloper => '除錯資訊'; @override - String get homeForecastEmpty => '目前沒有預報資料'; + String get mapLayerSatelliteB14 => 'ひまわり 長波紅外線(B14)'; @override - String get homeActiveEventsTitle => '生效中事件'; + String get meshtasticChannelUse => '頻道使用率'; @override - String get homeActiveEventsEmpty => '目前沒有生效中的事件'; + String get mapNavLightning => '閃電'; @override - String get homeRainTrendTitle => '近 1 小時降水趨勢'; + String get homeForecastEmpty => '目前沒有預報資料'; @override - String homeRainTrendMinute(int minute) { - return '$minute分'; - } + String get sponsorOneTime => '單次支援'; @override - String homeRainTrendUpdated(String time) { - return '更新 $time'; - } + String get mapLayerSatelliteBtdSplit => 'ひまわり 分割視窗'; @override - String get homeRainTrendNoData => '無資料'; + String get onboardingPermBackground => '背景定位'; @override - String get homeRainTrendScattered => '可能會有零星降雨'; + String get aedEmergencyPhone => '緊急聯絡電話'; @override - String get homeRainTrendLightSustained => '未來 1 小時會有持續小雨'; + String get dpmOpenInMaps => '開啟地圖'; @override - String homeRainTrendLightStopping(int minutes) { - return '預計 $minutes 分鐘後停止下小雨'; - } + String get meshtasticNotifyNodes => '新節點通知'; @override - String get homeRainTrendHeavySustained => '未來 1 小時會有持續大雨'; + String get onboardingPermCriticalDesc => '讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。'; @override - String homeRainTrendHeavyStopping(int minutes) { - return '預計 $minutes 分鐘後停止下大雨'; - } + String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,顯示底圖'; @override - String get mapLayers => '圖層'; + String get meshtasticSent => '已送出'; @override - String get mapLayerOrderTitle => '調整圖層順序'; + String get homeForecastTitle => '24小時預報'; @override - String get mapLayerOrderReset => '回復預設順序'; + String get typhoonLegendWarningAreas => '警報區域'; @override - String get mapLayerRadar => '雷達合成回波圖'; + String meshtasticExcludeMqttHidden(int count) { + return '已隱藏 $count 個'; + } @override - String get mapLayerSatellite => 'ひまわり 紅外線(B13)'; + String get notifyOptLocalIntensity1 => '所在地震度1以上'; @override - String get mapLayerSatelliteB01 => 'ひまわり 可見光-藍(B01)'; + String get mapTimelinePast => '歷史'; @override - String get mapLayerSatelliteB02 => 'ひまわり 可見光-綠(B02)'; + String get restroomTypeFemale => '女廁所'; @override - String get mapLayerSatelliteB03 => 'ひまわり 可見光-紅(B03)'; + String get reportListToday => '今天'; @override - String get mapLayerSatelliteB04 => 'ひまわり 近紅外(B04)'; + String get meshtasticTapNode => '點選節點查看詳細資訊'; @override - String get mapLayerSatelliteB05 => 'ひまわり 近紅外(B05)'; + String get commonLoading => '載入中…'; @override - String get mapLayerSatelliteB06 => 'ひまわり 近紅外(B06)'; + String get typhoonIntensityModerate => '中度颱風'; @override - String get mapLayerSatelliteB07 => 'ひまわり 短波紅外(B07)'; + String get typhoonWind => '風速'; @override - String get mapLayerSatelliteB08 => 'ひまわり 上層水氣(B08)'; + String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; @override - String get mapLayerSatelliteB09 => 'ひまわり 中層水氣(B09)'; + String get rainInterval3h => '3 時'; @override - String get mapLayerSatelliteB10 => 'ひまわり 低層水氣(B10)'; + String get reportListSearch => '查詢'; @override - String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/雲相(B11)'; + String get mapLayerCategorySatellite => '衛星'; @override - String get mapLayerSatelliteB12 => 'ひまわり 臭氧(B12)'; + String get meshtasticChannelReady => 'DPIP 頻道已就緒'; @override - String get mapLayerSatelliteB13 => 'ひまわり 紅外線(B13)'; + String get reportFilterLocation => '地點'; @override - String get mapLayerSatelliteB14 => 'ひまわり 長波紅外線(B14)'; + String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; @override - String get mapLayerSatelliteB15 => 'ひまわり 長波紅外線(B15)'; + String get typhoonIntensityTd => '熱帶性低氣壓'; @override - String get mapLayerSatelliteB16 => 'ひまわり 二氧化碳(B16)'; + String get reportFilterDate => '日期'; @override - String get mapLayerSatelliteTruecolor => 'ひまわり 真彩色'; + String get sponsorRestoreUnavailable => '無法連線至商店,請稍後再試'; @override - String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; + String homeForecastPop(String pop) { + return '$pop%'; + } @override - String get mapLayerSatelliteAsh => 'ひまわり 火山灰'; + String get regionEmpty => '尚未新增常用地區'; @override - String get mapLayerSatelliteDust => 'ひまわり 沙塵'; + String get onboardingPermBatteryDesc => '允許 DPIP 在背景持續運作,避免警報延遲或漏收。'; @override - String get mapLayerSatelliteAirmass => 'ひまわり 氣團'; + String get mapNavDisaster => '防災'; @override - String get mapLayerSatelliteNightmicrophysics => 'ひまわり 夜間微物理'; + String get radarScanRangeSubtitle => '標示四座雷達實際觀測到的範圍。'; @override - String get mapLayerSatelliteWatervapor => 'ひまわり 水氣'; + String get aedHoursSunday => '週日開放時間'; @override - String get mapLayerSatelliteBtdSplit => 'ひまわり 分割視窗'; + String get reportDetailOriginTime => '發震時間'; @override - String get mapLayerSatelliteBtdFog => 'ひまわり 夜間霧'; + String get trendNoData => '沒有趨勢資料'; @override - String get mapLayerSatelliteBtdWvirw => 'ひまわり 過衝雲頂'; + String get onboardingPermLocation => '定位'; @override - String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/雲相'; + String get moreDiscord => 'Discord 社群'; @override - String get mapLayerSatelliteBtdCo2 => 'ひまわり 卷雲/雲高'; + String get mapNavPressure => '氣壓'; @override - String get mapLayerSatelliteBtdOzone => 'ひまわり 對流層頂'; + String get mapLayerSatelliteB13 => 'ひまわり 紅外線(B13)'; @override - String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂溫度'; + String typhoonTdNo(String no) { + return 'TD $no'; + } @override - String get mapLayerSatelliteCloudmask => 'ひまわり 雲遮罩'; + String get changelogEmpty => '目前沒有更新日誌'; @override - String get mapLayerSatelliteSst => 'ひまわり 海表溫度'; + String get reportFilterDateStartNote => '開始日:當日 00:00(臺北時間)'; @override - String get mapLayerSatelliteNdvi => 'ひまわり 植生指數'; + String get eewTitle => '地震速報'; @override - String get mapLayerSatelliteNdwi => 'ひまわり 水體指數'; + String get mapLayerWindForecastEcmwf => 'ECMWF'; @override - String get mapLayerSatelliteMndwi => 'ひまわり 改良水體指數'; + String regionSelectCount(int count, int max) { + return '已選 $count/$max'; + } @override - String get mapLayerSatelliteGlobalOutline => '國界'; + String get mapLayerSatelliteBtdSo2 => 'ひまわり 二氧化硫/雲相'; @override - String get mapLayerSatelliteRgbComposite => 'RGB 合成(JMA 配方)'; + String get meshtasticStateError => '錯誤'; @override - String get mapLayerSatelliteCloudClear => '晴空'; + String get weatherModeOvercast => '陰天'; @override - String get mapLayerSatelliteCloudProbablyClear => '可能晴空'; + String get reportDetailDepth => '震源深度'; @override - String get mapLayerSatelliteCloudProbablyCloudy => '可能有雲'; + String get typhoonOverlayWarningTooltip => '標示警報區域縣市'; @override - String get mapLayerSatelliteCloudCloudy => '有雲'; + String get reportFilterDatePick => '選擇日期'; @override - String get mapLayerSatelliteTransparentWarm => '晴空(暖端) = 透明,顯示底圖'; + String get onboardingSkipStay => '返回授權'; @override - String get mapLayerSatelliteTransparentReflectance => '低反射率/夜間 = 透明,顯示底圖'; + String get commonFetchFailed => '無法獲取資料,請稍後重試'; @override - String get mapLayerSatelliteTransparentZero => '零差值 = 透明(無訊號)'; + String get shelterOutdoorLabel => '室外收容'; @override - String get mapLayerSatelliteTransparentNight => '夜間 = 透明,顯示底圖'; + String get meshtasticStateConnected => '已連線'; @override - String get mapLayerSatelliteTransparentNoData => '無資料(陸地) = 透明'; + String get mapNavRadar => '雷達'; @override - String get mapLayerSatelliteTransparentNoVegetation => '< 0.1 = 透明(無植被)'; + String get mapLayerSatelliteCloudClear => '晴空'; @override - String get mapLayerSatelliteTransparentNoWater => '≤ 0 = 透明(無水體)'; + String eewSummary(String magnitude, String depth) { + return '規模 $magnitude・深度 $depth 公里'; + } @override - String get mapLayerSatelliteTransparentClear => '晴空 = 透明,顯示底圖'; + String get locationBannerPermission => '尚未授權定位,無法針對你的所在地推送警報。'; @override - String get mapLayerStyleSection => '顯示樣式'; + String get typhoonOverlayWeatherNoneTooltip => '不疊雷達或紅外線'; @override - String get mapLayerStyleTooltip => '顯示樣式'; + String get radarCountyOutlineHint => '畫在回波之上'; @override - String get mapLayerStyleGray => '灰階(JMA)'; + String get windForecastCountyOutlineHint => '繪製於風場之上'; @override - String get mapLayerStyleGrayTooltip => '氣象廳灰階慣例:溫度越低越白'; + String get homeRainTrendTitle => '近 1 小時降水趨勢'; @override - String get mapLayerStyleJma => '雲頂強調(JMA)'; + String get moonPhaseFirstQuarter => '上弦月'; @override - String get mapLayerStyleJmaTooltip => '灰階為底,−40 °C 以下上色,凸顯雲頂高度'; + String get mapLayerCategoryTyphoon => '颱風'; @override - String get mapLayerStyleBd => 'Dvorak BD'; + String get meshtasticUtilization => '空中工時(24 小時)'; @override - String get mapLayerStyleBdTooltip => 'Dvorak BD 曲線——熱帶氣旋強度分析的階梯灰階'; + String get restroomTypeMixed => '混合廁所'; @override - String get mapLayerQpesums => '未來 1 小時降水預報'; + String get restroomGradeGood => '優等級'; @override - String get mapLayerLightning => '閃電'; + String get notifyTsunami => '海嘯資訊'; @override - String lightningLegendCg(int minutes) { - return '對地 · $minutes 分內'; - } + String get navData => '資料'; @override - String lightningLegendCc(int minutes) { - return '雲間 · $minutes 分內'; - } + String get mapLayerSatelliteBtdWvirw => 'ひまわり 過衝雲頂'; @override - String get mapTimelineNow => '現在'; + String get meshtasticReadingAge => '數值時間'; @override - String get mapTimelinePast => '歷史'; + String get mapAppCallFailed => '此裝置無法撥打電話'; @override - String get mapTimelineFuture => '未來'; + String get reportFilterAny => '不限'; @override - String get mapTimelineObserved => '觀測'; + String get weatherRankingMergeTo => '合併至'; @override - String get mapTimelineForecast => '預報'; + String get notifyIntensity => '震度速報'; @override - String mapTimelineDataTime(String time) { - return '資料時間 $time'; + String typhoonTimeChip(String day, String hour) { + return '$day日$hour時'; } @override - String get notifySettingsMenu => '通知設定'; + String get rainIntervalMenu => '累積時段'; @override - String get notifyTitle => '通知'; + String get reportDetailLocalFelt => '小區域有感地震'; @override - String get notifyUnavailable => '推播尚未就緒,請稍後再試。'; + String get meshtasticDevice => '裝置'; @override - String get notifySetFailed => '設定失敗,請稍後再試。'; + String get onboardingGrant => '授權'; @override - String get notifySectionEew => '地震速報'; + String get weatherModeRain => '雨天'; @override - String get notifySectionEarthquake => '地震'; + String get shelterVulnerableOkLabel => '適合避難弱者安置'; @override - String get notifySectionWeather => '天氣'; + String get stationSheetEmpty => '點選任一測站查看觀測值'; @override - String get notifySectionTsunami => '海嘯'; + String get typhoonLegendProbability => '侵襲機率'; @override - String get notifySectionOther => '其他'; + String get reportFilterMagnitude => '規模'; @override - String get notifyEew => '緊急地震速報'; + String get skyTimeMorning => '上午'; @override - String get notifyMonitor => '強震監視器'; + String get experimentalFeatures => '實驗性功能'; @override - String get notifyReport => '地震報告'; + String get onboardingTermsBody => + '使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。'; @override - String get notifyIntensity => '震度速報'; + String get reportFilterTitle => '篩選'; @override - String get notifyThunderstorm => '雷雨即時訊息'; + String get onboardingPermCritical => '重大通知'; @override - String get notifyAdvisory => '天氣警特報'; + String trendCumulativeTotal(String total) { + return '累計 $total mm'; + } @override - String get notifyEvacuation => '防災資訊'; + String get languageName => '繁體中文(臺灣)'; @override - String get notifyTsunami => '海嘯資訊'; + String get reportListEmptyFiltered => '沒有符合條件的地震報告'; @override - String get notifyAnnouncement => '公告'; + String get meshtasticExcludeMqtt => '隱藏 MQTT 節點'; @override - String get notifyOptOff => '關閉'; + String get mapNavTyphoon => '颱風'; + + @override + String get weatherModeSand => '沙塵'; @override - String get notifyOptAll => '接收全部'; + String get typhoonSatelliteTitle => '衛星雲圖'; @override - String get notifyOptLocalIntensity4 => '所在地震度4以上'; + String get notifyReport => '地震報告'; @override - String get notifyOptLocalIntensity1 => '所在地震度1以上'; + String get mapAppCoordinatesCopied => '已複製座標'; @override - String get notifyOptWeatherLocal => '接收所在地'; + String get skyTimeNight => '夜晚'; @override - String get notifyOptTsunamiWarning => '只接收海嘯警報'; + String get sponsorRecommended => '推薦'; @override - String get notifyOptTsunamiAll => '海嘯消息、海嘯警報'; + String get mapLayerSatelliteB15 => 'ひまわり 長波紅外線(B15)'; @override - String get onboardingNext => '下一步'; + String get weatherRankingWind => '風速'; @override - String get onboardingBack => '上一步'; + String get feedStale => '資料可能已過期'; @override - String get onboardingScrollHint => '往下捲動以繼續'; + String homeForecastWind(String direction, String level) { + return '$direction · $level 級'; + } @override - String get onboardingIntroTitle => '歡迎使用 DPIP'; + String get navHome => '首頁'; @override - String get onboardingIntroBody => - 'DPIP 是與你並肩的防災夥伴,整合強震即時警報、地震報告、天氣與各類災害資訊,在關鍵時刻即時通知你。\n\n• 地震:強震即時警報、震度速報與地震報告\n• 天氣:雷雨即時訊息、天氣警特報\n• 海嘯與防災資訊\n\n接下來,我們會請你閱讀服務條款,並授權幾項讓 DPIP 能即時守護你的權限。'; + String get meshtasticRegionLabel => '地區'; @override - String get onboardingTermsTitle => '服務條款'; + String get mapLayerSatelliteCloudtop => 'ひまわり 雲頂溫度'; @override - String get onboardingTermsBody => - '使用 DPIP 前,請詳閱以下注意事項:\n\n• 任何資訊應以中央氣象署發布之內容為準。\n\n• 根據網路狀態、伺服器狀態、應用程式狀態、上游資料來源狀態等,有收不到資訊的可能性,我們會盡力避免此類情況,但不保證一定不會發生。\n\n• 強烈搖晃有機率比通知早抵達使用者所在地。\n\n• 地震速報為快速計算之結果,可能存在較大誤差,應理解並謹慎使用。\n\n• 任何不被官方所認可的行為均有可能承擔法律風險,請務必遵守相關規範。\n\n此外,為提供在地化警報,本服務會在前景及背景蒐集並上傳您的概略位置與裝置推播識別碼,僅用於決定應向您推送之警報。\n\n點選下方「同意並繼續」即表示您已閱讀、理解並同意上述事項。'; + String get moonTimelineCaption => '月相'; @override - String get onboardingTermsAgree => '我已閱讀並同意服務條款'; + String reportListMeta(String magnitude, String depth) { + return 'M$magnitude · $depth 公里'; + } @override - String get onboardingAgreeContinue => '同意並繼續'; + String get openSourceLicenses => '引用套件'; @override - String get onboardingPermsTitle => '權限授權'; + String get weatherRankingLowest => '最低'; @override - String get onboardingPermsBody => '為了在災害發生的第一時間通知你,請授權以下權限。你隨時可以在系統設定中變更。'; + String get reportFilterSortDepth => '深度'; @override - String get onboardingPermNotify => '通知'; + String mapTimelineDataTime(String time) { + return '資料時間 $time'; + } @override - String get onboardingPermNotifyDesc => '在地震、天氣與災害發生時,即時傳遞警報通知。'; + String get radarScanRange => '顯示掃描範圍'; @override - String get onboardingPermCritical => '重大通知'; + String get meshtasticHopLimit => '跳數上限'; @override - String get onboardingPermCriticalDesc => '讓危及生命的強震即時警報,即使在靜音或勿擾模式下也能發出聲響。'; + String weatherRankingAnalysisRange(String value) { + return '溫差 $value°C'; + } @override - String get onboardingPermLocation => '定位'; + String get weatherRankingExtremeHigh => '今日最高'; @override - String get onboardingPermLocationDesc => '依你所在位置推送在地警報。'; + String get changelogVersionDetails => '版本資訊'; @override - String get onboardingPermBackground => '背景定位'; + String get sponsorPrivacy => '隱私權政策'; @override - String get onboardingPermBackgroundDesc => '選擇「一律允許」,關閉 App 也能推送在地警報。'; + String get reportDetailLocalIntensity => '所在地的震度'; @override - String get onboardingPermBattery => '省電白名單'; + String get mapLayerSatelliteNaturalcolor => 'ひまわり 自然色'; @override - String get onboardingPermBatteryDesc => '允許 DPIP 在背景持續運作,避免警報延遲或漏收。'; + String get meshtasticAirtime => '發射佔空比'; @override - String get onboardingGrant => '授權'; + String shelterCapacityValue(int n) { + return '$n 人'; + } @override - String get onboardingGranted => '已授權'; + String lightningLegendCc(int minutes) { + return '雲間 · $minutes 分內'; + } @override - String get onboardingStart => '開始使用'; + String get meshtasticSendHint => '要廣播的訊息'; @override - String get language => '語言'; + String monitorDelay(String value) { + return '延遲 $value s'; + } @override - String get languageSettings => '語言設定'; + String get dpmNo => '否'; @override - String get languageSystem => '系統預設'; + String get mapLayerSatelliteB08 => 'ひまわり 上層水氣(B08)'; @override - String get locationBannerServiceOff => '定位服務已關閉,無法針對你的所在地推送警報。'; + String get meshtasticReconnecting => '重新連線中…'; @override - String get locationBannerPermission => '尚未授權定位,無法針對你的所在地推送警報。'; + String get radarTownOutlineSubtitle => '讓鄉鎮界線在雷達回波下仍然清楚。'; @override - String get locationBannerFix => '開啟設定'; + String get typhoonOverlayWeatherSatelliteTooltip => '紅外線(對齊颱風報文時間)'; @override - String get notifyBannerDisabled => '通知已關閉,將收不到災害警報。'; + String get radarScanRangeHint => '框外空白代表未觀測'; @override - String get onboardingSkipTitle => '尚未完成授權'; + String typhoonPickerTd(String no) { + return '熱帶性低氣壓 TD $no'; + } @override - String get onboardingSkipBody => - '未授權定位與通知,DPIP 將無法即時通知你所在地的地震與災害。你仍可稍後在設定中開啟。'; + String get mapLayerSatelliteWatervapor => 'ひまわり 水氣'; @override - String get onboardingSkipStay => '返回授權'; + String get regionAddButton => '新增地區'; @override - String get onboardingSkipLeave => '仍要略過'; + String get displaySettings => '顯示設定'; @override - String get moreYoutube => 'YouTube'; + String get restroomGradePoor => '不合格'; @override - String get moreGithub => 'ExpTech GitHub'; + String get restroomCategoryTourist => '觀光地區及風景區'; @override - String get moreSourceCode => '原始碼'; + String get locationBannerServiceOff => '定位服務已關閉,無法針對你的所在地推送警報。'; @override - String get moreSectionApp => '取得 App'; + String get mapLayerStyleTooltip => '顯示樣式'; @override - String get moreGooglePlay => 'Google Play'; + String lightningLegendCg(int minutes) { + return '對地 · $minutes 分內'; + } @override - String get moreAppStore => 'App Store'; + String get skyTimeAuto => '自動'; @override - String get displaySettings => '顯示設定'; + String get appLogs => 'App 日誌'; @override - String get defaultMapLayerSettings => '地圖預設圖層'; + String get feedConnecting => '連線中…'; @override - String get defaultMapLayerSubtitle => '開啟地圖分頁時顯示此圖層,底部導覽列圖示與文字會一併更新。'; + String get notifyBannerDisabled => '通知已關閉,將收不到災害警報。'; @override - String get mapNavRadar => '雷達'; + String get weatherHumidity => '濕度'; @override - String get mapNavQpesums => '預報'; + String typhoonValueMs(String n) { + return '每秒 $n 公尺'; + } @override - String get mapNavSatellite => '衛星'; + String homeForecastHumidity(String value) { + return '濕度 $value%'; + } @override - String get mapNavLightning => '閃電'; + String get meshtasticBusyBody => + '請先在另一個 Meshtastic App 中斷線。兩個 App 同時連同一台裝置會互相搶走訊息,導致部分訊息遺失。'; @override - String get mapNavTyphoon => '颱風'; + String get meshtasticChannelNoSlot => '沒有可用的頻道空位 — 請先在裝置上空出一個'; @override - String get mapNavEarthquake => '地震'; + String get restroomCategoryTransport => '交通'; @override - String get mapNavTemperature => '溫度'; + String get reportFilterLocationHint => '例如:花蓮、東部海域'; @override - String get mapNavHumidity => '濕度'; + String get moonSubtitle => '月相與亮度 — 完全本地計算'; @override - String get mapNavPressure => '氣壓'; + String get meshtasticBattery => '電量'; @override - String get mapNavWind => '風向'; + String get meshtasticDistance => '距離'; @override - String get mapNavRain => '雨量'; + String get meshtasticSnrTrend => '訊號趨勢 (SNR)'; @override - String get mapNavDisaster => '防災'; + String get meshtasticBatteryTrend => '電量趨勢'; @override - String get displayTheme => '主題'; + String get typhoonOverlayMenuTooltip => '颱風圖層選項'; @override - String get themeSystem => '跟隨系統'; + String get mapLayerSatelliteBtdOzone => 'ひまわり 對流層頂'; @override - String get themeLight => '淺色'; + String meshtasticRegionMismatch(String region) { + return '裝置地區為 $region — DPIP 需要 TW'; + } @override - String get themeDark => '深色'; + String get notifySectionEarthquake => '地震'; @override - String get moreSectionAbout => '關於'; + String get mapLayerDisasterMap => '防災地圖'; @override - String get termsOfService => '服務條款'; + String get weatherModeFog => '大霧'; @override - String get faq => '常見問題'; + String typhoonPickerNamed(String no, String name) { + return '$name TY $no'; + } @override - String get openSourceLicenses => '引用套件'; + String get mapLayerStyleGrayTooltip => '氣象廳灰階慣例:溫度越低越白'; @override - String get sponsorTitle => '支持 DPIP'; + String get moreAnnouncements => '公告'; @override - String get sponsorIntro => - 'DPIP 致力於提供即時防災資訊,沒有廣告或其他營利模式。您的支持能幫助我們維持伺服器運作並持續開發。'; + String get mapLayerSatelliteTransparentNoData => '無資料(陸地) = 透明'; @override - String get sponsorSubscriptions => '訂閱制'; + String get restroomCategoryGovernment => '民眾洽公場所'; @override - String get sponsorRecommended => '推薦'; + String get typhoonLegendCurrent => '目前中心'; @override - String get sponsorOneTime => '單次支援'; + String get aedAddress => '地址'; @override - String sponsorPerMonth(String price) { - return '$price / 月'; - } + String get mapLayerAed => 'AED'; @override - String get sponsorRestore => '恢復購買'; + String get changelogTypePrerelease => '公測'; @override - String get sponsorTerms => '使用條款'; + String get reportFilterIntensityInfoModernBody => + '震度為 0–4、5弱、5強、6弱、6強、7。篩選滑桿依新制;列表中較早的地震會以舊制標示顯示。'; @override - String get sponsorPrivacy => '隱私權政策'; + String get typhoonOverlayWeatherNone => '無'; @override - String get sponsorRestoring => '正在恢復購買…'; + String get mapLayerStyleGray => '灰階(JMA)'; @override - String get sponsorRestoreUnavailable => '無法連線至商店,請稍後再試'; + String get weatherModeAuto => '自動'; @override - String get commonClose => '關閉'; + String get typhoonLabelProbCircle => '70%機率圓'; @override - String get mapLayerTemperature => '溫度'; + String get notifyOptAll => '接收全部'; @override - String get trendRange24h => '24 小時'; + String get displayTheme => '主題'; @override - String get trendRange7d => '7 天'; + String get mapLayerSatelliteB07 => 'ひまわり 短波紅外(B07)'; @override - String get trendNoData => '沒有趨勢資料'; + String get typhoonLabelDirection => '過去移動方向'; @override - String trendCumulativeTotal(String total) { - return '累計 $total mm'; - } + String get regionManageTitle => '常用地區'; @override - String chartHourLabel(int hour) { - return '$hour時'; - } + String get typhoonLegendCone => '預測圓錐'; @override - String get mapLayerHumidity => '濕度'; + String get moreCwaEew => '中央氣象署強震即時警報'; @override - String get mapLayerPressure => '氣壓'; + String get onboardingPermsTitle => '權限授權'; @override - String get mapLayerWind => '風向'; + String get mapLayerStyleJma => '雲頂強調(JMA)'; @override - String get mapLayerRain => '雨量'; + String get rainInterval10m => '10 分'; @override - String get rainIntervalMenu => '累積時段'; + String weatherRankingAnalysisLow(String value) { + return '最低 $value'; + } @override - String get rainIntervalNow => '今日'; + String get meshtasticConnectAnyway => '仍要連線'; @override - String get rainInterval10m => '10 分'; + String reportListDayCount(int count) { + return '$count'; + } @override - String get rainInterval1h => '1 時'; + String get mapLayerSatelliteB06 => 'ひまわり 近紅外(B06)'; @override - String get rainInterval3h => '3 時'; + String get mapLayerSatelliteTransparentReflectance => '低反射率/夜間 = 透明,顯示底圖'; @override - String get rainInterval6h => '6 時'; + String chartHourLabel(int hour) { + return '$hour時'; + } @override - String get rainInterval12h => '12 時'; + String get mapLayerShelter => '避難收容場所'; @override - String get rainInterval24h => '24 時'; + String get typhoonOverlayProbabilityTooltip => '顯示侵襲機率(會隱藏預測圓錐)'; @override - String get rainInterval2d => '2 日'; + String get mapLayerSatelliteNdwi => 'ひまわり 水體指數'; @override - String get rainInterval3d => '3 日'; + String get disasterMapOverlayShelterTooltip => '顯示避難收容場所'; @override - String get mapLayerTyphoon => '颱風'; + String get mapNavHumidity => '濕度'; @override - String get typhoonNoActive => '目前無颱風'; + String get reportDetailSortByIntensity => '依震度排序'; @override - String get typhoonWind => '風速'; + String get homeRainTrendNoData => '無資料'; @override - String get typhoonGust => '陣風'; + String get mapLayerCategoryRadar => '雷達'; @override - String get typhoonPressure => '氣壓'; + String get meshtasticShortName => '簡稱'; @override - String get typhoonMotion => '移動'; + String get mapLayerSatelliteAirmass => 'ひまわり 氣團'; @override - String get typhoonLabelPosition => '中心位置'; + String get typhoonTrackDetail => '路徑詳情'; @override - String get typhoonLabelDirection => '過去移動方向'; + String get dataSectionWeather => '氣象'; @override - String get typhoonLabelSpeed => '過去移動時速'; + String get aedHoursWeekday => '平日開放時間'; @override - String get typhoonLabelPressure => '中心氣壓'; + String get homeActiveEventsTitle => '生效中事件'; @override - String get typhoonLabelWind => '近中心最大風速'; + String weatherRankingAnalysisHigh(String value) { + return '最高 $value'; + } @override - String get typhoonLabelGust => '瞬間最大陣風'; + String get faq => '常見問題'; @override - String get typhoonLabelGaleAvg => '七級風平均暴風半徑'; + String get typhoonHistoryLive => '即時'; @override - String get typhoonLabelStormAvg => '十級風平均暴風半徑'; + String eewSerial(int serial) { + return '第 $serial 報'; + } @override - String get typhoonLabelProbCircle => '70%機率圓'; + String get reportFilterSort => '排序方式'; @override - String typhoonForecastLead(String hours) { - return '預測 +$hours 小時'; - } + String get meshtasticRegionConfirm => + '要將這台裝置切換為 TW 地區嗎?裝置會重新啟動並短暫斷線,上面的其他頻道也會一起改變。'; @override - String get typhoonLabelNw => '西北側'; + String get dataEarthquakeSubtitle => '地震報告'; @override - String get typhoonLabelNe => '東北側'; + String get typhoonNoActive => '目前無颱風'; @override - String get typhoonLabelSw => '西南側'; + String get mapLayerSatelliteB11 => 'ひまわり 二氧化硫/雲相(B11)'; @override - String get typhoonLabelSe => '東南側'; + String get navEvents => '事件'; @override - String typhoonValueLat(String lat) { - return '北緯 $lat 度'; - } + String get onboardingTermsTitle => '服務條款'; @override - String typhoonValueLon(String lon) { - return '東經 $lon 度'; - } + String get mapTownLabels => '鄉鎮名稱'; @override - String typhoonValueKm(String n) { - return '$n 公里'; - } + String get notifySetFailed => '設定失敗,請稍後再試。'; @override - String typhoonValueHpa(String n) { - return '$n 百帕'; - } + String get meshtasticDisconnect => '斷線'; @override - String typhoonValueMs(String n) { - return '每秒 $n 公尺'; - } + String get meshtasticUndecoded => '無法解密'; @override - String typhoonDataTime(String time) { - return '資料時間\n$time'; - } + String get notifyAnnouncement => '公告'; @override - String get mapLayerWindForecastEcmwf => 'ECMWF'; + String get onboardingIntroTitle => '歡迎使用 DPIP'; @override - String get mapLayerWindForecastGfs => 'GFS'; + String get regionCurrentUnavailable => '無法取得所在地位置資訊'; @override - String get mapLayerMonitor => '強震監視器'; + String get languageSystem => '系統預設'; @override - String get mapLayerDisasterMap => '防災地圖'; + String get skyTimeSunset => '日落'; @override - String get mapLayerAed => 'AED'; + String get mapLayerSatelliteDust => 'ひまわり 沙塵'; @override - String get disasterMapOverlayMenuTooltip => '防災地圖圖層'; + String get mapAppAppleMaps => 'Apple Maps'; @override - String get disasterMapOverlaySectionLayers => '圖層'; + String get regionEdit => '修改'; @override - String get disasterMapOverlayAedTooltip => '顯示 AED 位置'; + String get weatherDynamicState => '天氣動態狀態'; @override - String get aedAddress => '地址'; + String get mapPlaceholderDisabled => '地圖(暫時停用)'; @override - String get aedRegion => '縣市區域'; + String get moonNow => '現在'; @override - String get aedCategory => '場所分類'; + String get moonSectionAppearance => '外觀'; @override - String get aedType => '場所類型'; + String get moonSectionRiseSet => '月出月沒'; @override - String get aedPlaceDesc => '放置位置說明'; + String get moonSectionUpcoming => '接下來'; @override - String get aedDescription => '備註'; + String get moonSectionCalendar => '月曆'; @override - String get aedHoursWeekday => '平日開放時間'; + String get moonDistance => '距離'; @override - String get aedHoursSaturday => '週六開放時間'; + String get moonKilometres => '公里'; @override - String get aedHoursSunday => '週日開放時間'; + String get moonApparentSize => '視直徑'; @override - String get aedOpenRemark => '開放時間備註'; + String get moonRise => '月出'; @override - String get aedEmergencyPhone => '緊急聯絡電話'; + String get moonSet => '月沒'; @override - String get mapLayerRestroom => '公廁'; + String get moonNextNewMoon => '下次新月'; @override - String get mapLayerShelter => '避難收容場所'; + String get moonAlwaysUp => '整日在地平線上'; @override - String get disasterMapOverlayRestroomTooltip => '顯示公廁'; + String get moonNoEvent => '當日無'; @override - String get disasterMapOverlayShelterTooltip => '顯示避難收容場所'; + String get sunTitle => '太陽'; @override - String get dpmOpenInMaps => '開啟地圖'; + String get sunSubtitle => '日出日沒、曙暮光與節氣'; @override - String get mapAppGoogleMaps => 'Google Maps'; + String get sunSectionDaylight => '日照'; @override - String get mapAppAppleMaps => 'Apple Maps'; + String get sunSectionTwilight => '曙暮光'; @override - String mapAppDefault(String app) { - return '$app(預設)'; - } + String get sunSectionLight => '光線'; @override - String get mapAppCopyCoordinates => '複製座標'; + String get sunSectionSundial => '日晷'; @override - String get mapAppCoordinatesCopied => '已複製座標'; + String get sunSectionTerms => '節氣'; @override - String mapAppOpenFailed(String app) { - return '無法開啟 $app'; - } + String get sunRise => '日出'; @override - String get mapAppCallFailed => '此裝置無法撥打電話'; + String get sunSet => '日沒'; @override - String get mapOverlaySectionReference => '參考圖層'; + String get sunNoon => '正午'; @override - String get mapLayerCategoryEarthquake => '地震'; + String get sunDayLength => '白晝長度'; @override - String get mapLayerCategoryTyphoon => '颱風'; + String get sunTwilightCivil => '民用'; @override - String get mapLayerCategoryWeather => '氣象觀測'; + String get sunTwilightNautical => '航海'; @override - String get mapLayerCategorySatellite => '衛星'; + String get sunTwilightAstronomical => '天文'; @override - String get mapLayerCategoryRadar => '雷達'; + String get sunGoldenHourMorning => '晨間黃金時刻'; @override - String get mapLayerCategoryLife => '生活'; + String get sunGoldenHourEvening => '昏間黃金時刻'; @override - String get mapLayerCategoryForecast => '數值預報'; + String get sunBlueHour => '藍調時刻'; @override - String get mapOverlaySectionMap => '地圖'; + String get sunEquationOfTime => '均時差'; @override - String get rainIntervalSection => '統計時間'; + String get sunMinutes => '分'; @override - String get mapTownLabels => '鄉鎮名稱'; + String get solarTermNext => '下一個節氣'; @override - String get mapTownLabelsHint => '放大時顯示鄉鎮名稱'; + String get planetsTitle => '行星'; @override - String get mapTerrainRelief => '地形立體感'; + String get planetsSubtitle => '今晚在哪、有多亮'; @override - String get mapTerrainReliefHint => '在底圖上顯示立體地形陰影'; + String get planetsSectionTonight => '此刻'; @override - String get dpmSheetEmpty => '點選地圖上的標記查看詳情'; + String get planetUp => '地平線上'; @override - String get dpmAddress => '地址'; + String get planetDown => '地平線下'; @override - String get restroomTypeLabel => '廁所類型'; + String get planetInGlare => '太近太陽'; @override - String get restroomCategoryLabel => '類別'; + String get planetMagnitude => '亮度'; @override - String get restroomGradeLabel => '等級'; + String get planetElongation => '距日距角'; @override - String get restroomTypeFemale => '女廁所'; + String get planetSky => '時段'; @override - String get restroomTypeMale => '男廁所'; + String get planetEvening => '昏星'; @override - String get restroomTypeMixed => '混合廁所'; + String get planetMorning => '晨星'; @override - String get restroomTypeAccessible => '無障礙廁所'; + String get planetDistance => '距離'; @override - String get restroomTypeGenderNeutral => '性別友善廁所'; + String get planetAu => '天文單位'; @override - String get restroomTypeFamily => '親子廁所'; + String get planetAltitude => '仰角'; @override - String get restroomTypeUnspecified => '未設定'; + String get planetMercury => '水星'; @override - String get restroomCategoryTransport => '交通'; + String get planetVenus => '金星'; @override - String get restroomCategoryPark => '公園'; + String get planetMars => '火星'; @override - String get restroomCategoryCommercial => '商業營業場所'; + String get planetJupiter => '木星'; @override - String get restroomCategoryReligious => '宗教禮儀場所'; + String get planetSaturn => '土星'; @override - String get restroomCategoryCultural => '文化育樂活動場所'; + String get planetUranus => '天王星'; @override - String get restroomCategoryGovernment => '民眾洽公場所'; + String get planetNeptune => '海王星'; @override - String get restroomCategoryWelfare => '社福機構、集會場所'; + String get solarTermVernalEquinox => '春分'; @override - String get restroomCategoryTourist => '觀光地區及風景區'; + String get solarTermPureBrightness => '清明'; @override - String get restroomCategoryLeisure => '休閒娛樂場所'; + String get solarTermGrainRain => '穀雨'; @override - String get restroomCategoryOther => '其他'; + String get solarTermStartOfSummer => '立夏'; @override - String get restroomGradeExcellent => '特優級'; + String get solarTermGrainFull => '小滿'; @override - String get restroomGradeGood => '優等級'; + String get solarTermGrainInEar => '芒種'; @override - String get restroomGradeAverage => '普通級'; + String get solarTermSummerSolstice => '夏至'; @override - String get restroomGradePoor => '不合格'; + String get solarTermMinorHeat => '小暑'; @override - String get shelterAddressLabel => '地址'; + String get solarTermMajorHeat => '大暑'; @override - String get shelterCapacityLabel => '收容人數'; + String get solarTermStartOfAutumn => '立秋'; @override - String shelterCapacityValue(int n) { - return '$n 人'; - } + String get solarTermEndOfHeat => '處暑'; @override - String get shelterCategoryLabel => '適用災害'; + String get solarTermWhiteDew => '白露'; @override - String get shelterIndoorLabel => '室內收容'; + String get solarTermAutumnalEquinox => '秋分'; @override - String get shelterOutdoorLabel => '室外收容'; + String get solarTermColdDew => '寒露'; @override - String get shelterVulnerableOkLabel => '適合避難弱者安置'; + String get solarTermFrostDescent => '霜降'; @override - String get dpmYes => '是'; + String get solarTermStartOfWinter => '立冬'; @override - String get dpmNo => '否'; + String get solarTermMinorSnow => '小雪'; @override - String get stationSheetEmpty => '點選任一測站查看觀測值'; + String get solarTermMajorSnow => '大雪'; @override - String monitorDelay(String value) { - return '延遲 $value s'; - } + String get solarTermWinterSolstice => '冬至'; @override - String get monitorWaiting => '等待資料…'; + String get solarTermMinorCold => '小寒'; @override - String mapLegendUnit(String unit) { - return '單位:$unit'; - } + String get solarTermMajorCold => '大寒'; @override - String get typhoonLegendPast => '實際路徑'; + String get solarTermStartOfSpring => '立春'; @override - String get typhoonIntensityTd => '熱帶性低氣壓'; + String get solarTermRainWater => '雨水'; @override - String typhoonPickerNamed(String no, String name) { - return '$name TY $no'; - } + String get solarTermAwakeningOfInsects => '驚蟄'; @override - String typhoonPickerTd(String no) { - return '熱帶性低氣壓 TD $no'; - } + String get tonightTitle => '今夜'; @override - String typhoonTyNo(String no) { - return 'TY $no'; - } + String get tonightSubtitle => '現在看得到什麼、什麼時候'; @override - String typhoonTdNo(String no) { - return 'TD $no'; - } + String get tonightSectionDark => '觀測窗口'; @override - String get typhoonIntensityMild => '輕度颱風'; + String get tonightAstronomicalNight => '天文夜'; @override - String get typhoonIntensityModerate => '中度颱風'; + String get tonightNeverDark => '整夜不全暗'; @override - String get typhoonIntensityIntense => '強烈颱風'; + String get tonightDarkWindow => '暗窗'; @override - String get typhoonLegendForecast => '預測路徑'; + String get tonightMoonAllNight => '月亮整夜在天上'; @override - String get typhoonLegendForecastPoint => '預測點'; + String get tonightDarkTotal => '總暗時'; @override - String get typhoonLegendCurrent => '目前中心'; + String get tonightMoonlight => '月光'; @override - String get typhoonLegendCone => '預測圓錐'; + String get tonightSectionShowers => '流星雨'; @override - String get mapLegendExpand => '圖例'; + String get tonightRadiantDown => '輻射點不升起'; @override - String get mapLegendCollapse => '收合圖例'; + String get tonightPerHour => '顆/時'; @override - String get mapMyLocation => '我的位置'; + String get tonightSectionSatellites => '衛星過境'; @override - String get mapResetNorth => '回到北方'; + String get tonightSectionTargets => '此刻可觀測目標'; @override - String get typhoonLegendCircle15 => '七級風暴風圈'; + String get showerQuadrantids => '象限儀座'; @override - String get typhoonLegendCircleAvg => '平均圓'; + String get showerLyrids => '天琴座'; @override - String get typhoonLegendCircle25 => '十級風暴風圈'; + String get showerEtaAquariids => '寶瓶座η'; @override - String typhoonStormRadii(String ne, String se, String sw, String nw) { - return '東北 $ne · 東南 $se · 西南 $sw · 西北 $nw km'; - } + String get showerDeltaAquariids => '寶瓶座δ'; @override - String typhoonTimeChip(String day, String hour) { - return '$day日$hour時'; - } + String get showerPerseids => '英仙座'; @override - String get typhoonLegendProbability => '侵襲機率'; + String get showerOrionids => '獵戶座'; @override - String get typhoonLegendWarningAreas => '警報區域'; + String get showerSouthernTaurids => '金牛座南'; @override - String get typhoonOverlayMenuTooltip => '颱風圖層選項'; + String get showerLeonids => '獅子座'; @override - String get typhoonOverlaySectionStorm => '暴風圈'; + String get showerGeminids => '雙子座'; @override - String get typhoonOverlaySectionExtra => '覆蓋層'; + String get showerUrsids => '小熊座'; @override - String get typhoonOverlayStormBandSubtitle => '含平均圓'; + String get deepSkyOpenCluster => '疏散星團'; @override - String get typhoonOverlayProbabilityHint => '會隱藏預測圓錐'; + String get deepSkyGlobularCluster => '球狀星團'; @override - String get typhoonOverlayProbabilityTooltip => '顯示侵襲機率(會隱藏預測圓錐)'; + String get deepSkySpiralGalaxy => '螺旋星系'; @override - String get typhoonOverlayWarningTooltip => '標示警報區域縣市'; + String get deepSkyEllipticalGalaxy => '橢圓星系'; @override - String get typhoonOverlayStormL7Tooltip => '七級暴風圈+平均圓(紫色)'; + String get deepSkyIrregularGalaxy => '不規則星系'; @override - String get typhoonOverlayStormL10Tooltip => '十級暴風圈+平均圓(黃色)'; + String get deepSkyPlanetaryNebula => '行星狀星雲'; @override - String get typhoonOverlaySectionWeather => '天氣底圖'; + String get deepSkySupernovaRemnant => '超新星遺跡'; @override - String get typhoonOverlayWeatherNone => '無'; + String get deepSkyEmissionNebula => '發射星雲'; @override - String get typhoonOverlayWeatherHint => '對齊報文時間'; + String get deepSkyReflectionNebula => '反射星雲'; @override - String get typhoonOverlayWeatherNoneTooltip => '不疊雷達或紅外線'; + String get deepSkyAsterism => '星群'; @override - String get typhoonOverlayWeatherRadarTooltip => '雷達回波(對齊颱風報文時間)'; + String get almanacTitle => '曆法'; @override - String get typhoonOverlayWeatherSatelliteTooltip => '紅外線(對齊颱風報文時間)'; + String get almanacSubtitle => '農曆日期與未來的日月食'; @override - String get typhoonWarningTitle => '颱風警報'; + String get almanacSectionToday => '今日'; @override - String typhoonWarningAreas(String areas) { - return '警戒區域:$areas'; - } + String get almanacGregorian => '西曆'; @override - String get typhoonTrackDetail => '路徑詳情'; + String get almanacLunar => '農曆'; @override - String get typhoonHistoryTitle => '資料時間'; + String get almanacYear => '歲次'; @override - String get typhoonHistoryLive => '即時'; + String get almanacMonthLength => '月大小'; @override - String get typhoonSatelliteTitle => '衛星雲圖'; + String get almanacLongMonth => '三十日'; @override - String get typhoonOverlayForecastCallouts => '預測點資訊'; + String get almanacShortMonth => '二十九日'; @override - String get typhoonOverlayForecastCalloutsTooltip => '放大時顯示預測點詳細卡片'; + String get almanacLeapPrefix => '閏'; @override - String get dpmFilterSectionRestroom => '場所類型'; + String get almanacSectionLunarEclipses => '月食'; @override - String get dpmFilterSectionRestroomType => '廁所類型'; + String get almanacSectionSolarEclipses => '日食'; @override - String get dpmFilterSectionShelter => '避難所災害類型'; + String get almanacNoSolarEclipse => '範圍內無'; @override - String get dpmDisasterFlood => '水災'; + String get eclipseTotal => '全食'; @override - String get dpmDisasterEarthquake => '震災'; + String get eclipsePartial => '偏食'; @override - String get dpmDisasterLandslide => '土石流'; + String get eclipseAnnular => '環食'; @override - String get dpmDisasterTsunami => '海嘯'; + String get eclipsePenumbral => '半影食'; @override - String get dpmDisasterSlope => '坡地災害'; + String get zodiacRat => '鼠'; @override - String get dpmDisasterNuclear => '核子事故'; + String get zodiacOx => '牛'; @override - String get skyTime => '天空時間'; + String get zodiacTiger => '虎'; @override - String get skyTimeAuto => '自動'; + String get zodiacRabbit => '兔'; @override - String get skyTimeDawn => '黎明'; + String get zodiacDragon => '龍'; @override - String get skyTimeSunrise => '日出'; + String get zodiacSnake => '蛇'; @override - String get skyTimeMorning => '上午'; + String get zodiacHorse => '馬'; @override - String get skyTimeNoon => '正午'; + String get zodiacGoat => '羊'; @override - String get skyTimeAfternoon => '下午'; + String get zodiacMonkey => '猴'; @override - String get skyTimeGolden => '黃金時刻'; + String get zodiacRooster => '雞'; @override - String get skyTimeSunset => '日落'; + String get zodiacDog => '狗'; @override - String get skyTimeDusk => '暮色'; + String get zodiacPig => '豬'; @override - String get skyTimeNight => '夜晚'; + String get tideTitle => '潮汐'; @override - String get weatherModeCloudy => '多雲'; + String get tideSubtitle => '大潮、小潮與月球引力'; @override - String get weatherModeOvercast => '陰天'; + String get tideDisclaimer => '僅為天文引潮力,非港口潮汐表。水位請參考氣象署公布之潮汐預報。'; @override - String get weatherModeSnow => '下雪'; + String get tideSectionNow => '此刻'; @override - String get weatherModeSand => '沙塵'; + String get tidePhase => '週期'; @override - String get radarScanRange => '顯示掃描範圍'; + String get tideSpring => '大潮'; @override - String get radarScanRangeSubtitle => '標示四座雷達實際觀測到的範圍。'; + String get tideNeap => '小潮'; @override - String get radarScanRangeHint => '框外空白代表未觀測'; + String get tideMiddling => '中潮'; @override - String get radarOverlayMenuTooltip => '雷達圖層選項'; + String get tideLunarDistanceFactor => '月球引力'; @override - String get radarCountyOutline => '縣市界線'; + String get tideEquilibrium => '平衡潮高'; @override - String get radarGlobalOutline => '國界'; + String get tideMetres => '公尺'; @override - String get radarGlobalOutlineHint => '各國國界外框'; + String get tidePerigeanSpring => '下次近地點大潮'; @override - String get radarCountyOutlineHint => '畫在回波之上'; + String get tideSectionTurningPoints => '轉折點'; @override - String get radarCountyOutlineSubtitle => '讓縣市界線在雷達回波下仍然清楚。'; + String get tideHigh => '高'; @override - String get radarTownOutline => '鄉鎮界線'; + String get tideLow => '低'; @override - String get radarTownOutlineHint => '較細的分區'; + String get skyChartTitle => '星圖'; @override - String get radarTownOutlineSubtitle => '讓鄉鎮界線在雷達回波下仍然清楚。'; + String get skyChartSubtitle => '頭頂上肉眼可見的天空'; @override - String get qpesumsOverlayMenuTooltip => '定量降水預報圖層選項'; + String get skyChartNorth => '北'; @override - String get windForecastOverlayMenuTooltip => '風場預報圖層選項'; + String get skyChartEast => '東'; @override - String get windForecastCountyOutlineHint => '繪製於風場之上'; + String get skyChartSouth => '南'; @override - String get windForecastGlobalOutlineHint => '各國國界外框'; + String get skyChartWest => '西'; @override - String get windForecastTownOutlineHint => '更細的網格'; + String tonightElementAge(int days) { + return '軌道資料 $days 天前'; + } @override - String eewSerial(int serial) { - return '第 $serial 報'; + String almanacLunarDate(String leap, int month, int day) { + return '$leap$month 月 $day 日'; } @override - String get eewMaxIntensity => '最大震度'; + String get tonightNoShowers => '目前無流星雨'; @override - String get eewLocalIntensity => '所在地預估'; + String get tonightNoPasses => '48 小時內無可見過境'; @override - String get eewSWave => '震波'; + String get tonightSatellitesUnavailable => '無法讀取軌道資料'; @override - String get eewArrived => '已抵達'; + String get tonightNoTargets => '無足夠高度的目標'; @override - String eewCountdown(int seconds) { - return '$seconds 秒'; - } + String get skyChartUnavailable => '無法讀取星表'; } diff --git a/lib/shared/map/base_map.dart b/lib/shared/map/base_map.dart index 033b5453b..b9d99cfa7 100644 --- a/lib/shared/map/base_map.dart +++ b/lib/shared/map/base_map.dart @@ -5,6 +5,7 @@ import 'package:dpip/app/theme/app_spacing.dart'; import 'package:dpip/core/geo/town_directory.dart'; import 'package:dpip/core/logging/log.dart'; import 'package:dpip/shared/map/map_style.dart'; +import 'package:dpip/shared/navigation/refresh_on_appear.dart'; import 'package:flutter/material.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:provider/provider.dart'; @@ -48,6 +49,7 @@ class BaseMap extends StatefulWidget { this.showUserLocation = true, this.minZoomPreference = defaultMinZoom, this.maxZoomPreference = maxZoom, + this.tabIndex, }); /// Bounding box for the nationwide (全國) framing — the Taiwan main island @@ -137,6 +139,14 @@ class BaseMap extends StatefulWidget { /// Per-surface zoom ceiling (DPM AED may go to 16). final double maxZoomPreference; + /// Shell tab that owns this surface, if any — the map pauses its native + /// render loop while that tab is hidden and resumes when it comes back. + /// A hidden tab keeps its platform view alive (see `StatefulShellRoute. + /// indexedStack`), so without this the map would keep producing frames — + /// and burning GPU — offstage. `null` (full-screen routes, previews outside + /// the shell) never pauses. + final int? tabIndex; + @override State createState() => _BaseMapState(); } @@ -145,6 +155,13 @@ class _BaseMapState extends State { /// Set once the platform view reports in — the readiness gate for the retry. MapLibreMapController? _controller; + /// The shell's visible-tab notifier ([VisibleTabScope.of] may be null when + /// this surface lives outside the shell — then it never pauses). + VisibleTab? _visibleTab; + + /// Last-applied pause state, so [setRenderPaused] fires only on transitions. + bool _renderPaused = false; + /// Bumped to remount the map's platform view after a failed first attempt /// (see [_scheduleReadinessRetry]). int _mountAttempt = 0; @@ -157,12 +174,40 @@ class _BaseMapState extends State { _scheduleReadinessRetry(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final visibleTab = VisibleTabScope.of(context); + if (identical(visibleTab, _visibleTab)) return; + _visibleTab?.removeListener(_onTabChanged); + _visibleTab = visibleTab; + visibleTab?.addListener(_onTabChanged); + _syncRender(); + } + @override void dispose() { + _visibleTab?.removeListener(_onTabChanged); _readinessTimer?.cancel(); super.dispose(); } + /// A surface can live in only one tab; anything else (or no scope at all) + /// counts as visible. The controller may not exist yet — the state is kept + /// and applied when the platform view reports in ([_onMapCreated]), so + /// `_renderPaused` only records states that actually reached the platform. + void _syncRender() { + final visible = + _visibleTab == null || _visibleTab!.value == widget.tabIndex; + final pause = !visible && widget.tabIndex != null; + final controller = _controller; + if (controller == null || pause == _renderPaused) return; + _renderPaused = pause; + controller.setRenderPaused(pause); + } + + void _onTabChanged() => _syncRender(); + /// Forwards map readiness to the caller and stops the retry timer. void _onMapCreated(MapLibreMapController controller) { _controller = controller; @@ -170,6 +215,7 @@ class _BaseMapState extends State { // A remount (retry) may have superseded this element — never hand a stale // controller, whose native view is being torn down, to the caller. if (!mounted) return; + _syncRender(); widget.onMapCreated?.call(controller); } @@ -194,6 +240,24 @@ class _BaseMapState extends State { }); } + /// Style string memoised per palette — all other inputs are const, and + /// Every [build] used to re-interpolate the full style JSON — the string + /// only varies by palette and by the town-label directory, so it is memoised + /// on that pair. + static final Map<(MapPalette, String), String> _styleCache = {}; + + static String _styleString(MapPalette palette, String townLabelData) => + _styleCache.putIfAbsent( + (palette, townLabelData), + () => exptechVectorStyle( + palette, + basemapTileUrl: basemapOriginTileUrl, + glyphsUrl: glyphsOriginUrl, + terrainTileUrl: terrainOriginTileUrl, + townLabelData: townLabelData, + ), + ); + @override Widget build(BuildContext context) { final palette = MapColors.of(Theme.of(context).brightness); @@ -216,15 +280,14 @@ class _BaseMapState extends State { ), // Brightness flip / AED overlay changes this string → MapLibre reloads // style; layers re-attach via [onStyleLoaded] (see [MapScaffold]). - // Township-name labels come from the app's own town directory (a unique - // GeoJSON point per township), never the tile polygons — see - // [townLabelGeoJson]. - styleString: exptechVectorStyle( + // The interpolated string only varies by palette and the town directory, + // so it is memoised — every rebuild used to re-run the ~2 KB + // interpolation and the per-town GeoJSON stringification. Township-name + // labels come from the app's own directory (a unique GeoJSON point per + // township), never the tile polygons — see [townLabelGeoJson]. + styleString: _styleString( palette, - basemapTileUrl: basemapOriginTileUrl, - glyphsUrl: glyphsOriginUrl, - terrainTileUrl: terrainOriginTileUrl, - townLabelData: townLabelGeoJson(context.read()), + townLabelGeoJson(context.read()), ), // A remount gets a fresh id, so a collided first attempt recovers (see // [_scheduleReadinessRetry]). diff --git a/lib/shared/map/geo_circle.dart b/lib/shared/map/geo_circle.dart index 96563d123..1aca2f2e0 100644 --- a/lib/shared/map/geo_circle.dart +++ b/lib/shared/map/geo_circle.dart @@ -3,8 +3,15 @@ /// closed [LatLng.destinationPoint] polygon, drawn as an outline /// ([circleFeature], a `LineLayerProperties` line layer) and/or a translucent /// disc ([circleFillFeature], a `FillLayerProperties` fill layer). +/// +/// The ring math is hoisted so a repeated ring is cheap: the bearing table is +/// cached per step count, and each ring computes its centre/delta constants +/// once instead of re-deriving them per vertex (~7 trig per point → 2). library; +import 'dart:math' as math; + +import 'package:dpip/core/geo/geo_math.dart'; import 'package:dpip/core/models/lat_lng.dart'; /// A GeoJSON `Feature` (closed `LineString` geometry) tracing a [steps]-sided @@ -43,17 +50,51 @@ Map circleFillFeature( 'properties': properties, }; -List> _ring(LatLng center, double radiusMetres, int steps) => [ - for (var i = 0; i <= steps; i++) - _pointOnCircle(center, radiusMetres, i, steps), -]; +/// (sin, cos) of each bearing — the same table for every ring of that step +/// count, so per-vertex trig is just the asin/atan2 of the forward geodesic. +final Map> _bearingCache = {}; -List _pointOnCircle( - LatLng center, - double radiusMetres, - int i, - int steps, +List<(double, double)> _bearings(int steps) => _bearingCache.putIfAbsent( + steps, + () => [ + for (var i = 0; i <= steps; i++) + () { + final theta = i * 2 * math.pi / steps; + return (math.sin(theta), math.cos(theta)); + }(), + ], +); + +List> _ring(LatLng center, double radiusMetres, int steps) { + const earthRadius = 6378137.0; + final delta = radiusMetres / earthRadius; + final sinD = math.sin(delta); + final cosD = math.cos(delta); + final lat1 = degToRad(center.latitude); + final lon1 = degToRad(center.longitude); + final sinLat1 = math.sin(lat1); + final cosLat1 = math.cos(lat1); + return [ + for (final (sinTheta, cosTheta) in _bearings(steps)) + _vertex(sinLat1, cosLat1, lat1, lon1, sinD, cosD, sinTheta, cosTheta), + ]; +} + +/// One vertex of the forward geodesic — centre/delta constants are passed in +/// so only the per-vertex asin/atan2 remain. +List _vertex( + double sinLat1, + double cosLat1, + double lat1, + double lon1, + double sinD, + double cosD, + double sinTheta, + double cosTheta, ) { - final point = center.destinationPoint(i * 360.0 / steps, radiusMetres); - return [point.longitude, point.latitude]; + final lat2 = math.asin(sinLat1 * cosD + cosLat1 * sinD * cosTheta); + final lon2 = + lon1 + + math.atan2(sinTheta * sinD * cosLat1, cosD - sinLat1 * math.sin(lat2)); + return [lon2 * 180.0 / math.pi, lat2 * 180.0 / math.pi]; } diff --git a/lib/shared/map/map_layer_category.dart b/lib/shared/map/map_layer_category.dart index 5ec7705d1..6058b6eae 100644 --- a/lib/shared/map/map_layer_category.dart +++ b/lib/shared/map/map_layer_category.dart @@ -62,6 +62,10 @@ MapLayerCategory categoryOf(String layerId) { // purely an instrument. 'qpesums' => MapLayerCategory.forecast, 'dpm' => MapLayerCategory.life, + // Grouped with the everyday-facility layer rather than given a category of + // its own: a mesh node is a thing you go and find, like an AED or a + // shelter, and a one-item group would be a header with one row under it. + 'meshtastic' => MapLayerCategory.life, _ => MapLayerCategory.weather, }; } diff --git a/lib/shared/map/map_layer_switcher.dart b/lib/shared/map/map_layer_switcher.dart index a303abdd2..bc67b61e4 100644 --- a/lib/shared/map/map_layer_switcher.dart +++ b/lib/shared/map/map_layer_switcher.dart @@ -378,9 +378,8 @@ class _LayerOrderSheetState extends State<_LayerOrderSheet> { title: categoryLabel(editing, AppLocalizations.of(context)), left: IconButton( icon: const Icon(Icons.arrow_back), - tooltip: MaterialLocalizations.of( - context, - ).backButtonTooltip, + tooltip: MaterialLocalizations.of(context) + .backButtonTooltip, onPressed: () => setState(() => _editing = null), ), right: closeButton, diff --git a/lib/shared/map/map_scaffold.dart b/lib/shared/map/map_scaffold.dart index 0651b1095..5ae7b69c1 100644 --- a/lib/shared/map/map_scaffold.dart +++ b/lib/shared/map/map_scaffold.dart @@ -4,6 +4,7 @@ import 'package:dpip/app/theme/app_radius.dart'; import 'package:dpip/app/theme/app_spacing.dart'; import 'package:dpip/core/error/failure.dart'; import 'package:dpip/core/logging/log.dart'; +import 'package:dpip/core/realtime/app_time.dart'; import 'package:dpip/l10n/gen/app_localizations.dart'; import 'package:dpip/shared/map/base_map.dart'; import 'package:dpip/shared/map/camera_fit.dart'; @@ -19,6 +20,8 @@ import 'package:dpip/shared/map/map_style.dart'; import 'package:dpip/shared/map/map_timeline.dart'; import 'package:dpip/shared/map/map_town_labels.dart'; import 'package:dpip/shared/map/raster_timeline_layer.dart'; +import 'package:dpip/shared/navigation/refresh_on_appear.dart' + show VisibleTab, VisibleTabScope; import 'package:dpip/shared/widgets/collapsible_map_legend.dart'; import 'package:dpip/shared/widgets/frosted_surface.dart'; import 'package:flutter/material.dart'; @@ -41,8 +44,12 @@ const String _basemapTileUrl = basemapOriginTileUrl; /// calls never overlap, and a generation counter drops results from a superseded /// layer load so a slow fetch can't render onto the wrong layer. class MapScaffold extends StatefulWidget { - const MapScaffold({super.key, required this.layers, this.initialLayerId}) - : assert(layers.length > 0, 'MapScaffold needs at least one layer'); + const MapScaffold({ + super.key, + required this.layers, + this.initialLayerId, + this.tabIndex, + }) : assert(layers.length > 0, 'MapScaffold needs at least one layer'); /// The layers this surface offers; [initialLayerId] (or the first entry) is /// shown initially. @@ -52,14 +59,22 @@ class MapScaffold extends StatefulWidget { /// missing → [layers].first. final String? initialLayerId; + /// Shell tab owning this surface — forwarded to [BaseMap] so the map pauses + /// its native render loop while the tab is hidden. `null` never pauses. + final int? tabIndex; + @override State createState() => _MapScaffoldState(); } -class _MapScaffoldState extends State { +class _MapScaffoldState extends State with WidgetsBindingObserver { MapLibreMapController? _controller; bool _styleLoaded = false; + /// The shell's visible-tab notifier — same contract as [BaseMap]: null + /// (full-screen routes, previews) means always visible. + VisibleTab? _visibleTab; + /// Whether the initial framing has run. Only on first load — a reload (theme /// change) keeps whatever the user has panned/zoomed to. bool _framed = false; @@ -94,7 +109,10 @@ class _MapScaffoldState extends State { Future _mapOps = Future.value(); /// Bumped on camera idle so screen-space [MapLayer.buildMapOverlay] reprojects. - int _cameraEpoch = 0; + /// A [ValueNotifier], not a `setState` bump: only the overlay subtree (which + /// keys off it) rebuilds, instead of the whole scaffold — every pan/zoom + /// settle used to rebuild the platform view, chrome and legend too. + final ValueNotifier _cameraEpoch = ValueNotifier(0); /// Basemap tile warm-up. Its own warmer so a layer's cancel can't abort it. MapTileWarmer? _basemapWarmer; @@ -144,6 +162,12 @@ class _MapScaffoldState extends State { /// must not move the camera at all). bool _reframeOnMeasure = false; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -157,11 +181,24 @@ class _MapScaffoldState extends State { _stationHandoff?.removeListener(_onStationHandoff); _stationHandoff = station..addListener(_onStationHandoff); } + final visibleTab = VisibleTabScope.of(context); + if (identical(visibleTab, _visibleTab)) return; + _visibleTab?.removeListener(_onTabChanged); + _visibleTab = visibleTab; + visibleTab?.addListener(_onTabChanged); } + /// Whether this surface is on screen — null tab (full-screen) counts as + /// visible, matching [BaseMap]'s own gate. + bool get _isVisible => + _visibleTab == null || _visibleTab!.value == widget.tabIndex; + @override void dispose() { + _visibleTab?.removeListener(_onTabChanged); + WidgetsBinding.instance.removeObserver(this); _bearing.dispose(); + _cameraEpoch.dispose(); _showTownLabels.dispose(); _showTerrain.dispose(); _basemapWarmer?.cancel(); @@ -170,6 +207,28 @@ class _MapScaffoldState extends State { super.dispose(); } + /// The timeline's "now" and its frames went stale while this surface was + /// off-screen — the app backgrounded, or the user sat on another tab. + /// + /// Re-fetch and re-centre on the present, but only when this map can be + /// seen: the IndexedStack keeps hidden tabs mounted, and a hidden map has + /// no timeline to update. Non-timeline layers are skipped entirely — their + /// data sources (RTS, the mesh node store) refresh themselves, and a bare + /// re-render would only flash the map. + void _onTabChanged() { + if (!_isVisible) return; + if (_active.usesTimeline) unawaited(_loadActive()); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state != AppLifecycleState.resumed || !_isVisible) return; + // Coming back from the background is the same event as re-entering the + // tab: what is on screen has been sitting there unattended. A radar frame + // every 10 minutes means a half-hour away is three frames missed. + if (_active.usesTimeline) unawaited(_loadActive()); + } + /// A framing request arrived (map re-opened from Home / the nav bar) — apply it /// once the style is up. Leaves it pending if not, for [_onStyleLoaded]. void _onHandoff() { @@ -491,7 +550,12 @@ class _MapScaffoldState extends State { ok: (frames) { setState(() { _frames = frames; - _selectedIndex = nowFrameIndex(frames); + // The calibrated clock, not device time: the frames are server + // timestamps, and a device clock that drifted (or a timezone the + // device changed) would pick the wrong "now" frame. The NTP resync + // on foreground is what makes this actually correct after a + // background stretch. + _selectedIndex = nowFrameIndex(frames, now: AppTime.utc); }); if (frames.isNotEmpty) { // Register the set, then reveal the present (a layer adds tiles @@ -663,16 +727,19 @@ class _MapScaffoldState extends State { final c = _controller; if (c == null || !c.isCameraMoving) { _active.onMapGestureEnd(); - if (mounted) setState(() => _cameraEpoch++); + _cameraEpoch.value++; } }, onPointerCancel: (_) { _active.onMapGestureEnd(); - if (mounted) setState(() => _cameraEpoch++); + _cameraEpoch.value++; }, child: BaseMap( minZoomPreference: _active.mapMinZoom, maxZoomPreference: _active.mapMaxZoom, + // The map tab owns this surface — pause native rendering when the + // user is on another tab (indexedStack keeps it mounted). + tabIndex: widget.tabIndex, onMapCreated: _onMapCreated, onStyleLoaded: _onStyleLoaded, onMapClick: (_, latLng) => _onMapClick(latLng), @@ -684,8 +751,7 @@ class _MapScaffoldState extends State { unawaited(_active.onCameraIdle(controller)); unawaited(_warmBasemap(controller)); } - if (!mounted) return; - setState(() => _cameraEpoch++); + _cameraEpoch.value++; }, // The native compass lives inside the platform view, so any // Flutter overlay paints over it — MapScaffold draws its own @@ -696,11 +762,16 @@ class _MapScaffoldState extends State { ), // Screen-space Flutter overlays (e.g. typhoon forecast tips) — under // chrome/sheet so they don't steal taps; IgnorePointer keeps pan/zoom. + // Only this subtree rebuilds on a camera settle (ValueListenableBuilder + // + keyed reprojection); the map and chrome stay put. Positioned.fill( child: IgnorePointer( - child: KeyedSubtree( - key: ValueKey('${_active.id}-$_cameraEpoch'), - child: _active.buildMapOverlay(context), + child: ValueListenableBuilder( + valueListenable: _cameraEpoch, + builder: (context, epoch, _) => KeyedSubtree( + key: ValueKey('${_active.id}-$epoch'), + child: _active.buildMapOverlay(context), + ), ), ), ), diff --git a/lib/shared/map/map_tile_cache.dart b/lib/shared/map/map_tile_cache.dart index 6215f1c46..e301fad64 100644 --- a/lib/shared/map/map_tile_cache.dart +++ b/lib/shared/map/map_tile_cache.dart @@ -62,22 +62,27 @@ class MapTileCache { /// large and the mirror quietly becomes the real cache, leaving /// [EtagCacheStore] doing nothing but the cold start. /// - /// 24 MB holds a full ±12-frame scrub band of webp tiles *plus* the basemap + /// 48 MB holds a full ±64-frame scrub band of webp tiles *plus* the basemap /// viewport, so a fast timeline drag stays on memory hits even while the map /// itself downloads. [warm]'s fill mode ([warm]) tops it up outward from the /// current frame until it is near this cap, then stops — the mirror trims /// LRU beyond it, dropping the frames a scrub swept past. - static const int defaultMemoryBytes = 24 * 1024 * 1024; + static const int defaultMemoryBytes = 48 * 1024 * 1024; /// Tiles per `injectTiles` message — roughly one frame's viewport. static const int _injectChunk = 24; + /// The cap [install] sized the mirror with — remembered so a fill warm can + /// estimate how much it may inject without overshooting into a trim. + int _memoryLimit = defaultMemoryBytes; + /// Binds this store as the tile authority and sizes the native mirror. /// /// The patterns come from [EtagInterceptor.immutableAssetMarkers] — the same /// list [_isTile] gates on — so native can never end up asking about a URL /// this store would refuse to keep. Future install({int memoryBytes = defaultMemoryBytes}) async { + _memoryLimit = memoryBytes; await bindMapLibreTileCache( cacheablePatterns: EtagInterceptor.immutableAssetMarkers, getBatch: _onGetBatch, @@ -144,9 +149,10 @@ class MapTileCache { /// /// When [fillUntil] is non-zero, injection runs in [fillUntil]'s **fill /// mode**: [urls] must be ordered most-wanted first, and the loop stops once - /// the native mirror is at `fillUntil × limit` — so a timeline can top the - /// mirror up outward from the current frame until it is nearly full and then - /// stop, instead of over-filling and churning LRU (or re-reading on settle). + /// the native mirror is estimated to be at `fillUntil × limit` — so a + /// timeline can top the mirror up outward from the current frame until it is + /// nearly full and then stop, instead of over-filling into a native LRU trim + /// (which would evict the very frames just injected). Future warm(List urls, {double fillUntil = 0}) async { final wanted = urls.where(_isTile).toList(growable: false); if (wanted.isEmpty) return 0; @@ -166,28 +172,69 @@ class MapTileCache { etag: entry.value.etag, ), ]; - // Chunked: warming a wide band of frames is megabytes of image data, and - // one giant message would occupy the platform channel long enough to be - // felt by whatever gesture is in progress. Each inject echoes the - // mirror's post-injection usage, so a fill warm can bail as it nears the - // cap — the remaining (more distant) urls simply stay cold. - for (var i = 0; i < tiles.length; i += _injectChunk) { - final end = math.min(i + _injectChunk, tiles.length); - final usage = await injectMapLibreTiles(tiles.sublist(i, end)); - if (fillUntil > 0 && - usage != null && - usage.limit > 0 && - usage.used >= usage.limit * fillUntil) { - break; - } - } - return hits.length; + if (fillUntil > 0) return await _injectFill(tiles, fillUntil); + return await _injectAll(tiles); } catch (error, stackTrace) { Log.handle(error, stackTrace, 'MapTileCache.warm'); return 0; } } + /// Injects every [tiles] in fixed chunks. + /// + /// Chunked because warming a wide band of frames is megabytes of image data, + /// and one giant message would occupy the platform channel long enough to be + /// felt by whatever gesture is in progress. + Future _injectAll(List tiles) async { + for (var i = 0; i < tiles.length; i += _injectChunk) { + final end = math.min(i + _injectChunk, tiles.length); + await injectMapLibreTiles(tiles.sublist(i, end)); + } + return tiles.length; + } + + /// Injects [tiles] (ordered most-wanted first) until the mirror is estimated + /// to hold `fillUntil × cap` bytes, then stops. + /// + /// The estimate is the last `injectTiles` echo plus the bytes this side is + /// about to send — native trims LRU beyond the cap and *down to 85% of it*, + /// so overshooting once (a chunk's worth of tiles past the goal) would evict + /// exactly the frames that were just injected, which is worse than stopping + /// a little short. When a chunk straddles the goal it is split and only the + /// fitting prefix is sent. + Future _injectFill(List tiles, double fillUntil) async { + final cap = (_memoryLimit * fillUntil).floor(); + if (cap <= 0) return 0; + var used = 0; // No pre-inject usage query — start at the optimistic 0. + var injected = 0; + for (var i = 0; i < tiles.length; i += _injectChunk) { + final end = math.min(i + _injectChunk, tiles.length); + var chunkBytes = 0; + for (var j = i; j < end; j++) { + chunkBytes += tiles[j].data.length; + } + if (used + chunkBytes > cap) { + // Split the chunk at the goal — send only the tiles that fit. + final fits = []; + var size = 0; + for (var j = i; j < end; j++) { + if (used + size + tiles[j].data.length > cap) break; + fits.add(tiles[j]); + size += tiles[j].data.length; + } + if (fits.isEmpty) break; + final usage = await injectMapLibreTiles(fits); + used = usage?.used ?? used + size; + injected += fits.length; + break; + } + final usage = await injectMapLibreTiles(tiles.sublist(i, end)); + used = usage?.used ?? used + chunkBytes; + injected += end - i; + } + return injected; + } + /// Stores [bytes] for [url] directly (a body the app fetched itself). Future put(String url, Uint8List bytes, {String? contentType}) async { final uri = Uri.tryParse(url); diff --git a/lib/shared/map/map_timeline.dart b/lib/shared/map/map_timeline.dart index 4268152f8..39b794e4c 100644 --- a/lib/shared/map/map_timeline.dart +++ b/lib/shared/map/map_timeline.dart @@ -1,5 +1,6 @@ import 'package:dpip/app/theme/app_motion.dart'; import 'package:dpip/app/theme/app_spacing.dart'; +import 'package:dpip/core/realtime/app_time.dart'; import 'package:dpip/l10n/gen/app_localizations.dart'; import 'package:dpip/shared/map/map_layer.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,8 @@ class MapTimeline extends StatefulWidget { this.caption, this.framePeriod, this.dataTime, + this.timeFormat, + this.itemExtent = _defaultSlotWidth, }); /// Frames in chronological order (oldest first); must be non-empty. @@ -46,6 +49,18 @@ class MapTimeline extends StatefulWidget { /// "forecast" for a forecast layer. final String? caption; + /// Tick / big-label time format — defaults to HH:mm. A daily timeline (e.g. + /// lunar phases) passes `M/d` so the ruler reads dates instead of a clock. + final DateFormat? timeFormat; + + /// Slot width per frame — the scroll offset that centres frame `i` is + /// `i * itemExtent` (the leading/trailing pads are symmetric). Radar frames + /// sit 14 px apart; a daily timeline needs a wider slot to stay draggable. + final double itemExtent; + + /// Default frame slot width. + static const double _defaultSlotWidth = 14; + /// How long one frame's data represents, when it is a period rather than a /// point. A next-hour forecast's frame at 21:00 covers 21:00–22:00, so the /// big time label renders that range instead of a bare instant. `null` keeps @@ -68,9 +83,6 @@ class _MapTimelineState extends State { // The data-time line: the model run's issue time, `8/11 14:00`. static final DateFormat _data = DateFormat('M/d HH:mm'); - /// Slot width per frame — the scroll offset that centres frame `i` is - /// `i * _slotWidth` (the leading/trailing pads are symmetric). - static const double _slotWidth = 14; static const double _rulerHeight = 48; /// Formatted labels per frame, built once per frame set. @@ -83,7 +95,8 @@ class _MapTimelineState extends State { List _dates = const []; void _cacheLabels() { - _times = [for (final frame in widget.frames) _time.format(frame.time)]; + final format = widget.timeFormat ?? _time; + _times = [for (final frame in widget.frames) format.format(frame.time)]; _dates = [for (final frame in widget.frames) _date.format(frame.time)]; } @@ -105,9 +118,9 @@ class _MapTimelineState extends State { } /// Seeded so the first paint already sits on the selected frame (no flash), - /// since `i * _slotWidth` centres frame `i`. + /// since `i * itemExtent` centres frame `i`. late final ScrollController _scroll = ScrollController( - initialScrollOffset: widget.selectedIndex * _slotWidth, + initialScrollOffset: widget.selectedIndex * widget.itemExtent, ); /// The frame under the scrubber right now — follows the live scroll so the @@ -143,12 +156,14 @@ class _MapTimelineState extends State { super.dispose(); } - int get _centredIndex => - (_scroll.offset / _slotWidth).round().clamp(0, widget.frames.length - 1); + int get _centredIndex => (_scroll.offset / widget.itemExtent).round().clamp( + 0, + widget.frames.length - 1, + ); void _centreOn(int index, {required bool animate}) { if (!_scroll.hasClients) return; - final target = (index * _slotWidth).clamp( + final target = (index * widget.itemExtent).clamp( 0.0, _scroll.position.maxScrollExtent, ); @@ -196,11 +211,13 @@ class _MapTimelineState extends State { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final colors = theme.colorScheme; - // Asked of the clock rather than of the list, so a forecast's "現在" lands - // on the present rather than on its furthest step. - final nowIndex = nowFrameIndex(widget.frames); + // Asked of the calibrated clock rather than of the list, so a forecast's + // "現在" lands on the present rather than on its furthest step — and the + // clock resyncs on foreground, so returning from the background moves the + // marker to the real now instead of the device clock's guess. + final nowIndex = nowFrameIndex(widget.frames, now: AppTime.utc); final era = _eraOf(_liveIndex, nowIndex); - final labelStep = (48 / _slotWidth).ceil(); + final labelStep = (48 / widget.itemExtent).ceil(); return Column( mainAxisSize: MainAxisSize.min, @@ -278,10 +295,10 @@ class _MapTimelineState extends State { children: [ LayoutBuilder( builder: (context, constraints) { - final pad = (constraints.maxWidth - _slotWidth) / 2; + final pad = (constraints.maxWidth - widget.itemExtent) / 2; // ListView.builder (not a Row) so a week of frames only ever // builds the ~dozens of ticks on screen. itemExtent keeps the - // centring math: offset `i * _slotWidth` centres frame `i`. + // centring math: offset `i * itemExtent` centres frame `i`. return NotificationListener( onNotification: _onScroll, child: ListView.builder( @@ -289,10 +306,10 @@ class _MapTimelineState extends State { scrollDirection: Axis.horizontal, physics: const _ScrubPhysics(), padding: EdgeInsets.symmetric(horizontal: pad), - itemExtent: _slotWidth, + itemExtent: widget.itemExtent, itemCount: widget.frames.length, itemBuilder: (context, i) => _Tick( - width: _slotWidth, + width: widget.itemExtent, label: i % labelStep == 0 ? _times[i] : null, labelColor: _eraColor( _eraOf(i, nowIndex), diff --git a/lib/shared/map/maps_launcher.dart b/lib/shared/map/maps_launcher.dart index 552d71cfe..3bba1777f 100644 --- a/lib/shared/map/maps_launcher.dart +++ b/lib/shared/map/maps_launcher.dart @@ -117,9 +117,8 @@ Future showMapAppPicker(BuildContext context, MapLaunchTarget target) { ), child: Text( l10n.dpmOpenInMaps, - style: Theme.of( - sheetContext, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(sheetContext).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w700), ), ), for (final app in [ diff --git a/lib/shared/map/raster_timeline_layer.dart b/lib/shared/map/raster_timeline_layer.dart index 4d748e9e6..7ddf69b8c 100644 --- a/lib/shared/map/raster_timeline_layer.dart +++ b/lib/shared/map/raster_timeline_layer.dart @@ -73,7 +73,7 @@ abstract class RasterTimelineLayer implements MapLayer { /// This is the **guaranteed** band: [fill] warm always covers it. Beyond it, /// [MapTileCache.warm]'s fill mode keeps topping the mirror up outward from /// the current frame until it is nearly full — the actual reach is set by - /// the memory cap, not by taste, so a 24 MB mirror easily covers far more. + /// the memory cap, not by taste, so a 48 MB mirror easily covers far more. @protected int get warmRadius => 4; @@ -81,10 +81,12 @@ abstract class RasterTimelineLayer implements MapLayer { /// /// A cap on the spread, not a target: a fill warm injects centre → ±1 → ±2 … /// and stops at the native mirror's cap ([MapTileCache.defaultMemoryBytes]), - /// so beyond this only the most distant frames stay cold. Kept finite so a - /// hundreds-of-frames history never unfolds into one giant URL list. + /// so beyond this only the most distant frames stay cold. Sized to what a + /// 48 MB mirror can actually hold — a radar frame's viewport is roughly + /// 100–400 KB of webp, so a ±64 band is a believable full-mirror working set + /// rather than a number that silently under-fills the new budget. @protected - int get maxWarmRadius => 24; + int get maxWarmRadius => 64; /// Mounted-source ceiling. Beyond this the least-recently-shown frame is /// removed outright — `visibility: none` keeps GPU textures for a fast @@ -403,11 +405,13 @@ abstract class RasterTimelineLayer implements MapLayer { await _retireOutside(controller, ring); await _warmBand(controller, index); - for (final id in ring) { - if (id == frameId) continue; - await _mount(controller, id, 0); - _ring.add(id); - } + // The neighbours mount on independent ids — parallelise the platform + // round trips instead of serialising up to four of them. + await Future.wait([ + for (final id in ring) + if (id != frameId) _mount(controller, id, 0), + ]); + _ring.addAll(ring); await _evictOverflow(controller, keep: ring); } @@ -613,9 +617,16 @@ abstract class RasterTimelineLayer implements MapLayer { /// Decodes a frame id into its instant. /// /// Ids are Unix seconds (or milliseconds — both are in use across endpoints); -/// an ISO-8601 string is accepted as a fallback. +/// an ISO-8601 string is accepted as a fallback. Memoised per id: the ids are +/// globally unique (timestamps), so a re-parse — every time a layer reloads +/// its frames — is pure waste. @visibleForTesting -DateTime parseFrameTime(String id) { +DateTime parseFrameTime(String id) => + _frameTimeCache.putIfAbsent(id, () => _parse(id)); + +final Map _frameTimeCache = {}; + +DateTime _parse(String id) { final epoch = int.tryParse(id); if (epoch != null) { final ms = epoch >= 1000000000000 ? epoch : epoch * 1000; diff --git a/lib/shared/navigation/app_routes.dart b/lib/shared/navigation/app_routes.dart index 14a73f4b5..80607da0f 100644 --- a/lib/shared/navigation/app_routes.dart +++ b/lib/shared/navigation/app_routes.dart @@ -48,6 +48,34 @@ abstract final class AppRoutes { static const String weatherRanking = 'weatherRanking'; static const String weatherRankingPath = 'weather-ranking'; + /// Lunar phase — nested under [dataPath]. Local computation only. + static const String moon = 'moon'; + static const String moonPath = 'moon'; + + /// Daylight, twilight and the solar terms — nested under [dataPath]. + static const String sun = 'sun'; + static const String sunPath = 'sun'; + + /// The planets tonight — nested under [dataPath]. + static const String planets = 'planets'; + static const String planetsPath = 'planets'; + + /// Tonight's observing window and what is up — nested under [dataPath]. + static const String tonight = 'tonight'; + static const String tonightPath = 'tonight'; + + /// 農曆 and upcoming eclipses — nested under [dataPath]. + static const String almanac = 'almanac'; + static const String almanacPath = 'almanac'; + + /// The naked-eye sky chart — nested under [dataPath]. + static const String skyChart = 'skyChart'; + static const String skyChartPath = 'sky-chart'; + + /// Astronomical tidal forcing — nested under [dataPath]. + static const String tide = 'tide'; + static const String tidePath = 'tide'; + static const String more = 'more'; static const String morePath = '/more'; @@ -77,6 +105,10 @@ abstract final class AppRoutes { static const String log = 'log'; static const String logPath = '/log'; + /// LoRa mesh (Meshtastic) BLE test page — scan/connect/chat against a radio. + static const String meshtastic = 'meshtastic'; + static const String meshtasticPath = '/meshtastic'; + /// App release notes (GitHub releases). static const String changelog = 'changelog'; static const String changelogPath = '/changelog'; diff --git a/lib/shared/navigation/refresh_on_appear.dart b/lib/shared/navigation/refresh_on_appear.dart index 69904562d..b267d0ac7 100644 --- a/lib/shared/navigation/refresh_on_appear.dart +++ b/lib/shared/navigation/refresh_on_appear.dart @@ -119,6 +119,14 @@ class _RefreshOnAppearState extends State /// An [InheritedWidget] rather than a provider so a page can be pumped in a test /// (or hosted outside the shell) without one: [of] returns null and /// [RefreshOnAppear] then treats the page as always visible. +/// +/// The shell hands the **same** [VisibleTab] instance down for the page's +/// whole life, so [updateShouldNotify] can never fire (both sides of the +/// comparison read the same object, and its value has already moved by the +/// time a rebuild compares them). Consumers must therefore subscribe to the +/// notifier itself in `didChangeDependencies` — `visibleTab.addListener(...)` — +/// like [RefreshOnAppear], [BaseMap], and the wind overlay do. Reading the +/// scope is how a consumer *finds* the notifier; it is not a change signal. class VisibleTabScope extends InheritedWidget { const VisibleTabScope({ super.key, diff --git a/lib/shared/widgets/intensity_legend.dart b/lib/shared/widgets/intensity_legend.dart index 0ca49a089..66e534ff8 100644 --- a/lib/shared/widgets/intensity_legend.dart +++ b/lib/shared/widgets/intensity_legend.dart @@ -36,9 +36,8 @@ class IntensityLegend extends StatelessWidget { // Pin the line height so a label's box never exceeds a [_cell]-tall row — // otherwise the taller default line height spaces the EEW cells apart and // the intended continuous bar fragments. - final labelStyle = Theme.of( - context, - ).textTheme.labelSmall?.copyWith(height: 1); + final labelStyle = Theme.of(context).textTheme.labelSmall + ?.copyWith(height: 1); return mode == IntensityLegendMode.rts ? _rtsScale(labelStyle) : _eewScale(labelStyle); diff --git a/lib/shared/widgets/location_permission_banner.dart b/lib/shared/widgets/location_permission_banner.dart index f5dec972d..67f75b477 100644 --- a/lib/shared/widgets/location_permission_banner.dart +++ b/lib/shared/widgets/location_permission_banner.dart @@ -47,9 +47,8 @@ class LocationPermissionBanner extends StatelessWidget { Expanded( child: Text( message, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colors.onErrorContainer, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colors.onErrorContainer), ), ), TextButton( diff --git a/lib/shared/widgets/map_chip_button.dart b/lib/shared/widgets/map_chip_button.dart index 2de4fb39a..de29428e4 100644 --- a/lib/shared/widgets/map_chip_button.dart +++ b/lib/shared/widgets/map_chip_button.dart @@ -141,9 +141,8 @@ class MapMenuDivider extends StatelessWidget { ), child: Divider( height: 1, - color: Theme.of( - context, - ).colorScheme.outlineVariant.withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outlineVariant + .withValues(alpha: 0.5), ), ); } diff --git a/lib/shared/widgets/notification_permission_banner.dart b/lib/shared/widgets/notification_permission_banner.dart index 375196000..e510512ee 100644 --- a/lib/shared/widgets/notification_permission_banner.dart +++ b/lib/shared/widgets/notification_permission_banner.dart @@ -81,9 +81,8 @@ class _NotificationPermissionBannerState Expanded( child: Text( l10n.notifyBannerDisabled, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colors.onErrorContainer, - ), + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colors.onErrorContainer), ), ), TextButton( diff --git a/lib/shared/widgets/realtime_view.dart b/lib/shared/widgets/realtime_view.dart index 01540a42e..6eefcd571 100644 --- a/lib/shared/widgets/realtime_view.dart +++ b/lib/shared/widgets/realtime_view.dart @@ -123,9 +123,8 @@ class _FreshnessBanner extends StatelessWidget { const SizedBox(width: AppSpacing.sm), Text( label, - style: Theme.of( - context, - ).textTheme.labelLarge?.copyWith(color: foreground), + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(color: foreground), ), ], ), diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..ea2d89d05 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,32 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import awesome_notifications +import firebase_core +import firebase_messaging +import flutter_blue_plus_darwin +import geolocator_apple +import in_app_purchase_storekit +import package_info_plus +import share_plus +import shared_preferences_foundation +import sqflite_darwin +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 000000000..167132a2f --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 000000000..795135b5c --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - awesome_notifications (0.12.0): + - FlutterMacOS + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - awesome_notifications (from `Flutter/ephemeral/.symlinks/plugins/awesome_notifications/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + awesome_notifications: + :path: Flutter/ephemeral/.symlinks/plugins/awesome_notifications/macos + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + awesome_notifications: 4e05708c3d44949fca858ace458b3c2ee823da8f + FlutterMacOS: c232990155153907050900a2e175c7773903ba4e + +PODFILE CHECKSUM: 1e95c36afbfd1cb6423ceca4de7a8e1b256fb6ac + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..db692292e --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,825 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 7D8FA1934B33054489FFC783 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7942483BDD4BE4115BB9143E /* Pods_Runner.framework */; }; + D331474A99BC54782397B40B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8DC769F1EF2D111DD46756 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 05422854A0BE1053194A7501 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 0A3DCBFBFF5CCA0B94E86D22 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1B8DC769F1EF2D111DD46756 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* dpip.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dpip.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 48E8F5178FBFA0F8C69A4173 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 681AD74598C459F7F7256371 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7942483BDD4BE4115BB9143E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + DB139019BC2395D1508DA44A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DE5216EABA163CDB8F22F7A0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D331474A99BC54782397B40B /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 7D8FA1934B33054489FFC783 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + A08E85351B2627510C9F8F12 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* dpip.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + A08E85351B2627510C9F8F12 /* Pods */ = { + isa = PBXGroup; + children = ( + DB139019BC2395D1508DA44A /* Pods-Runner.debug.xcconfig */, + 681AD74598C459F7F7256371 /* Pods-Runner.release.xcconfig */, + 48E8F5178FBFA0F8C69A4173 /* Pods-Runner.profile.xcconfig */, + 0A3DCBFBFF5CCA0B94E86D22 /* Pods-RunnerTests.debug.xcconfig */, + 05422854A0BE1053194A7501 /* Pods-RunnerTests.release.xcconfig */, + DE5216EABA163CDB8F22F7A0 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7942483BDD4BE4115BB9143E /* Pods_Runner.framework */, + 1B8DC769F1EF2D111DD46756 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 4DCF2CD99520323860480D5E /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 19F2836D47C185D821D65F11 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 0040772DB111EC287515B8B4 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* dpip.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0040772DB111EC287515B8B4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 19F2836D47C185D821D65F11 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 4DCF2CD99520323860480D5E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0A3DCBFBFF5CCA0B94E86D22 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/dpip.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/dpip"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 05422854A0BE1053194A7501 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/dpip.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/dpip"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DE5216EABA163CDB8F22F7A0 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/dpip.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/dpip"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..851094125 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,122 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", + "version" : "11.3.1" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270", + "version" : "12.15.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55", + "version" : "3.6.1" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b", + "version" : "12.15.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3", + "version" : "8.1.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc", + "version" : "5.3.1" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163", + "version" : "2.30910.1" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" + } + } + ], + "version" : 2 +} diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..e672226b5 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..851094125 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,122 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", + "version" : "11.3.1" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270", + "version" : "12.15.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55", + "version" : "3.6.1" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b", + "version" : "12.15.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3", + "version" : "8.1.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc", + "version" : "5.3.1" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163", + "version" : "2.30910.1" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" + } + } + ], + "version" : 2 +} diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..b3c176141 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..6b07b4f8d --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = dpip + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.exptech.dpip.dpip + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.exptech.dpip. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..3ba6c1266 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..3cc05eb23 --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 000000000..ee95ab7e5 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..61f3bd1fc --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/mise.toml b/mise.toml index bc02215ce..d1cc18b60 100644 --- a/mise.toml +++ b/mise.toml @@ -2,4 +2,4 @@ # Pin the numeric stable release only — the platform URLs already append # `-stable`. A bare `"3.44.8-stable"` makes mise request # `…_3.44.8-stable-stable.{tar.xz,zip}` and 404s on install (breaks CI). -flutter = { version = "3.44.8", platforms = { linux-x64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_{{ version }}-stable.tar.xz" }, macos-arm64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_{{ version }}-stable.zip" }, macos-x64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_{{ version }}-stable.zip" }, windows-x64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_{{ version }}-stable.zip" } }, version_expr = 'fromJSON(body).releases | filter({ #.channel == "stable" }) | map({ replace(#.version, "-stable", "") }) | sortVersions()', version_list_url = "https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json" } +flutter = { version = "3.47.0", platforms = { linux-x64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_{{ version }}-stable.tar.xz" }, macos-arm64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_{{ version }}-stable.zip" }, macos-x64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_{{ version }}-stable.zip" }, windows-x64 = { url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_{{ version }}-stable.zip" } }, version_expr = 'fromJSON(body).releases | filter({ #.channel == "stable" }) | map({ replace(#.version, "-stable", "") }) | sortVersions()', version_list_url = "https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json" } diff --git a/pubspec.lock b/pubspec.lock index 29e4f24ba..8514190b4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc" + sha256: "1b0e6a07425a3e460666e88bf1c949ccc7bb0116ad562ce94a1eca60fe820725" url: "https://pub.dev" source: hosted - version: "96.0.0" + version: "103.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,18 @@ packages: dependency: transitive description: name: analyzer - sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0" + sha256: "61c04d0c1bfed555c681ea079519933f071a5a026578ff73c4ff0df2d3462e5e" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "13.3.0" + analyzer_buffer: + dependency: transitive + description: + name: analyzer_buffer + sha256: "445b77e2054fa3e8c8a8ef1b5e9e6b23bb8028fffd34b5e60eaef315b7750674" + url: "https://pub.dev" + source: hosted + version: "0.3.3" ansicolor: dependency: transitive description: @@ -65,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.12.1" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" boolean_selector: dependency: transitive description: @@ -77,34 +93,34 @@ packages: dependency: transitive description: name: build - sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + sha256: b94f5da9ed3d081fd4ecc426c260998b5f4f4eb2f6d89b7e5472edef0b2b2a1b url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "4.0.10" build_config: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: "94eaf6708fe64408c632ef2689ca3777b112f9421306ccf4f8c84d7c5c9f83f8" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: "79e05eaf15a48d7230b053a4363b8eaac0cc234bbd0134c3229455481f55cbc6" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.5" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + sha256: "90363d438bd9f84a4d5bbb83352251f57d2d9d771bc95a44e6a33fe25fa52774" url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "2.16.0" built_collection: dependency: transitive description: @@ -117,10 +133,10 @@ packages: dependency: transitive description: name: built_value - sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" url: "https://pub.dev" source: hosted - version: "8.12.6" + version: "8.12.7" characters: dependency: transitive description: @@ -149,10 +165,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" collection: dependency: transitive description: @@ -173,10 +189,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.5+4" crypto: dependency: transitive description: @@ -185,14 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" dart_style: dependency: transitive description: name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + sha256: "3f88fc9c96c568d631356507355a2d1e983ee000c9ac008bdcb39b5bf53ce777" url: "https://pub.dev" source: hosted - version: "3.1.7" + version: "3.1.12" dbus: dependency: transitive description: @@ -205,18 +229,18 @@ packages: dependency: "direct main" description: name: dio - sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 + sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c" url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.11.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 + sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" equatable: dependency: transitive description: @@ -326,6 +350,54 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" + url: "https://pub.dev" + source: hosted + version: "1.36.8" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" + url: "https://pub.dev" + source: hosted + version: "7.0.4" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 + url: "https://pub.dev" + source: hosted + version: "7.0.2" flutter_lints: dependency: "direct dev" description: @@ -369,10 +441,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 + sha256: "9ec135696554923c59339d46dd50adbaf81099be06ffda0fb9c06f410aea9137" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "4.0.0-dev.3" freezed_annotation: dependency: "direct main" description: @@ -457,10 +529,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a" + sha256: d7a3576cb312649eaa51f2356450aed686085fb58fcdebda5b359aa951eef7ea url: "https://pub.dev" source: hosted - version: "17.3.0" + version: "17.5.0" graphs: dependency: transitive description: @@ -489,10 +561,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -545,26 +617,26 @@ packages: dependency: transitive description: name: in_app_purchase_platform_interface - sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" + sha256: "0b0076cac8ce4fa7048f01e76af8b123aeb6a7c4e0dea2a5206d6664454f3e36" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" in_app_purchase_storekit: dependency: transitive description: name: in_app_purchase_storekit - sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" + sha256: "9602e249a0e30351f047d5715957f27709ed7b42f631fba8941dcad51489932a" url: "https://pub.dev" source: hosted - version: "0.4.8+1" + version: "0.4.11+1" intl: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" io: dependency: transitive description: @@ -577,18 +649,26 @@ packages: dependency: transitive description: name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.3" jni_flutter: dependency: transitive description: name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" json_annotation: dependency: "direct main" description: @@ -601,10 +681,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 + sha256: e45aefa0324f08c683caafbb94b72837aa6193c61822799c916e45f4a263113d url: "https://pub.dev" source: hosted - version: "6.14.0" + version: "6.14.1" leak_tracker: dependency: transitive description: @@ -638,7 +718,7 @@ packages: source: hosted version: "6.1.0" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 @@ -649,26 +729,28 @@ packages: dependency: "direct main" description: path: maplibre_gl - ref: "7c2e249149ff3023c54c86a1f4c0dca5fb43d6a1" - resolved-ref: "7c2e249149ff3023c54c86a1f4c0dca5fb43d6a1" + ref: "0674b9be9f4dc2ee7146d528ed9225bb0ce86baf" + resolved-ref: "0674b9be9f4dc2ee7146d528ed9225bb0ce86baf" url: "https://github.com/ExpTechTW/flutter-maplibre-gl.git" source: git version: "0.26.2" maplibre_gl_platform_interface: - dependency: transitive + dependency: "direct main" description: - name: maplibre_gl_platform_interface - sha256: "1f0ca8a99f03fa9434618ee21f4e42dd615830a7dd973e632b11c73ffec993a8" - url: "https://pub.dev" - source: hosted + path: maplibre_gl_platform_interface + ref: "0674b9be9f4dc2ee7146d528ed9225bb0ce86baf" + resolved-ref: "0674b9be9f4dc2ee7146d528ed9225bb0ce86baf" + url: "https://github.com/ExpTechTW/flutter-maplibre-gl.git" + source: git version: "0.26.2" maplibre_gl_web: - dependency: transitive + dependency: "direct overridden" description: - name: maplibre_gl_web - sha256: bbf022f29ceef26d73f63e584819fbd02fbaf4eb1facf5234206c78936cf8f1c - url: "https://pub.dev" - source: hosted + path: maplibre_gl_web + ref: "0674b9be9f4dc2ee7146d528ed9225bb0ce86baf" + resolved-ref: "0674b9be9f4dc2ee7146d528ed9225bb0ce86baf" + url: "https://github.com/ExpTechTW/flutter-maplibre-gl.git" + source: git version: "0.26.2" markdown: dependency: transitive @@ -682,10 +764,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -694,14 +776,21 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" + meshtastic_flutter: + dependency: "direct main" + description: + path: "third_party/meshtastic_flutter" + relative: true + source: path + version: "0.0.3" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.18.3" mime: dependency: transitive description: @@ -714,10 +803,10 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72 url: "https://pub.dev" source: hosted - version: "0.17.6" + version: "0.19.2" nested: dependency: transitive description: @@ -730,26 +819,26 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.2.1" package_info_plus_platform_interface: dependency: transitive description: @@ -814,6 +903,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8 + url: "https://pub.dev" + source: hosted + version: "9.6.1" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac" + url: "https://pub.dev" + source: hosted + version: "0.1.4+1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23 + url: "https://pub.dev" + source: hosted + version: "4.4.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd + url: "https://pub.dev" + source: hosted + version: "0.2.2" petitparser: dependency: transitive description: @@ -854,6 +991,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.2" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e + url: "https://pub.dev" + source: hosted + version: "4.2.0" provider: dependency: "direct main" description: @@ -878,22 +1023,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" share_plus: dependency: transitive description: name: share_plus - sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" + sha256: "34f00f9becd2743c1fb05363d624f9f70d37f7ccdcdda47450bc0b8c9d327b8c" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.3.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9" + sha256: "365ef7379fc22507256adda3385152942ffce08935452bc972c2e52a0bebae41" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" shared_preferences: dependency: "direct main" description: @@ -906,10 +1067,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" url: "https://pub.dev" source: hosted - version: "2.4.26" + version: "2.4.27" shared_preferences_foundation: dependency: transitive description: @@ -975,18 +1136,18 @@ packages: dependency: transitive description: name: source_gen - sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.4" source_helper: dependency: transitive description: name: source_helper - sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" + sha256: "5e6f216fdf6376c9f3852381ae037499797a3385377d388b011dac98d303c67c" url: "https://pub.dev" source: hosted - version: "1.3.12" + version: "1.3.13" source_span: dependency: transitive description: @@ -1047,10 +1208,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "61c7930bebf32c552ac5808c91bf33802e66711c9216090d3b5579c9f862e80c" + sha256: "64b2c63c8232dd20d14b34105a81ebfd74320442e8451f836179ec89986aa478" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.5.1" stack_trace: dependency: transitive description: @@ -1087,34 +1248,34 @@ packages: dependency: transitive description: name: synchronized - sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.4.1+1" talker: dependency: transitive description: name: talker - sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757 + sha256: "57b160b70123fdb28cc1035987e4718bce5715c724a40ff74d6466d1559de68a" url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.20" talker_flutter: dependency: "direct main" description: name: talker_flutter - sha256: "7e4b5fb520b4dadfc8db97e73a2a76ea5d6eda471a51489f3c0bd58b96a1ed43" + sha256: "9be092f9661d217edcfb3e2d7e3581a290a69ecfbbd2e19e582ff9d49e7f0e97" url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.20" talker_logger: dependency: transitive description: name: talker_logger - sha256: "459205c3e571f97ecc6be6e1b1b7e6b97b853e78ea458894650be407596e3216" + sha256: b24550d115f209a1b84a697d65f5ec0fa9eb483735c802f18077692fa83cb5f8 url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.20" term_glyph: dependency: transitive description: @@ -1127,10 +1288,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" typed_data: dependency: transitive description: @@ -1191,10 +1352,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -1207,18 +1368,18 @@ packages: dependency: transitive description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_math: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.0" vm_service: dependency: transitive description: @@ -1263,10 +1424,10 @@ packages: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.0" xdg_directories: dependency: transitive description: @@ -1292,5 +1453,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.12.2 <4.0.0" + dart: ">=3.13.0 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index b5aa9db13..80c3a05a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,15 +4,18 @@ publish_to: 'none' version: 3.9.9+1 environment: - sdk: ^3.12.2 + sdk: ^3.13.0 dependencies: awesome_notifications: ^0.12.1 + cupertino_icons: ^1.0.8 dio: ^5.10.0 # Firebase pinned deliberately — a newer major bumps the min iOS target and # breaks older-device compatibility. Do not upgrade without checking that. - firebase_core: ^4.11.0 - firebase_messaging: ^16.4.1 + # Pinned to the exact version (not `^`) so a plain `pub upgrade` can't drift + # the minor either. + firebase_core: 4.11.0 + firebase_messaging: 16.4.1 flutter: sdk: flutter flutter_localizations: @@ -26,17 +29,45 @@ dependencies: in_app_purchase: ^3.3.0 intl: ^0.20.2 json_annotation: ^4.12.0 - # ExpTech fork — HTTPS intercept + Dart tile-cache bridge + Android gzip fix. + # Direct dep: MeshtasticClientImpl bridges the vendored package's + # `package:logging` records into the app Log. + logging: ^1.3.0 + # ExpTech fork — HTTPS intercept + Dart tile-cache bridge + Android gzip fix + # + setRenderPaused (pause native render loop for hidden tabs). # Requires MapLibre Native 6.27.0 (ios/vendor/maplibre). # https://github.com/ExpTechTW/flutter-maplibre-gl # Pin a commit (not floating `main`) so CI/`pub get` can't rewrite # pubspec.lock. Local path override stays in gitignored # `pubspec_overrides.yaml` — after `pub get` with it, discard lock churn. + # The fork's controller.dart calls `setRenderPaused`, which exists only in + # the fork's interface/web packages — pub.dev's 0.26.2 lacks it. The + # overrides below force those two siblings (declared hosted ^0.26.2 inside + # the fork) to resolve from the same fork commit, so `setRenderPaused` + # exists on every platform. maplibre_gl: git: url: https://github.com/ExpTechTW/flutter-maplibre-gl.git path: maplibre_gl - ref: 7c2e249149ff3023c54c86a1f4c0dca5fb43d6a1 + ref: 0674b9be9f4dc2ee7146d528ed9225bb0ce86baf + # Direct (not just override) so tests can import the platform interface. + maplibre_gl_platform_interface: + git: + url: https://github.com/ExpTechTW/flutter-maplibre-gl.git + path: maplibre_gl_platform_interface + ref: 0674b9be9f4dc2ee7146d528ed9225bb0ce86baf + # LoRa mesh (Meshtastic) over BLE — off-grid emergency messaging. Requires + # Bluetooth + location permissions (Android manifest / iOS Info.plist below). + # Vendored (third_party/) with two upstream fixes: requestMtu is skipped off + # Android (CoreBluetooth negotiates MTU; flutter_blue_plus throws there) and + # text/JSON payloads decode as UTF-8 (fromCharCodes garbles CJK). + meshtastic_flutter: + path: third_party/meshtastic_flutter + # Direct dep: MeshtasticClientImpl's device map holds flutter_blue_plus's + # BluetoothDevice (meshtastic_flutter doesn't re-export it). + flutter_blue_plus: ^1.36.8 + # Direct dep: the impl requests Bluetooth/location permissions itself so a + # denial surfaces a typed failure with a settings path. + permission_handler: ^12.0.3 package_info_plus: ^10.2.0 path_provider: ^2.1.6 provider: ^6.1.5+1 @@ -49,12 +80,33 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - build_runner: ^2.15.0 - freezed: ^3.1.0 - json_serializable: ^6.14.0 + # Dart 3.13 (Flutter 3.47) makes `final` illegal on non-primary-constructor + # parameters — freezed 3.2.x emits `required final T x` for collections, so + # it can no longer compile. Only the 4.0 line (dev.3 today) supports 3.13; + # pinned as prerelease until it goes stable. + build_runner: ^2.16.0 + freezed: ^4.0.0-dev.3 + json_serializable: ^6.14.1 sqflite_common_ffi: ^2.4.2 url_launcher_platform_interface: ^2.3.2 +dependency_overrides: + # Same fork commit as the direct git dependencies above — the fork's web + # package declares the interface as hosted ^0.26.2, which would resolve to + # pub.dev (no setRenderPaused) and break compilation. Inline overrides live + # in pubspec.yaml (not the gitignored pubspec_overrides.yaml) so CI's plain + # `pub get` resolves identically and pubspec.lock stays stable. + maplibre_gl_platform_interface: + git: + url: https://github.com/ExpTechTW/flutter-maplibre-gl.git + path: maplibre_gl_platform_interface + ref: 0674b9be9f4dc2ee7146d528ed9225bb0ce86baf + maplibre_gl_web: + git: + url: https://github.com/ExpTechTW/flutter-maplibre-gl.git + path: maplibre_gl_web + ref: 0674b9be9f4dc2ee7146d528ed9225bb0ce86baf + flutter: config: enable-swift-package-manager: true @@ -68,35 +120,19 @@ flutter: - shaders/weather/sun_flare.frag - shaders/weather/galaxy.frag - shaders/weather/night.frag + - shaders/weather/night_field.frag + - shaders/weather/moon_display.frag - shaders/weather/rain_on_glass.frag - shaders/weather/lightning.frag - shaders/weather/rainbow.frag - shaders/weather/card_water.frag assets: - assets/DPIP.png - - assets/box.json + - assets/box.json.gz - assets/weather/clouds/ - assets/weather/particles/ - assets/weather/sky/ - assets/location.json.gz - assets/map/town_boundaries.bin.gz - assets/travel_time.json.gz - - assets/map/icons/cross.png - - assets/map/icons/intensity-1.png - - assets/map/icons/intensity-1-dark.png - - assets/map/icons/intensity-2.png - - assets/map/icons/intensity-2-dark.png - - assets/map/icons/intensity-3.png - - assets/map/icons/intensity-3-dark.png - - assets/map/icons/intensity-4.png - - assets/map/icons/intensity-4-dark.png - - assets/map/icons/intensity-5.png - - assets/map/icons/intensity-5-dark.png - - assets/map/icons/intensity-6.png - - assets/map/icons/intensity-6-dark.png - - assets/map/icons/intensity-7.png - - assets/map/icons/intensity-7-dark.png - - assets/map/icons/intensity-8.png - - assets/map/icons/intensity-8-dark.png - - assets/map/icons/intensity-9.png - - assets/map/icons/intensity-9-dark.png + - assets/astro/ diff --git a/shaders/README.md b/shaders/README.md index d0aec408a..94da48919 100644 --- a/shaders/README.md +++ b/shaders/README.md @@ -1,8 +1,8 @@ # shaders — the weather scene A layered, GPU-rendered weather backdrop: an atmospheric sky baked into two -lookup tables, a deck of lit cloud sprites over it, and the precipitation, -lightning, flare and rainbow layers on top. +lookup tables, a deck of lit cloud sprites over it, and the precipitation +particles, lightning, flare and rainbow layers on top. The pipeline follows a reference implementation whose behaviour was measured rather than copied; every texture it draws is generated by the scripts in @@ -39,7 +39,7 @@ MIT-licensed. The constants are that paper's standard Earth atmosphere, in **metres** and **1/metre**: Rayleigh `(5.802e-6, 1.3558e-5, 3.31e-5)`, ozone `(6.5e-7, 1.881e-6, 8.5e-8)`, planet 6 360 000 m, atmosphere 6 460 000 m, 30 march steps. The per-keyframe values live in -`lib/.../weather_sky/sky_keyframe_data.dart`. +`lib/features/home/presentation/widgets/weather_sky/sky_keyframe_data.dart`. The visible sky is a deliberately narrow **~19° band** starting just above the horizon, not a dome — the horizon band is the busiest part of the sky and a @@ -48,19 +48,37 @@ change available to the look. ## Layer stack (paint order) +The `WeatherSkyPainter` draws eight layers; rain and snow are **sprite +particles** (`PrecipitationField`), not shaders: + ``` -sky CPU-baked gradient from the LUT column → sky_lut_cache (gradient) -night stars + Milky Way + moon → weather/night.frag -clouds lit sprites, drawn per instance → cloud/clouds.frag -rain multi-depth streak curtain → weather/rain.frag -snow multi-depth octagonal flakes → weather/snow.frag -lightning bolt + full-screen flash → weather/lightning.frag -sun_flare crepuscular rays, ghosts, ring → weather/sun_flare.frag -rainbow spectral primary + secondary bow → weather/rainbow.frag -fog raymarched drifting banks → weather/fog.frag -grade grain + scene cast (softLight) → post/grade.frag +sky CPU-baked gradient from the LUT column → SkyLutCache (gradient) +night stars + Milky Way band + moon → weather/night.frag +clouds lit sprites, drawn per instance → cloud/clouds.frag +rain multi-depth streak curtain (particles) → PrecipitationField +snow multi-depth octagonal flakes (particles) → PrecipitationField +lightning bolt + full-screen flash → weather/lightning.frag +sun_flare disc + rays + ghosts + ring → weather/sun_flare.frag +rainbow spectral primary + secondary bow → weather/rainbow.frag ``` +Two more shaders run *off the backdrop*, as effects on the cards themselves: + +| shader | role | +|---|---| +| `weather/rain_on_glass.frag` | droplets clinging to a card face, refracted via `RenderEffect` (`rain_on_glass.dart`) | +| `weather/card_water.frag` | water pooled on the panel edge, composited by `rain_on_card.dart` | + +## Which shaders actually load + +`pubspec.yaml` lists 11 fragment shaders; the runtime loads **9** of them +(transmittance, sky_lut, night, clouds, lightning, sun_flare, rainbow, +rain_on_glass, card_water — assets resolved in +`weather_sky_background.dart` / `weather_sky_painter.dart`). The two leftovers +are deliberate: `sky_view.frag` is the superseded screen pass kept as the +preview reference, and `galaxy.frag` was removed from production (the dedicated +galaxy layer never loaded; `night.frag` carries its own galaxy tint at 0.92). + ## Clouds Clouds are **authored volumetric sprites with baked normal maps**, not diff --git a/shaders/weather/moon_display.frag b/shaders/weather/moon_display.frag new file mode 100644 index 000000000..29ddbe6b4 --- /dev/null +++ b/shaders/weather/moon_display.frag @@ -0,0 +1,170 @@ +// Moon **display** shader — the real Moon, lit for a given phase. +// +// The surface is not invented here. It is NASA/GSFC's CGI Moon Kit: a colour +// map built from over 100,000 Lunar Reconnaissance Orbiter Wide Angle Camera +// images, and an elevation map from the LOLA laser altimeter. Procedural +// craters can be made to look plausible but never look like *this* Moon — +// Tycho's rays, Mare Tranquillitatis, Copernicus and Grimaldi are recognised, +// not evaluated, and getting them wrong is the whole difference between "a +// moon" and "the Moon". +// +// What makes it read as a sphere rather than a printed disc: +// +// 1. **Orthographic sphere lookup.** Each pixel of the disc is turned into a +// point on a unit sphere, then into latitude/longitude, then into a +// texel of the equirectangular map. That is what compresses features +// toward the limb — a flat texture lookup leaves the maria the same size +// at the edge as at the centre, which the eye reads immediately as a +// sticker. +// 2. **Relief from real elevation.** Normals are finite-differenced from the +// height map in *surface* space and rotated into view space, so the +// shading of a crater depends on where it sits on the globe. Under +// grazing light near the terminator this is what throws the long shadows +// that make the phase look three-dimensional. +// 3. **Lommel-Seeliger scattering, not Lambert.** Lunar regolith is +// backscattering: a Lambert sphere darkens steadily toward the limb, but +// the real full Moon is famously *flat* and bright all the way to the +// edge. The L/(L+V) term is the standard first-order fix and is the +// single biggest reason a rendered full moon looks wrong or right. +// 4. **Opposition surge.** Near full, shadows hide behind the grains that +// cast them and the disc brightens sharply — a few degrees wide, and very +// characteristic. +// 5. **Earthshine.** The night side is not black: it is lit by a nearly full +// Earth, blue-grey, strongest around new moon. +// 6. **The observer's tilt.** The pole and the terminator are placed by two +// *independent* bearings supplied from Dart, not by assuming north is up +// and the terminator vertical. Seen from Taiwan the crescent is rolled, +// and near the horizon it lies on its back; the two angles also differ +// from each other by up to ~30°, because the Sun sits on the ecliptic +// while the Moon's axis follows the equator. +// +// Uniform contract — slots are float indices in declaration order. +// iResolution (0..1) draw size in pixels +// iPhase (2) phase angle in radians, 0 = new, π = full +// iLibration (3..4) sub-earth longitude/latitude offset, radians +// iNorthRoll (5) screen bearing of the Moon's north pole, radians, +// measured from straight up and increasing clockwise +// iLimb (6) screen bearing of the lit limb, same convention +// iColor (s0) equirectangular colour map, 0° longitude centred +// iHeight (s1) equirectangular elevation map, greyscale +#include + +precision highp float; + +uniform vec2 iResolution; +uniform float iPhase; +uniform vec2 iLibration; +uniform float iNorthRoll; +uniform float iLimb; +uniform sampler2D iColor; +uniform sampler2D iHeight; + +out vec4 fragColor; + +const float PI = 3.14159265359; + +/// Equirectangular texel for a point on the unit sphere, in surface space. +/// +/// The maps are centred on 0° longitude — the middle of the near side — so the +/// centre of the disc must land on u = 0.5. `+z` points at the viewer, hence +/// `atan(x, z)`: negating z instead would centre the disc on ±180° and quietly +/// render the *far* side, which is the same brightness and the wrong Moon. +vec2 sphereUv(vec3 p) { + float lon = atan(p.x, p.z); + float lat = asin(clamp(p.y, -1.0, 1.0)); + return vec2(lon / (2.0 * PI) + 0.5, 0.5 - lat / PI); +} + +float heightAt(vec3 p) { return texture(iHeight, sphereUv(p)).r; } + +/// Screen space to the Moon's own frame. +/// +/// Two rotations. The first rolls about the line of sight so the Moon's north +/// pole ends up where the observer actually sees it. The second is the +/// libration: the Moon rocks a few degrees each month, showing a little around +/// each limb in turn, and iLibration is the selenographic point facing Earth — +/// so this brings that point to the centre of the disc. +vec3 toMoonFrame(vec3 v) { + float cr = cos(iNorthRoll), sr = sin(iNorthRoll); + vec3 r = vec3(cr * v.x - sr * v.y, sr * v.x + cr * v.y, v.z); + float cl = cos(iLibration.x), sl = sin(iLibration.x); + float cb = cos(iLibration.y), sb = sin(iLibration.y); + float ex = -sl * r.x + cl * r.z; + return vec3(cl * r.x + sl * r.z, cb * r.y + sb * ex, cb * ex - sb * r.y); +} + +void main() { + vec2 uv = FlutterFragCoord().xy / iResolution; + vec2 p = uv * 2.0 - 1.0; + // Flip y: screen y grows downward, the sphere's north is up. + p.y = -p.y; + + float r2 = dot(p, p); + // Anti-aliased limb, one pixel wide in screen terms. + float px = 2.0 / iResolution.y; + float limb = 1.0 - smoothstep(1.0 - px * 1.5, 1.0, sqrt(r2)); + if (limb <= 0.0) { + fragColor = vec4(0.0); + return; + } + + // Screen-space normal of the visible hemisphere: the disc *is* the sphere + // seen orthographically, so z falls out of x and y. + vec3 view = vec3(p, sqrt(max(1.0 - r2, 0.0))); + + // Everything below is done in the Moon's own frame. Rotating the *light* and + // the *eye* into it, rather than rotating a tangent basis back out of it, + // keeps one frame in play: dot products are rotation-invariant, so the + // shading is identical and there is no half-transformed vector to get wrong. + vec3 surf = toMoonFrame(view); + vec3 albedo = texture(iColor, sphereUv(surf)).rgb; + + // Relief: sample the elevation along two tangents of the sphere and tilt the + // normal by the slope. In surface space, so a crater near the limb is shaded + // by its own geometry rather than by its screen position. + vec3 tangentU = normalize(cross(vec3(0.0, 1.0, 0.0), surf) + vec3(1e-5)); + vec3 tangentV = cross(surf, tangentU); + float step = 0.004; + float hU = heightAt(normalize(surf + tangentU * step)) - + heightAt(normalize(surf - tangentU * step)); + float hV = heightAt(normalize(surf + tangentV * step)) - + heightAt(normalize(surf - tangentV * step)); + // Relief is exaggerated: at true scale the Moon is smoother than a billiard + // ball and would show nothing at this size. + const float relief = 9.0; + vec3 n = normalize(surf - relief * (hU * tangentU + hV * tangentV)); + + // Sun direction, built in screen space and then carried into the Moon's + // frame. θ = 0 puts it behind the Moon (new) and θ = π in front (full); + // in between, its in-plane direction is the bright limb's screen bearing — + // measured from straight up, clockwise, so x = sin and y = cos. + vec3 sun = toMoonFrame(normalize(vec3( + sin(iPhase) * sin(iLimb), + sin(iPhase) * cos(iLimb), + -cos(iPhase) + ))); + vec3 eye = toMoonFrame(vec3(0.0, 0.0, 1.0)); + + float mu0 = dot(n, sun); // cos(incidence) + float mu = dot(n, eye); // cos(emission) + + // Lommel-Seeliger: bright to the limb, unlike Lambert. + float lit = clamp(mu0, 0.0, 1.0); + float scatter = lit > 0.0 ? mu0 / max(mu0 + mu, 0.05) : 0.0; + + // Opposition surge — a narrow brightening as the phase angle goes to zero. + float alpha = PI - iPhase; + float surge = 1.0 + 0.55 * exp(-abs(alpha) / 0.12); + + // Terminator softening: the Sun is half a degree wide, so the shadow edge is + // not a razor line. + float terminator = smoothstep(-0.06, 0.10, mu0); + + vec3 col = albedo * scatter * surge * terminator * 1.55; + + // Earthshine: a nearly full Earth lights the night side around new moon. + float earthshine = 0.055 * (1.0 + cos(iPhase)) * 0.5 * (1.0 - terminator); + col += albedo * vec3(0.45, 0.55, 0.85) * earthshine; + + fragColor = vec4(col, limb); +} diff --git a/shaders/weather/night.frag b/shaders/weather/night.frag index 72bd7d80e..5aeda9dc1 100644 --- a/shaders/weather/night.frag +++ b/shaders/weather/night.frag @@ -2,17 +2,20 @@ // engine's the reference shader (stars) and the look of // The reference shader (band). // -// Stars follow the reference's `drawStars` closely: the sky is diced into `numCells` -// cells, each holding one star at a hashed offset, drawn as a smoothstep of -// the distance to that centre. Three passes at 4/8/16 cells give bright, -// medium and faint stars. The bright pass also gets the reference's glow stack — a -// wide halo, a four-point diffraction cross built from the angle modulo 90°, -// and a tight core — gated by a slow per-star twinkle so only a few sparkle -// at once instead of the whole field shimmering. +// The star field itself is pre-baked into a tiling RGBA texture +// ([night_field.frag]: R = bright-core stars, G = bright glow, B = medium +// stars, A = faint stars) and sampled here — the CPU-side bake runs once per +// sky size, not per frame. What stays per-frame: // -// The reference samples a starmap texture for the galaxy; this draws the band -// procedurally: a rotated, softly-bounded strip filled with fbm dust and -// scattered faint stars, which needs no 4K texture upload. +// * the per-star shimmer (`star *= 1.2 + sin(…)·0.6`) — recomputed per cell +// from the same hashes the bake used, so every star keeps its own phase; +// * the glow's twinkle gate (`glow *= 0.5 + sin(…)·0.5`) — ditto, bright +// cells only; +// * the slow drift (`coord += iTime·0.004`). +// +// The hash grid repeats every 4 cells (`hash22(mod(cellID, numCells))`), and +// the texture tiles via `fract(coord / 4.0)` on the same cycle — sampling is +// exactly equivalent to the original per-pixel field, not an approximation. // // There is no moon: the reference's scene has no moon layer at all, and the one this // file used to draw was invented. @@ -25,6 +28,7 @@ // iScroll (9) parallax offset in pixels // iCloudCover (10) 0..1 cloud cover, dims the field // iGalaxy (11) Milky Way strength 0..1 +// sampler 0 iStarField — the baked RGBA star texture #include precision highp float; @@ -37,6 +41,8 @@ uniform float iScroll; uniform float iCloudCover; uniform float iGalaxy; +uniform sampler2D iStarField; + out vec4 fragColor; #define PI 3.14159265 @@ -76,54 +82,29 @@ float fbm(vec2 p) { return v; } -/// One star field pass. [numCells] sets density, [size] the radius, [br] the -/// brightness; [showGlow] adds the halo and diffraction spikes. -float drawStars(vec2 coord, float numCells, float size, float br, - bool showGlow) { - vec2 bigCoord = coord * numCells; - vec2 cellID = floor(bigCoord); - - vec2 rnd = hash22(mod(cellID, numCells)); - vec2 centerOffset = rnd * 0.7 + rnd.x * 0.3; - // Keep glowing stars away from the cell edge so their halo is not clipped. - vec2 center = cellID + (showGlow ? mix(vec2(0.1), vec2(0.9), centerOffset) - : mix(vec2(0.01), vec2(0.99), centerOffset)); - - vec2 offset = (bigCoord - center) / (numCells * size); - float d = dot(offset, offset); - if (d > 0.16 || (!showGlow && d > 0.0025)) return 0.0; - - float sqrtd = 1.0 - sqrt(d); - float star = br * smoothstep(0.95, 1.0, sqrtd); - // Base shimmer, always on and subtle. - star *= 1.2 + sin(center.y * 10.0 + center.x * 71.9 + iTime * 2.0) * 0.6; - - if (!showGlow) return star; - - // Wide halo. - float glow = pow(clamp(br * smoothstep(0.6, 1.0, sqrtd), 0.0, 2.0), 3.5) * - 0.016666667; - - // Four-point diffraction cross: the spike is longest at ±45°/±135°. - float angle = atan(offset.y, offset.x) + PI * 0.25; - float angleOffset = abs(mod(angle, PI * 0.5) - PI * 0.25); - float len = mix(0.76, 0.88, sqrt(angleOffset) / (PI * 0.25)); - glow += smoothstep(len + 0.05, 1.1, sqrtd) * 0.5; - - // Tight core. - if (sqrtd > 0.895) { - float core = clamp(br * smoothstep(0.895, 1.0, sqrtd), 0.0, 2.0); - glow += core * core * 0.33333333; - } +/// The bright pass's star centre in cell [cell] — the same hash the bake used, +/// so per-star shimmer/twinkle phases line up with the sampled field. +vec2 starCenter(vec2 cell, float numCells, bool glow) { + vec2 rnd = hash22(mod(cell, numCells)); + vec2 offset = rnd * 0.7 + rnd.x * 0.3; + return cell + (glow ? mix(vec2(0.1), vec2(0.9), offset) + : mix(vec2(0.01), vec2(0.99), offset)); +} - // Twinkle gate: mostly dark, with a brief sine pulse every interval. +/// The `star *= 1.2 + sin(…)·0.6` shimmer for the star in [cell]. +float shimmer(vec2 cell, float numCells) { + vec2 center = starCenter(cell, numCells, false); + return 1.2 + sin(center.y * 10.0 + center.x * 71.9 + iTime * 2.0) * 0.6; +} + +/// The bright pass's twinkle gate (`glow *= 0.5 + sin(…)·0.5`) for [cell]. +float twinkle(vec2 cell) { + vec2 center = starCenter(cell, 4.0, true); float t = center.y * 721.3 + center.x * 37.1 + iTime * 2.0; float phase = mod(t, (2.0 + kTwinkleInterval) * PI) <= TWO_PI ? mod(t, TWO_PI) : TWO_PI; - glow *= 0.5 + sin(phase - PI * 0.5) * 0.5; - - return star + glow; + return 0.5 + sin(phase - PI * 0.5) * 0.5; } void main() { @@ -160,17 +141,24 @@ void main() { float density = band * mix(0.35, 1.0, dust) * (1.0 - 0.65 * dark); col += iGalaxyTint * density * 0.22 * galaxy * horizonFade; - // Unresolved stars inside the band. - col += vec3(1.0) * drawStars(coord * 1.3, 26.0, 0.02, 0.35, false) * - band * galaxy * horizonFade; } - // --- star field -------------------------------------------------------- - col += vec3(0.74, 0.74, 0.74) * drawStars(coord, 4.0, 0.08, 2.0, true) * + // --- star field (sampled, per-cell animation) -------------------------- + // The baked texture holds bright-cell world [0,4]; the field repeats on the + // same 4-cell cycle, so `fract` wraps exactly. + vec4 field = texture(iStarField, fract(coord / 4.0)); + + vec2 cell4 = floor(coord / 4.0); + vec2 cell8 = floor(coord / 8.0); + vec2 cell16 = floor(coord / 16.0); + + // Bright pass: the glow rides the twinkle gate, the core star the shimmer. + col += vec3(0.74, 0.74, 0.74) * + (field.r * shimmer(cell4, 4.0) + field.g * twinkle(cell4)) * horizonFade; - col += vec3(0.97, 0.85, 0.80) * drawStars(coord, 8.0, 0.05, 1.0, false) * + col += vec3(0.97, 0.85, 0.80) * field.b * shimmer(cell8, 8.0) * horizonFade; - col += vec3(0.85, 0.90, 1.00) * drawStars(coord, 16.0, 0.025, 0.5, false) * + col += vec3(0.85, 0.90, 1.00) * field.a * shimmer(cell16, 16.0) * horizonFade; col *= alpha; diff --git a/shaders/weather/night_field.frag b/shaders/weather/night_field.frag new file mode 100644 index 000000000..193089772 --- /dev/null +++ b/shaders/weather/night_field.frag @@ -0,0 +1,89 @@ +// Night star-field **bake** shader — renders the static star layers into a +// single tiling RGBA texture for `night.frag` to sample: +// +// R = bright pass core stars (4-cell grid) +// G = bright pass glow (halo + diffraction cross + tight core) +// B = medium pass stars (8-cell grid) +// A = faint pass stars (16-cell grid) +// +// The per-star shimmer and twinkle are multiplicative and time-driven, so they +// are NOT baked — `night.frag` recomputes them per cell from the same hashes +// this file uses, which keeps the frame-time animation identical. Splitting +// star and glow into separate channels is what lets the two be modulated +// differently at display time (shimmer rides the star, the twinkle gate the +// glow), exactly as the original single-pass shader did. +// +// The texture covers 4×4 bright cells of the world coordinate space (`coord` +// spans -2..2) and tiles via `fract` at sample time — the original field +// repeats every 4 cells anyway (`hash22(mod(cellID, numCells))`), so tiling is +// exactly equivalent, not an approximation. +// +// Uniform contract — slots are float indices in declaration order. +// iResolution (0..1) bake texture size in pixels +#include + +precision highp float; + +uniform vec2 iResolution; + +out vec4 fragColor; + +#define PI 3.14159265 + +vec2 hash22(vec2 p) { + p = vec2(dot(p, vec2(12.9898, 78.233)), dot(p, vec2(26.65125, 83.054543))); + return fract(sin(p) * 43758.5453); +} + +/// Core star brightness — `night.frag`'s `star` term before shimmer. +float starCore(vec2 coord, float numCells, float size, float br) { + vec2 bigCoord = coord * numCells; + vec2 cellID = floor(bigCoord); + vec2 rnd = hash22(mod(cellID, numCells)); + vec2 center = cellID + mix(vec2(0.01), vec2(0.99), rnd * 0.7 + rnd.x * 0.3); + vec2 offset = (bigCoord - center) / (numCells * size); + float d = dot(offset, offset); + if (d > 0.0025) return 0.0; + return br * smoothstep(0.95, 1.0, 1.0 - sqrt(d)); +} + +/// The bright pass's glow stack — `night.frag`'s `glow` term, unmodulated. +float glowCore(vec2 coord, float numCells, float size, float br) { + vec2 bigCoord = coord * numCells; + vec2 cellID = floor(bigCoord); + vec2 rnd = hash22(mod(cellID, numCells)); + vec2 center = cellID + mix(vec2(0.1), vec2(0.9), rnd * 0.7 + rnd.x * 0.3); + vec2 offset = (bigCoord - center) / (numCells * size); + float d = dot(offset, offset); + if (d > 0.16) return 0.0; + float sqrtd = 1.0 - sqrt(d); + + // Wide halo. + float glow = pow(clamp(br * smoothstep(0.6, 1.0, sqrtd), 0.0, 2.0), 3.5) * + 0.016666667; + + // Four-point diffraction cross: the spike is longest at ±45°/±135°. + float angle = atan(offset.y, offset.x) + PI * 0.25; + float angleOffset = abs(mod(angle, PI * 0.5) - PI * 0.25); + float len = mix(0.76, 0.88, sqrt(angleOffset) / (PI * 0.25)); + glow += smoothstep(len + 0.05, 1.1, sqrtd) * 0.5; + + // Tight core. + if (sqrtd > 0.895) { + float core = clamp(br * smoothstep(0.895, 1.0, sqrtd), 0.0, 2.0); + glow += core * core * 0.33333333; + } + return glow; +} + +void main() { + // The bake covers bright cells 0..3 of the same world space the display + // shader uses (texture [0,1] ↔ world [0,4]); the texture tiles via + // `fract(coord / 4.0)` there, which lands cell N back on its own hash. + vec2 coord = FlutterFragCoord().xy / iResolution * 4.0; + float r = starCore(coord, 4.0, 0.08, 2.0); + float g = glowCore(coord, 4.0, 0.08, 2.0); + float b = starCore(coord, 8.0, 0.05, 1.0); + float a = starCore(coord, 16.0, 0.025, 0.5); + fragColor = vec4(r, g, b, a); +} diff --git a/test/app/router/notification_routes_test.dart b/test/app/router/notification_routes_test.dart index 0e6f7442c..aad99e198 100644 --- a/test/app/router/notification_routes_test.dart +++ b/test/app/router/notification_routes_test.dart @@ -13,12 +13,14 @@ void main() { AppRoutes.earthquake, AppRoutes.eew, AppRoutes.more, + AppRoutes.meshtastic, }; const knownGroups = { 'group_eew', 'group_eq', 'group_info', 'group_tsunami', + 'group_mesh', 'group_other', }; diff --git a/test/core/astro/eclipse_test.dart b/test/core/astro/eclipse_test.dart new file mode 100644 index 000000000..611ffe5fc --- /dev/null +++ b/test/core/astro/eclipse_test.dart @@ -0,0 +1,151 @@ +/// Golden pins for eclipses, against NASA's five-millennium canon. +/// +/// Two very different things are being checked. The **lunar** side is a global +/// event, so the published greatest-eclipse instant and umbral magnitude are +/// directly comparable. The **solar** side is local, and the test that matters +/// most is not a magnitude at all — it is that an eclipse crossing Iceland is +/// *not* reported for Taiwan. The geometry alone will happily produce a small +/// formal separation for an observer on the night side of the Earth; only +/// requiring the Sun to be above the horizon turns that into an answer. +library; + +import 'package:dpip/core/astro/eclipse.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _taipei = (latitude: 25.0330, longitude: 121.5654); +const _reykjavik = (latitude: 64.1466, longitude: -21.9426); +const _luxor = (latitude: 25.6872, longitude: 32.6396); + +void main() { + group('lunar eclipses', () { + test('finds the right events, at the right times, 2025-2028', () { + // NASA's greatest-eclipse instants and umbral magnitudes. Magnitudes + // agree to about 0.015, which is the truncated ephemeris plus the + // convention for enlarging the shadow; the timings agree to a minute. + const published = <(String, EclipseKind, double)>[ + ('2025-03-14T06:58', EclipseKind.total, 1.178), + ('2025-09-07T18:11', EclipseKind.total, 1.362), + ('2026-03-03T11:33', EclipseKind.total, 1.151), + ('2026-08-28T04:12', EclipseKind.partial, 0.928), + ('2028-01-12T04:13', EclipseKind.partial, 0.066), + ]; + for (final (stamp, kind, magnitude) in published) { + final expected = DateTime.parse('${stamp}Z'); + final eclipse = Eclipses.lunarAt(expected); + expect(eclipse.kind, kind, reason: stamp); + expect( + eclipse.peak.difference(expected).inMinutes.abs(), + lessThan(2), + reason: '$stamp timing', + ); + expect( + eclipse.magnitude, + closeTo(magnitude, 0.02), + reason: '$stamp magnitude', + ); + } + }); + + test('a penumbral eclipse is reported as penumbral, not missed', () { + // 2027-02-20 is penumbral only: the Moon never touches the umbra. An + // implementation that only tests the umbra reports nothing at all. + final eclipse = Eclipses.lunarAt(DateTime.utc(2027, 2, 20, 23, 13)); + expect(eclipse.kind, EclipseKind.penumbral); + expect(eclipse.penumbralMagnitude, greaterThan(0.8)); + }); + + test('most full moons are not eclipses', () { + // The Moon's orbit is tilted 5°, so it usually misses the shadow + // entirely. A finder that returns something every month is broken. + var eclipses = 0; + var at = DateTime.utc(2026); + for (var i = 0; i < 12; i++) { + final found = Eclipses.nextLunar(at, withinDays: 40); + if (found != null) { + eclipses++; + at = found.peak.add(const Duration(days: 20)); + } else { + at = at.add(const Duration(days: 30)); + } + } + expect(eclipses, inInclusiveRange(1, 4)); + }); + + test('contacts bracket the peak', () { + final eclipse = Eclipses.lunarAt(DateTime.utc(2026, 3, 3, 11, 33)); + expect(eclipse.begins!.isBefore(eclipse.peak), isTrue); + expect(eclipse.ends!.isAfter(eclipse.peak), isTrue); + // A total lunar eclipse's umbral phase runs a few hours. + expect( + eclipse.ends!.difference(eclipse.begins!).inMinutes, + inInclusiveRange(120, 260), + ); + }); + }); + + group('solar eclipses', () { + test('2026-08-12 is total from Iceland', () { + final eclipse = Eclipses.solarAt( + DateTime.utc(2026, 8, 12, 17), + latitude: _reykjavik.latitude, + longitude: _reykjavik.longitude, + ); + expect(eclipse.kind, EclipseKind.total); + expect(eclipse.magnitude, greaterThan(0.99)); + }); + + test('2027-08-02 is total from Luxor', () { + final eclipse = Eclipses.solarAt( + DateTime.utc(2027, 8, 2, 10), + latitude: _luxor.latitude, + longitude: _luxor.longitude, + ); + expect(eclipse.kind, EclipseKind.total); + expect(eclipse.magnitude, greaterThan(1.02)); + }); + + test('the same eclipse is not visible from Taiwan', () { + // The single most important check here. Taiwan is on the night side at + // the time; the parallax-corrected geometry still yields a small formal + // separation, and without the horizon test this reports a partial + // eclipse that nobody on the island could see. + final eclipse = Eclipses.solarAt( + DateTime.utc(2026, 8, 12, 17), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(eclipse.isVisible, isFalse); + }); + + test('Taiwan\'s next visible solar eclipse is the 2030 one', () { + // 2030-06-01 is annular across Asia; Taiwan catches a solid partial. + // Nothing before it is visible from the island, which is a strong check + // on the horizon filter over a five-year search. + final eclipse = Eclipses.nextSolar( + DateTime.utc(2026), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + withinDays: 2000, + ); + expect(eclipse, isNotNull); + expect(eclipse!.peak.year, 2030); + expect(eclipse.peak.month, 6); + expect(eclipse.kind, EclipseKind.partial); + expect(eclipse.magnitude, inInclusiveRange(0.2, 0.6)); + }); + + test('contacts bracket the peak and last a couple of hours', () { + final eclipse = Eclipses.solarAt( + DateTime.utc(2027, 8, 2, 10), + latitude: _luxor.latitude, + longitude: _luxor.longitude, + ); + expect(eclipse.begins!.isBefore(eclipse.peak), isTrue); + expect(eclipse.ends!.isAfter(eclipse.peak), isTrue); + expect( + eclipse.ends!.difference(eclipse.begins!).inMinutes, + inInclusiveRange(90, 200), + ); + }); + }); +} diff --git a/test/core/astro/lunisolar_calendar_test.dart b/test/core/astro/lunisolar_calendar_test.dart new file mode 100644 index 000000000..ea453f781 --- /dev/null +++ b/test/core/astro/lunisolar_calendar_test.dart @@ -0,0 +1,214 @@ +/// Golden pins for 農曆, against the CWA's own published calendar. +/// +/// Two independent things are checked, because the calendar has two halves +/// that fail differently: +/// +/// * **Day-to-day mapping** — anchors spanning all twelve months of 2026, +/// taken from 中華民國115年日曆資料表, including every month boundary. A +/// new-moon rounding error moves a boundary by one day and nothing else +/// would notice. +/// * **The leap rule** — the CWA states which years carry a 閏月 and which +/// month it is, for 2015 through 2028. That is the part a packed lookup +/// table would get right by construction and a derived calendar has to +/// actually earn: 2017 閏6, 2020 閏4, 2023 閏2, 2025 閏6, 2028 閏5, and +/// nothing in the other years. +library; + +import 'package:dpip/core/astro/lunisolar_calendar.dart'; +import 'package:dpip/core/astro/solar_terms.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _zone = Duration(hours: 8); + +LunisolarDate _on(int year, int month, int day) => + LunisolarCalendar.of(DateTime.utc(year, month, day, 12).subtract(_zone)); + +void main() { + group('LunisolarCalendar', () { + test('matches the CWA 2026 calendar, month boundaries included', () { + // (Gregorian month, day, lunar month, lunar day). + const anchors = <(int, int, int, int)>[ + (1, 1, 11, 13), + (1, 17, 11, 29), + (1, 18, 11, 30), + (1, 19, 12, 1), + (2, 1, 12, 14), + (2, 16, 12, 29), + (2, 17, 1, 1), // 春節 + (3, 1, 1, 13), + (3, 18, 1, 30), + (3, 19, 2, 1), + (4, 1, 2, 14), + (4, 16, 2, 29), + (4, 17, 3, 1), + (5, 1, 3, 15), + (6, 1, 4, 16), + (6, 14, 4, 29), + (6, 15, 5, 1), + (7, 1, 5, 17), + (7, 13, 5, 29), + (7, 14, 6, 1), + (8, 1, 6, 19), + (8, 12, 6, 30), + (8, 13, 7, 1), + (9, 1, 7, 20), + (9, 10, 7, 29), + (9, 11, 8, 1), + (10, 1, 8, 21), + (10, 9, 8, 29), + (10, 10, 9, 1), + (11, 1, 9, 23), + (11, 8, 9, 30), + (11, 9, 10, 1), + (12, 1, 10, 23), + (12, 8, 10, 30), + (12, 9, 11, 1), + (12, 22, 11, 14), + (12, 31, 11, 23), + ]; + for (final (month, day, lunarMonth, lunarDay) in anchors) { + final lunar = _on(2026, month, day); + expect( + (lunar.month, lunar.day), + (lunarMonth, lunarDay), + reason: '2026-$month-$day', + ); + expect(lunar.isLeapMonth, isFalse, reason: '2026 has no leap month'); + } + }); + + test('春節 2026 is 2/17, and it is the only 正月初一 that year', () { + expect(_on(2026, 2, 17).isNewYearDay, isTrue); + var newYears = 0; + for (var day = 0; day < 365; day++) { + final date = LunisolarCalendar.of( + DateTime.utc(2026, 1, 1, 12).add(Duration(days: day)).subtract(_zone), + ); + if (date.isNewYearDay) newYears++; + } + expect(newYears, 1); + }); + + test('歲次 and the zodiac follow the sexagenary cycle', () { + // The CWA names 2026 農曆歲次丙午年 — the year of the horse. + final year = _on(2026, 6, 1); + expect(year.year, 2026); + expect(year.sexagenaryYear, '丙午'); + expect(year.zodiacIndex, 6); // 午, the horse + // 2025 is 乙巳 (snake), 2027 丁未 (goat). + expect(_on(2025, 6, 1).sexagenaryYear, '乙巳'); + expect(_on(2027, 6, 1).sexagenaryYear, '丁未'); + }); + + test('the leap month is where the CWA says it is, 2015-2028', () { + // Null means the CWA lists no 閏月 for that lunar year. + const published = { + 2015: null, + 2016: null, + 2017: 6, + 2018: null, + 2019: null, + 2020: 4, + 2021: null, + 2022: null, + 2023: 2, + 2024: null, + 2025: 6, + 2026: null, + 2027: null, + 2028: 5, + }; + for (final entry in published.entries) { + int? leap; + // Walk the lunar year from its 正月初一 forward; a leap month, if any, + // shows up as a repeated number flagged 閏. + for (var day = 0; day < 400; day++) { + final date = LunisolarCalendar.of( + DateTime.utc( + entry.key, + 1, + 20, + 12, + ).add(Duration(days: day)).subtract(_zone), + ); + if (date.year != entry.key) continue; + if (date.isLeapMonth) { + leap = date.month; + break; + } + } + expect(leap, entry.value, reason: '${entry.key} leap month'); + } + }); + + test('a leap year runs 383-385 days and a common year 353-355', () { + // The CWA gives 384 days for 2017/2020/2023/2025/2028 and 354-355 for + // the rest, which is the same statement as the leap rule seen from the + // other end. + int lengthOf(int lunarYear) { + var start = DateTime.utc(lunarYear, 1, 1, 12).subtract(_zone); + while (!LunisolarCalendar.of(start).isNewYearDay) { + start = start.add(const Duration(days: 1)); + } + var end = start.add(const Duration(days: 300)); + while (!LunisolarCalendar.of(end).isNewYearDay) { + end = end.add(const Duration(days: 1)); + } + return end.difference(start).inDays; + } + + expect(lengthOf(2026), inInclusiveRange(353, 355)); + expect(lengthOf(2025), inInclusiveRange(383, 385)); + expect(lengthOf(2017), inInclusiveRange(383, 385)); + }); + + test('冬至 always falls in month 11 — the rule the numbering rests on', () { + // Checked over twenty years, because this is the invariant that decides + // every other month number. If it ever slips, the whole year is wrong + // and nothing else in the calendar would flag it. + for (var year = 2015; year <= 2035; year++) { + final solstice = SolarTerms.next( + DateTime.utc(year, 11, 1), + SolarTerm.winterSolstice, + ); + final lunar = LunisolarCalendar.of(solstice); + expect(lunar.month, 11, reason: 'winter solstice $year'); + expect(lunar.isLeapMonth, isFalse); + } + }); + + test('months are 29 or 30 days, and days never leave that range', () { + var seen29 = false; + var seen30 = false; + for (var day = 0; day < 800; day++) { + final date = LunisolarCalendar.of( + DateTime.utc(2025, 1, 1, 12).add(Duration(days: day)).subtract(_zone), + ); + expect(date.monthLength, anyOf(29, 30)); + expect(date.day, inInclusiveRange(1, date.monthLength)); + expect(date.month, inInclusiveRange(1, 12)); + seen29 |= date.monthLength == 29; + seen30 |= date.monthLength == 30; + } + expect(seen29 && seen30, isTrue); + }); + + test('every day maps to exactly one lunar date, with no gaps', () { + // Walking a year, the lunar day must advance by one or reset to 1 — + // never jump. A boundary computed from the wrong new moon shows up as a + // repeat or a skip. + var previous = LunisolarCalendar.of( + DateTime.utc(2025, 6, 1, 12).subtract(_zone), + ); + for (var day = 1; day < 500; day++) { + final date = LunisolarCalendar.of( + DateTime.utc(2025, 6, 1, 12).add(Duration(days: day)).subtract(_zone), + ); + final continued = date.day == previous.day + 1; + final rolled = date.day == 1 && previous.day == previous.monthLength; + expect(continued || rolled, isTrue, reason: 'day $day: $date'); + previous = date; + } + }); + }); +} diff --git a/test/core/astro/moon_ephemeris_test.dart b/test/core/astro/moon_ephemeris_test.dart new file mode 100644 index 000000000..ce564dfbc --- /dev/null +++ b/test/core/astro/moon_ephemeris_test.dart @@ -0,0 +1,153 @@ +/// Golden pins for the lunar ephemeris. +/// +/// Two independent authorities, because "it looks about right" is how an +/// astronomy bug survives: +/// +/// * **Meeus's own worked example 45.a** pins the *transcription* of the +/// periodic-term table. One mistyped row moves the answer by arcminutes, +/// and nothing else in the app would notice. +/// * **JPL Horizons** pins the *result* — the DE ephemeris the truncated +/// series is an approximation of. The samples below were taken from +/// `ssd.jpl.nasa.gov/api/horizons.api` (geocentric, ecliptic of date) and +/// include the closest perigee and furthest apogee of 2024–2027, where a +/// distance error would be largest. +/// +/// The tolerances are the measured error over the full 1385-sample sweep, not +/// a number chosen to make the test pass: 37″ of longitude, 32″ of latitude, +/// 59 km of distance. Widening one means the series changed. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/moon_ephemeris.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const double _arcsecond = math.pi / 180 / 3600; + +/// `(utc, ecliptic longitude °, ecliptic latitude °, distance km)`. +const _horizons = <(String, double, double, double)>[ + ('2024-01-01T00:00', 155.9922, 3.5675, 404634), + ('2024-06-04T23:00', 54.3551, 3.2986, 371314), + ('2024-11-07T22:00', 299.4705, -4.7423, 383280), + ('2025-04-12T21:00', 201.6714, -2.1600, 405914), + ('2025-09-15T20:00', 101.2139, 4.8697, 376702), + ('2025-11-19T18:00', 231.8805, -4.5897, 406643), // furthest apogee in range + ('2026-02-18T19:00', 346.0357, 0.6534, 379828), + ('2026-07-24T18:00', 248.3680, -5.1564, 405125), + ('2026-12-24T13:00', 99.5921, 3.4321, 356681), // closest perigee in range + ('2026-12-27T17:00', 146.8894, -0.4926, 366736), +]; + +double _degrees(double radians) => radians * 180 / math.pi; + +void main() { + group('MoonEphemeris', () { + test('reproduces Meeus worked example 45.a', () { + // 1992 April 12, 0h TD. Meeus gets λ = 133.162659°, β = -3.229127°, + // Δ = 368409.7 km. The test instant is UTC, so ΔT (~59 s in 1992) is + // added back by the ephemeris itself — feeding it 0h UTC and expecting + // the 0h TD answer would be off by 0.008°, which the tolerance excludes. + final moon = MoonEphemeris.at( + DateTime.utc(1992, 4, 12).subtract(const Duration(seconds: 59)), + ); + expect(_degrees(moon.longitude), closeTo(133.162659, 0.001)); + expect(_degrees(moon.latitude), closeTo(-3.229127, 0.001)); + // Meeus evaluates all 60 rows; this keeps 35, which costs 12 km here. + // The angles are unaffected at this tolerance — it is the distance terms + // that are still contributing past row 35. + expect(moon.distanceKm, closeTo(368409.7, 20)); + // Meeus's π for the same instant: 0°.991990. + expect(_degrees(moon.parallax), closeTo(0.991990, 0.0001)); + }); + + test('tracks JPL Horizons to the measured tolerance', () { + for (final (stamp, longitude, latitude, distance) in _horizons) { + final moon = MoonEphemeris.at(DateTime.parse('${stamp}Z')); + expect( + _degrees(moon.longitude), + closeTo(longitude, 37 / 3600), + reason: 'longitude at $stamp', + ); + expect( + _degrees(moon.latitude), + closeTo(latitude, 32 / 3600), + reason: 'latitude at $stamp', + ); + expect( + moon.distanceKm, + closeTo(distance, 59), + reason: 'distance at $stamp', + ); + } + }); + + test('distance spans perigee to apogee, and nothing beyond', () { + // A term dropped from the wrong column shows up here before anywhere + // else: the swing is 14%, and a broken series either flattens it or + // overshoots the physical range. + var closest = double.infinity; + var furthest = 0.0; + for (var hours = 0; hours < 24 * 400; hours += 3) { + final d = MoonEphemeris.at( + DateTime.utc(2026).add(Duration(hours: hours)), + ).distanceKm; + closest = math.min(closest, d); + furthest = math.max(furthest, d); + } + expect(closest, inInclusiveRange(356400, 358000)); + expect(furthest, inInclusiveRange(405500, 406800)); + }); + + test('parallax and apparent size follow the distance', () { + final perigee = MoonEphemeris.at(DateTime.utc(2026, 12, 24, 13)); + final apogee = MoonEphemeris.at(DateTime.utc(2025, 11, 19, 18)); + expect(_degrees(perigee.parallax) * 60, closeTo(61.5, 0.5)); + expect(_degrees(apogee.parallax) * 60, closeTo(53.9, 0.5)); + // The famous "supermoon" difference: about 14% wider. + expect( + perigee.angularDiameter / apogee.angularDiameter, + closeTo(1.14, 0.01), + ); + }); + + test('the equatorial conversion is consistent with the ecliptic one', () { + // Round-trip: back-project RA/Dec through the obliquity and the ecliptic + // longitude has to reappear. Catches a swapped sine in the rotation, + // which a rise/set test would only show as a few minutes of drift. + final moon = MoonEphemeris.at(DateTime.utc(2026, 5, 3, 7)); + final equatorial = moon.equatorial; + final obliquity = + (23.439291 - 0.0130042 * moon.centuries) * math.pi / 180; + final longitude = math.atan2( + math.sin(equatorial.rightAscension) * math.cos(obliquity) + + math.tan(equatorial.declination) * math.sin(obliquity), + math.cos(equatorial.rightAscension), + ); + expect( + _degrees(turn(longitude) - moon.longitude).abs(), + lessThan(1 / 3600), + ); + }); + + test('is continuous across the longitude wrap', () { + // λ passes 360° once a month; a naive normalisation there would put a + // step in the phase, which the page renders as the Moon jumping. + var previous = MoonEphemeris.at(DateTime.utc(2026)).longitude; + for (var hours = 1; hours < 24 * 60; hours++) { + final current = MoonEphemeris.at( + DateTime.utc(2026).add(Duration(hours: hours)), + ).longitude; + final step = turn(current - previous); + expect(step, lessThan(1 * math.pi / 180), reason: 'step at $hours h'); + previous = current; + } + }); + + test('turn normalises onto [0, 2π)', () { + expect(turn(0), 0); + expect(turn(-_arcsecond), closeTo(2 * math.pi - _arcsecond, 1e-12)); + expect(turn(7 * math.pi), closeTo(math.pi, 1e-12)); + }); + }); +} diff --git a/test/core/astro/moon_orientation_test.dart b/test/core/astro/moon_orientation_test.dart new file mode 100644 index 000000000..62a722f3c --- /dev/null +++ b/test/core/astro/moon_orientation_test.dart @@ -0,0 +1,193 @@ +/// The Moon's tilt, checked against physics rather than against a convention. +/// +/// Orientation is where sign errors hide: a rendered globe rolled the wrong +/// way looks entirely plausible, and no other reading on the page disagrees +/// with it. So nothing here trusts a position-angle formula. Each test states +/// something that must be true of the sky itself — the crescent points at the +/// Sun; the pole leans away from the meridian on the side the object is on; +/// the tilt vanishes at the pole and is extreme at the equator — and checks +/// the computed angles against that. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/moon_orientation.dart'; +import 'package:dpip/core/astro/moon_rise_set.dart'; +import 'package:dpip/core/astro/sun_events.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _taipei = (latitude: 25.0330, longitude: 121.5654); + +double _deg(double radians) => radians * 180 / math.pi; + +void main() { + group('MoonOrientation', () { + test('the lit limb points at the Sun', () { + // The defining fact. Checked by walking a month at three-hour steps and + // comparing the limb bearing against the Sun's own bearing from the + // Moon, computed independently from the two horizontal positions. + var worst = 0.0; + for (var hours = 0; hours < 24 * 30; hours += 3) { + final at = DateTime.utc(2026, 6).add(Duration(hours: hours)); + final orientation = MoonOrientation.at( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final sun = SunEvents.lookFrom( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final moon = MoonRiseSet.lookFrom( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + worst = math.max( + worst, + signedTurn(orientation.brightLimbBearing - moon.bearingTo(sun)).abs(), + ); + } + expect(_deg(worst), lessThan(0.001)); + }); + + test('the pole is upright on the meridian and leans either side of it', () { + // At upper transit the celestial pole is directly above or below the + // object, so the roll is zero; east of the meridian it leans one way and + // west the other. A sign flip in the bearing shows up here as the lean + // going the wrong way, which no luminance test could see. + final transit = MoonRiseSet.of( + DateTime.utc(2026, 8, 15, -8), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ).transit!; + + double rollAt(Duration offset) => MoonOrientation.at( + transit.add(offset), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ).northBearing; + + expect(_deg(rollAt(Duration.zero)).abs(), lessThan(0.2)); + final before = rollAt(const Duration(hours: -3)); + final after = rollAt(const Duration(hours: 3)); + expect(before.sign, isNot(after.sign)); + expect(_deg(before).abs(), greaterThan(1)); + }); + + test('the tilt is a latitude effect — none at the pole, most at the ' + 'equator', () { + // The whole reason this exists. From the north pole the sky never + // rotates and a north-up render is right; on the equator the Moon rolls + // through a large angle between rising and setting. + double spread(double latitude) { + var low = math.pi; + var high = -math.pi; + for (var hours = 0; hours < 24; hours++) { + final roll = MoonOrientation.at( + DateTime.utc(2026, 3, 20).add(Duration(hours: hours)), + latitude: latitude, + longitude: 121.0, + ).northBearing; + low = math.min(low, roll); + high = math.max(high, roll); + } + return _deg(high - low); + } + + expect(spread(89.5), lessThan(2), reason: 'near the pole, no roll'); + expect(spread(25.0), greaterThan(40), reason: 'Taiwan rolls visibly'); + expect(spread(0.0), greaterThan(80), reason: 'the equator rolls most'); + }); + + test('the terminator is not perpendicular to the polar axis', () { + // The reason two angles are carried instead of one. If the lit limb were + // simply 90° from the pole, a single roll would do — but the Sun is on + // the ecliptic and the Moon's axis follows the equator, so the two + // disagree by tens of degrees over a month. + var worst = 0.0; + for (var hours = 0; hours < 24 * 30; hours += 6) { + final tilt = MoonOrientation.at( + DateTime.utc(2026).add(Duration(hours: hours)), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ).limbTiltFromPole; + // Distance from a right angle, either way round. + worst = math.max(worst, (tilt.abs() - math.pi / 2).abs()); + } + expect(_deg(worst), greaterThan(10)); + }); + + test('carries the Moon\'s own position, so a caller needs one call', () { + final at = DateTime.utc(2026, 8, 15, 12); + final orientation = MoonOrientation.at( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final direct = MoonRiseSet.lookFrom( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(orientation.horizontal.altitude, direct.altitude); + expect(orientation.horizontal.azimuth, direct.azimuth); + }); + }); + + group('Observer', () { + test('azimuth is measured from north, eastward', () { + // A transiting body is due south from Taiwan and due north from Sydney. + // Getting this backwards would send every pointing instruction 180° out. + final transit = MoonRiseSet.of( + DateTime.utc(2026, 8, 15, -8), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ).transit!; + final north = MoonRiseSet.lookFrom( + transit, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(_deg(north.azimuth), closeTo(180, 5)); + + final southern = MoonRiseSet.of( + DateTime.utc(2026, 8, 15, -10), + latitude: -33.8688, + longitude: 151.2093, + ).transit!; + final south = MoonRiseSet.lookFrom( + southern, + latitude: -33.8688, + longitude: 151.2093, + ); + expect( + math.min(_deg(south.azimuth), 360 - _deg(south.azimuth)), + lessThan(5), + ); + }); + + test('the Sun is in the east at sunrise and the west at sunset', () { + final events = SunEvents.of( + DateTime.utc(2026, 3, 20, -8), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final rise = SunEvents.lookFrom( + events.rise!, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final set = SunEvents.lookFrom( + events.set!, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + // Equinox, so both are within a degree or so of due east and due west. + expect(_deg(rise.azimuth), closeTo(90, 2)); + expect(_deg(set.azimuth), closeTo(270, 2)); + }); + }); +} diff --git a/test/core/astro/moon_phase_test.dart b/test/core/astro/moon_phase_test.dart new file mode 100644 index 000000000..2ea48f27d --- /dev/null +++ b/test/core/astro/moon_phase_test.dart @@ -0,0 +1,163 @@ +/// Golden pins for the lunar phase, distance and libration readouts. +/// +/// The anchors are historical facts, not self-generated: the 2024 +/// North-America total eclipse new moon (2024-04-08 18:21 UTC), that month's +/// full moon (2024-04-23 23:49 UTC), and Meeus's worked example 51.a for the +/// libration. Since the phase became a difference of two ecliptic longitudes +/// rather than a truncated series of its own, these hold to about a minute of +/// arc — the old tolerances were 0.12 rad (7°) and are now 0.01 rad. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/moon_phase.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// A total solar eclipse is a new moon observed to the minute. +final _newMoon = DateTime.utc(2024, 4, 8, 18, 21); +final _fullMoon = DateTime.utc(2024, 4, 23, 23, 49); + +void main() { + group('MoonPhase', () { + test('new moon at the 2024 eclipse', () { + final angle = MoonPhase.at(_newMoon).angle; + expect(math.min(angle, 2 * math.pi - angle), lessThan(0.01)); + }); + + test('full moon 15.2 days later', () { + expect((MoonPhase.at(_fullMoon).angle - math.pi).abs(), lessThan(0.01)); + }); + + test('brightness is 0 at new and 1 at full', () { + expect(MoonPhase.at(_newMoon).brightness, lessThan(0.001)); + expect(MoonPhase.at(_fullMoon).brightness, greaterThan(0.999)); + }); + + test('first quarter lights the right half (waxing) at ~50%', () { + final q1 = MoonPhase.at(DateTime.utc(2024, 4, 15, 19, 13)); + expect(q1.waxing, isTrue); + expect(q1.brightness, closeTo(0.5, 0.01)); + expect(q1.name, MoonPhaseName.firstQuarter); + }); + + test('age in days runs 0..synodic month and resets at new moon', () { + expect( + MoonPhase.at(_fullMoon).ageInDays, + closeTo(synodicMonthDays / 2, 0.1), + ); + // At new moon the age is *both* ends of the range — the instant is the + // wrap — so either answer is right and only the distance to it matters. + final atNew = MoonPhase.at(_newMoon).ageInDays; + expect(math.min(atNew, synodicMonthDays - atNew), lessThan(0.1)); + }); + + test('distance is the ephemeris distance, in the physical range', () { + final phase = MoonPhase.at(_fullMoon); + expect(phase.distanceKm, inInclusiveRange(356400, 406800)); + expect(phase.apparentDiameterDegrees, inInclusiveRange(0.49, 0.56)); + }); + + group('phase search', () { + test('finds the April 2024 full moon from the eclipse new moon', () { + final next = MoonPhase.nextFullMoon(_newMoon); + expect(next.difference(_fullMoon).inMinutes.abs(), lessThan(30)); + }); + + test('finds the next new moon from mid-lunation', () { + // 2024-05-08 03:22 UTC, seen from the full moon before it. + final next = MoonPhase.nextNewMoon(_fullMoon); + expect( + next.difference(DateTime.utc(2024, 5, 8, 3, 22)).inMinutes.abs(), + lessThan(30), + ); + }); + + test('lands on the target angle, not merely near it', () { + // The search is an iteration, not a bracket: what has to hold is that + // it converged, and it must hold for every lunation, not a lucky one. + var at = DateTime.utc(2024); + for (var lunation = 0; lunation < 40; lunation++) { + final full = MoonPhase.nextFullMoon(at); + expect( + (MoonPhase.angleAt(full) - math.pi).abs(), + lessThan(1e-4), + reason: 'lunation $lunation', + ); + at = full; + } + }); + + test('is always strictly after, including from a full moon', () { + // Coasting from a zero remainder lands back where it started, so the + // search has to notice and step on — otherwise a caller walking the + // phases stops dead. + for (final from in [ + _newMoon, + _fullMoon, + MoonPhase.nextFullMoon(_newMoon), + ]) { + expect(MoonPhase.nextFullMoon(from).isAfter(from), isTrue); + expect(MoonPhase.nextNewMoon(from).isAfter(from), isTrue); + } + }); + + test('successive full moons are one synodic month apart', () { + // Walking the calendar is what the page does, and it is where a search + // that quietly returns its own input would show up as a stall. + var previous = MoonPhase.nextFullMoon(DateTime.utc(2024)); + for (var lunation = 0; lunation < 24; lunation++) { + final next = MoonPhase.nextFullMoon( + previous.add(const Duration(days: 1)), + ); + // Real lunations vary about ±0.5 d around the mean. + expect( + next.difference(previous).inMinutes / (60 * 24), + closeTo(synodicMonthDays, 0.7), + reason: 'lunation $lunation', + ); + previous = next; + } + }); + }); + + group('libration', () { + test('reproduces Meeus worked example 51.a', () { + // 1992 April 12, 0h TD: l' = -1.206°, b' = +4.194°. + final libration = MoonPhase.librationAt( + DateTime.utc(1992, 4, 12).subtract(const Duration(seconds: 59)), + ); + expect(libration.longitude * 180 / math.pi, closeTo(-1.206, 0.01)); + expect(libration.latitude * 180 / math.pi, closeTo(4.194, 0.01)); + }); + + test('stays inside the physical rocking range', () { + // Optical libration is bounded by the orbit: ±7.9° in longitude, + // ±6.9° in latitude. Outside that the globe would visibly swing. + var maxLongitude = 0.0; + var maxLatitude = 0.0; + for (var hours = 0; hours < 24 * 400; hours += 6) { + final l = MoonPhase.librationAt( + DateTime.utc(2026).add(Duration(hours: hours)), + ); + maxLongitude = math.max(maxLongitude, l.longitude.abs()); + maxLatitude = math.max(maxLatitude, l.latitude.abs()); + } + expect(maxLongitude * 180 / math.pi, inInclusiveRange(5, 8)); + expect(maxLatitude * 180 / math.pi, inInclusiveRange(5, 7)); + }); + + test('does not jump — the wrap is signed, not modular', () { + // l' is A - F, both of which wrap; taken modulo 2π the result would + // flip between -7° and +353° and snap the rendered globe around. + var previous = MoonPhase.librationAt(DateTime.utc(2026)).longitude; + for (var hours = 1; hours < 24 * 90; hours += 3) { + final current = MoonPhase.librationAt( + DateTime.utc(2026).add(Duration(hours: hours)), + ).longitude; + expect((current - previous).abs(), lessThan(0.05)); + previous = current; + } + }); + }); + }); +} diff --git a/test/core/astro/moon_rise_set_test.dart b/test/core/astro/moon_rise_set_test.dart new file mode 100644 index 000000000..a7517e5a3 --- /dev/null +++ b/test/core/astro/moon_rise_set_test.dart @@ -0,0 +1,183 @@ +/// Golden pins for moonrise and moonset. +/// +/// Two authorities, deliberately not ones that would agree with a mistake: +/// +/// * The **US Naval Observatory** (`aa.usno.navy.mil/api/rstt/oneday`) for +/// exact coordinates — Taipei, Sydney to catch a hemisphere sign, and +/// Reykjavík at 64°N where the Moon skims the horizon and a weak search +/// fails first. +/// * The **CWA's published 2026 timetable** for Keelung, because that is the +/// table a user in Taiwan would check this page against. +/// +/// Both publish to the minute, so a one-minute disagreement is their rounding, +/// not an error here. Over the full 2026 sweep (312 events, four sites) the +/// mean gap was 15 s — which *is* the mean gap minute-rounding alone produces +/// — the worst was 33 s, and no rise or set was either missed or invented. +library; + +import 'package:dpip/core/astro/moon_rise_set.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Taiwan runs on UTC+8 all year — no daylight saving to model. +const _taiwan = Duration(hours: 8); +const _iceland = Duration.zero; +const _sydney = Duration(hours: 10); + +const _taipei = (latitude: 25.0330, longitude: 121.5654); +const _keelung = (latitude: 25.1276, longitude: 121.7392); +const _reykjavik = (latitude: 64.1466, longitude: -21.9426); +const _sydneyCity = (latitude: -33.8688, longitude: 151.2093); + +/// The events of the local day beginning at midnight on [year]-[month]-[day]. +MoonRiseSet _localDay( + int year, + int month, + int day, + Duration offset, + ({double latitude, double longitude}) place, +) => MoonRiseSet.of( + DateTime.utc(year, month, day).subtract(offset), + latitude: place.latitude, + longitude: place.longitude, +); + +/// `HH:mm` in the given zone, or `--:--`. Rounded to the minute, as both +/// reference tables are. +String _clock(DateTime? utc, Duration offset) { + if (utc == null) return '--:--'; + final local = utc.add(offset); + final rounded = local.second >= 30 + ? local.add(const Duration(minutes: 1)) + : local; + return '${rounded.hour.toString().padLeft(2, '0')}:' + '${rounded.minute.toString().padLeft(2, '0')}'; +} + +void main() { + group('MoonRiseSet', () { + test('matches the CWA 2026 timetable for Keelung', () { + // 基隆市, from 中華民國115年月出月沒時刻表. + const rows = <(int, int, String, String)>[ + (1, 1, '14:49', '04:08'), + (1, 15, '03:42', '14:10'), + (2, 1, '16:58', '06:02'), + (3, 1, '15:47', '04:41'), + (4, 1, '17:30', '05:01'), + (5, 1, '18:07', '04:36'), + (6, 1, '19:41', '05:16'), + (6, 15, '04:39', '19:12'), + ]; + for (final (month, day, rise, set) in rows) { + final events = _localDay(2026, month, day, _taiwan, _keelung); + expect(_clock(events.rise, _taiwan), rise, reason: '$month/$day rise'); + expect(_clock(events.set, _taiwan), set, reason: '$month/$day set'); + } + }); + + test('matches the USNO for Taipei', () { + const rows = <(int, int, String, String)>[ + (1, 1, '14:50', '04:09'), + (8, 15, '07:45', '20:05'), + (11, 9, '05:54', '16:51'), + ]; + for (final (month, day, rise, set) in rows) { + final events = _localDay(2026, month, day, _taiwan, _taipei); + expect(_clock(events.rise, _taiwan), rise, reason: '$month/$day rise'); + expect(_clock(events.set, _taiwan), set, reason: '$month/$day set'); + } + }); + + test('matches the USNO in the southern hemisphere', () { + final events = _localDay(2026, 1, 1, _sydney, _sydneyCity); + expect(_clock(events.rise, _sydney), '17:08'); + expect(_clock(events.set, _sydney), '01:52'); + }); + + test('matches the USNO at 64°N, where the Moon skims', () { + // Reykjavík. The Moon's daily path is shallow here, so it crosses the + // horizon at a glancing angle — a coarse scan either misses the crossing + // or lands minutes from it. + const rows = <(int, int, String, String)>[ + (1, 11, '03:38', '11:33'), + (7, 20, '14:05', '22:59'), + (12, 27, '21:36', '12:44'), + ]; + for (final (month, day, rise, set) in rows) { + final events = _localDay(2026, month, day, _iceland, _reykjavik); + expect(_clock(events.rise, _iceland), rise, reason: '$month/$day rise'); + expect(_clock(events.set, _iceland), set, reason: '$month/$day set'); + } + }); + + group('days that are missing an event', () { + test('no moonrise: the Moon comes up ~50 min later each day', () { + // Roughly once a month a calendar day gets skipped entirely. `null` is + // the correct answer; a time from the neighbouring day would be worse + // than none, because it would look right. + final events = _localDay(2026, 3, 10, _taiwan, _taipei); + expect(events.rise, isNull); + expect(_clock(events.set, _taiwan), '09:57'); + }); + + test('no moonset: the mirror case, later the same month', () { + final events = _localDay(2026, 4, 23, _taiwan, _taipei); + expect(_clock(events.rise, _taiwan), '10:27'); + expect(events.set, isNull); + }); + + test('neither, at 64°N — and which one it is stays answerable', () { + // The USNO reports no rise and no set at Reykjavík this day. That is + // ambiguous on its own (up all day, or down all day?), so the answer + // is only useful alongside where the Moon actually is. + final events = _localDay(2026, 1, 1, _iceland, _reykjavik); + expect(events.isCircumpolar, isTrue); + expect( + MoonRiseSet.aboveHorizon( + DateTime.utc(2026, 1, 1, 12), + latitude: _reykjavik.latitude, + longitude: _reykjavik.longitude, + ), + isTrue, + reason: 'above all day, not below', + ); + }); + + test('a rise with no set, at 64°N', () { + final events = _localDay(2026, 11, 17, _iceland, _reykjavik); + expect(_clock(events.rise, _iceland), '15:26'); + expect(events.set, isNull); + }); + }); + + test('every event falls inside the requested window', () { + final start = DateTime.utc(2026, 3, 10); + final events = MoonRiseSet.of( + start, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final end = start.add(const Duration(hours: 24)); + for (final event in [events.rise, events.set].nonNulls) { + expect(event.isBefore(start), isFalse); + expect(event.isAfter(end), isFalse); + } + }); + + test('the Moon is up between its rise and its set', () { + // Ties the two answers to the altitude they came from: a crossing time + // on the wrong side of the horizon fails here even if it looks plausible + // on a clock. + final events = _localDay(2026, 8, 15, _taiwan, _taipei); + bool up(DateTime at) => MoonRiseSet.aboveHorizon( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + const minute = Duration(minutes: 5); + expect(up(events.rise!.add(minute)), isTrue); + expect(up(events.rise!.subtract(minute)), isFalse); + expect(up(events.set!.subtract(minute)), isTrue); + expect(up(events.set!.add(minute)), isFalse); + }); + }); +} diff --git a/test/core/astro/planet_ephemeris_test.dart b/test/core/astro/planet_ephemeris_test.dart new file mode 100644 index 000000000..7714c5d1f --- /dev/null +++ b/test/core/astro/planet_ephemeris_test.dart @@ -0,0 +1,257 @@ +/// Golden pins for the planets, against JPL Horizons. +/// +/// The elements come from JPL and the check comes from JPL's own ephemeris — +/// different enough to be a real test, because the elements are a fitted +/// approximation and Horizons is the DE integration they approximate. A +/// mistyped digit in the table moves a planet by degrees and shows up here +/// immediately. +/// +/// Tolerances are the measured maxima over 711 samples spanning 2024–2027: +/// longitude within 285″ (Saturn, the worst), latitude within 17″, magnitude +/// within 0.3 where the planet is far enough from the Sun to be observed. +/// Distance is checked as a fraction, since Neptune's astronomical unit and +/// Mercury's are not comparable quantities. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/planet_ephemeris.dart'; +import 'package:dpip/core/astro/sky_position.dart'; +import 'package:flutter_test/flutter_test.dart'; + +double _deg(double radians) => radians * 180 / math.pi; +double _wrap180(double d) => ((d + 180) % 360 + 360) % 360 - 180; + +/// `(planet, utc, ecliptic longitude °, ecliptic latitude °, distance au, +/// apparent magnitude)` — from `ssd.jpl.nasa.gov/api/horizons.api`, +/// geocentric, ecliptic of date. +const _horizons = <(Planet, String, double, double, double, double)>[ + ( + Planet.mercury, + '2024-01-01T00:00', + 262.2816922, + 3.0649825, + 0.7775454, + 0.508, + ), + ( + Planet.mercury, + '2026-04-14T01:00', + 358.4740090, + -2.5573511, + 1.0308555, + 0.074, + ), + (Planet.venus, '2024-01-01T00:00', 242.6122975, 1.9498256, 1.1819073, -4.039), + (Planet.venus, '2026-04-14T01:00', 47.6461538, 0.1273597, 1.5178550, -3.902), + (Planet.mars, '2024-01-01T00:00', 267.3083422, -0.5504977, 2.4238068, 1.418), + (Planet.mars, '2026-04-14T01:00', 3.2860846, -0.9879993, 2.2742771, 1.251), + ( + Planet.jupiter, + '2024-01-01T00:00', + 35.5823812, + -1.1854765, + 4.4815037, + -2.589, + ), + ( + Planet.jupiter, + '2026-04-14T01:00', + 106.8665112, + 0.3798162, + 5.2801833, + -2.112, + ), + ( + Planet.saturn, + '2024-01-01T00:00', + 333.2435330, + -1.6341214, + 10.2947007, + 0.955, + ), + (Planet.saturn, '2026-04-14T01:00', 7.1538969, -2.1418000, 10.4408377, 0.933), + ( + Planet.uranus, + '2024-01-01T00:00', + 49.3839119, + -0.3061202, + 18.9754148, + 5.691, + ), + ( + Planet.uranus, + '2026-04-14T01:00', + 59.3720097, + -0.1677465, + 20.2820211, + 5.813, + ), + ( + Planet.neptune, + '2024-01-01T00:00', + 355.0761521, + -1.2372427, + 30.1425627, + 7.775, + ), + ( + Planet.neptune, + '2026-04-14T01:00', + 2.6841033, + -1.3115590, + 30.8133183, + 7.821, + ), +]; + +void main() { + group('PlanetEphemeris', () { + test('tracks JPL Horizons to the measured tolerance', () { + for (final (planet, stamp, longitude, latitude, distance, magnitude) + in _horizons) { + final body = PlanetEphemeris.at(planet, DateTime.parse('${stamp}Z')); + expect( + _wrap180(_deg(body.longitude) - longitude).abs(), + lessThan(285 / 3600), + reason: '${planet.name} longitude at $stamp', + ); + expect( + (_deg(body.latitude) - latitude).abs(), + lessThan(17 / 3600), + reason: '${planet.name} latitude at $stamp', + ); + expect( + (body.distanceAu - distance).abs() / distance, + lessThan(0.0011), + reason: '${planet.name} distance at $stamp', + ); + expect( + (body.magnitude - magnitude).abs(), + lessThan(0.3), + reason: '${planet.name} magnitude at $stamp', + ); + } + }); + + test( + 'light time is applied — the planet is where it was, not where it is', + () { + // Dropping the correction shifts Saturn by roughly its own motion over + // the 80 minutes its light takes to arrive. The check is that the + // position differs from an uncorrected one by about that much, in the + // direction of the planet's travel. + final at = DateTime.utc(2026, 4, 14, 1); + final saturn = PlanetEphemeris.at(Planet.saturn, at); + final lightTimeMinutes = saturn.distanceAu * 8.317; + expect(lightTimeMinutes, closeTo(87, 5)); + + final earlier = PlanetEphemeris.at( + Planet.saturn, + at.subtract(Duration(minutes: lightTimeMinutes.round())), + ); + // The corrected position at `at` should sit within a few arcseconds of + // the *uncorrected* position one light-time earlier. + expect( + _wrap180(_deg(saturn.longitude - earlier.longitude)).abs() * 3600, + lessThan(60), + ); + }, + ); + + test('the inner planets show phases and the outer ones do not', () { + // A geometric fact, and the cheapest check that the Sun–planet–Earth + // triangle is being solved the right way round: Venus can be a crescent, + // Jupiter never is. + var venusMin = 1.0; + var jupiterMin = 1.0; + for (var days = 0; days < 600; days += 3) { + final at = DateTime.utc(2026).add(Duration(days: days)); + venusMin = math.min( + venusMin, + PlanetEphemeris.at(Planet.venus, at).illuminated, + ); + jupiterMin = math.min( + jupiterMin, + PlanetEphemeris.at(Planet.jupiter, at).illuminated, + ); + } + expect(venusMin, lessThan(0.05), reason: 'Venus becomes a thin crescent'); + expect(jupiterMin, greaterThan(0.98), reason: 'Jupiter stays full'); + }); + + test( + 'elongation is bounded for the inner planets and free for the outer', + () { + // Mercury and Venus can never be opposite the Sun — they orbit inside + // us. Their maximum elongations (about 28° and 47°) are a strong check + // on the orbits themselves. + var mercuryMax = 0.0; + var venusMax = 0.0; + var marsMax = 0.0; + for (var days = 0; days < 800; days += 2) { + final at = DateTime.utc(2026).add(Duration(days: days)); + mercuryMax = math.max( + mercuryMax, + _deg(PlanetEphemeris.at(Planet.mercury, at).elongation), + ); + venusMax = math.max( + venusMax, + _deg(PlanetEphemeris.at(Planet.venus, at).elongation), + ); + marsMax = math.max( + marsMax, + _deg(PlanetEphemeris.at(Planet.mars, at).elongation), + ); + } + expect(mercuryMax, inInclusiveRange(26, 29)); + expect(venusMax, inInclusiveRange(45, 48)); + expect(marsMax, greaterThan(175), reason: 'Mars reaches opposition'); + }, + ); + + test('the signed elongation says evening or morning', () { + // Which side of the Sun a planet is on decides whether you look after + // dusk or before dawn, and the unsigned elongation cannot tell you. + // At greatest *eastern* elongation Venus is the evening star. + var best = 0.0; + var bestAt = DateTime.utc(2026); + for (var days = 0; days < 400; days++) { + final at = DateTime.utc(2026).add(Duration(days: days)); + final venus = PlanetEphemeris.at(Planet.venus, at); + if (venus.signedElongation > best) { + best = venus.signedElongation; + bestAt = at; + } + } + expect(_deg(best), inInclusiveRange(44, 48)); + expect(PlanetEphemeris.at(Planet.venus, bestAt).isEvening, isTrue); + }); + + test('Saturn dims when its rings close', () { + // The rings swing Saturn by more than a magnitude. 2025 was a ring-plane + // crossing, so Saturn is near its faintest then and brightens after. + final closed = PlanetEphemeris.at( + Planet.saturn, + DateTime.utc(2025, 3, 23), + ); + final open = PlanetEphemeris.at(Planet.saturn, DateTime.utc(2032, 6)); + expect(open.magnitude, lessThan(closed.magnitude - 0.4)); + }); + + test('provides a track the shared rise/set solver can use', () { + final track = PlanetEphemeris.trackOf(Planet.jupiter); + final events = RiseSet.solve( + from: DateTime.utc(2026, 8, 15, -8), + observer: const Observer(latitude: 25.033, longitude: 121.5654), + track: track, + horizon: (_) => pointHorizon, + ); + // Jupiter rises and sets from Taiwan like everything else on the + // ecliptic; what matters is that all three events are found and ordered. + expect(events.rise, isNotNull); + expect(events.set, isNotNull); + expect(events.transit, isNotNull); + }); + }); +} diff --git a/test/core/astro/satellite_test.dart b/test/core/astro/satellite_test.dart new file mode 100644 index 000000000..4fc422bd8 --- /dev/null +++ b/test/core/astro/satellite_test.dart @@ -0,0 +1,235 @@ +/// Golden pins for SGP4. +/// +/// The anchor is the near-Earth verification case from **Spacetrack Report +/// #3** — satellite 88888, the element set the model was published with. It is +/// the right test because SGP4 is *defined* by its output: a TLE is a set of +/// mean elements fitted to this propagator, so "close enough" is not a thing. +/// Three separate errors in the implementation (the sign of A(3,0), a factor +/// of two in C2, a flipped 3θ²−1 in C4) each left the answer looking plausible +/// and moved it by tens of kilometres; this test is what found them. +/// +/// The two published epochs pinned here are matched to a few **metres** — on +/// a 6,650 km radius, which is 4 parts in 10^9. The +/// rest of the file checks physics rather than a table: the analytic velocity +/// against the numerical derivative of the analytic position, and the orbit +/// against the mean motion the elements declare. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/satellite.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Spacetrack Report #3's near-Earth test object. +final _testCase = TleSet.parse( + 'TEST', + '1 88888U 80275.98708465 .00073094 13844-3 66816-4 0 8', + '2 88888 72.8435 115.9689 0086731 52.6988 110.5714 16.05824518 105', +); + +/// A real, recent element set — the shape the app actually parses. +final _iss = TleSet.parse( + 'ISS (ZARYA)', + '1 25544U 98067A 26226.43871707 .00004555 00000+0 89427-4 0 9997', + '2 25544 51.6329 11.7957 0007493 45.9133 314.2471 15.49439755580788', +); + +double _length((double, double, double) v) => + math.sqrt(v.$1 * v.$1 + v.$2 * v.$2 + v.$3 * v.$3); + +void main() { + group('TleSet', () { + test('parses by column, not by splitting', () { + expect(_iss.catalogNumber, 25544); + expect(_iss.name, 'ISS (ZARYA)'); + expect(_iss.epoch.year, 2026); + // Day 226.43871707 of 2026. + expect(_iss.epoch.month, 8); + expect(_iss.epoch.day, 14); + expect(_iss.eccentricity, closeTo(0.0007493, 1e-9)); + expect(_iss.inclination / degrees, closeTo(51.6329, 1e-6)); + // 15.49 revolutions a day, in radians per minute. + expect( + _iss.meanMotion * 1440 / (2 * math.pi), + closeTo(15.49439755, 1e-6), + ); + }); + + test('decodes the implied-exponent drag field', () { + // "89427-4" is 0.89427e-4 — a format that no general number parser + // handles and that a whitespace split would mangle. + expect(_iss.bstar, closeTo(0.89427e-4, 1e-12)); + expect(_testCase.bstar, closeTo(0.66816e-4, 1e-12)); + }); + + test('reads a whole file and skips anything malformed', () { + const file = ''' +ISS (ZARYA) +1 25544U 98067A 26226.43871707 .00004555 00000+0 89427-4 0 9997 +2 25544 51.6329 11.7957 0007493 45.9133 314.2471 15.49439755580788 +CSS (TIANHE) +1 48274U 21035A 26224.98627525 .00000101 00000+0 54127-5 0 9991 +2 48274 41.4709 337.2096 0001079 250.4973 109.5748 15.58975796302033 +'''; + final sets = TleSet.parseAll(file); + expect(sets, hasLength(2)); + expect(sets.map((s) => s.catalogNumber), [25544, 48274]); + }); + + test('reports its own age, because that is what decays', () { + final age = _iss.ageAt(DateTime.utc(2026, 8, 20)); + expect(age.inDays, 5); + }); + }); + + group('Sgp4', () { + test('reproduces the Spacetrack Report #3 vectors', () { + // Position, kilometres, in the TEME frame. Ten metres is the point: + // every one of the three bugs moved this by tens of kilometres, and a + // looser tolerance would have let them through. + const published = <(double, List)>[ + (0, [2328.97048951, -5995.22076416, 1719.97067261]), + (360, [2456.10705566, -6071.93853760, 1222.89727783]), + ]; + for (final (minutes, want) in published) { + final position = _testCase.propagatedBy(minutes); + expect( + _length(( + position.$1 - want[0], + position.$2 - want[1], + position.$3 - want[2], + )), + lessThan(0.01), + reason: 't = $minutes min', + ); + } + }); + + test('the analytic velocity agrees with the position it belongs to', () { + // SGP4 gives position and velocity from separate expressions. They have + // to describe the same motion, and a differentiated position is the only + // check on that which does not need a second implementation. + final sgp4 = Sgp4(_testCase); + for (final minutes in [0.0, 120.0, 720.0]) { + const h = 1 / 600; // 0.1 s + final before = sgp4.propagate(minutes - h).position; + final after = sgp4.propagate(minutes + h).position; + final numeric = ( + (after.$1 - before.$1) / (2 * h * 60), + (after.$2 - before.$2) / (2 * h * 60), + (after.$3 - before.$3) / (2 * h * 60), + ); + final analytic = sgp4.propagate(minutes).velocity; + final difference = _length(( + analytic.$1 - numeric.$1, + analytic.$2 - numeric.$2, + analytic.$3 - numeric.$3, + )); + // Under 0.2%. They are not identical by design — the analytic velocity + // drops the time-derivative of the short-period terms — but a real + // error shows up as whole km/s, as it did. + expect(difference / _length(analytic), lessThan(0.002)); + } + }); + + test('keeps the ISS at the altitude and speed it actually has', () { + final sgp4 = Sgp4(_iss); + var lowest = double.infinity; + var highest = 0.0; + var slowest = double.infinity; + for (var minutes = 0; minutes < 1440; minutes += 5) { + final state = sgp4.propagate(minutes.toDouble()); + final altitude = _length(state.position) - 6378.135; + lowest = math.min(lowest, altitude); + highest = math.max(highest, altitude); + slowest = math.min(slowest, _length(state.velocity)); + } + // A near-circular orbit a little above 400 km, at 7.6-7.7 km/s. + expect(lowest, inInclusiveRange(370, 430)); + expect(highest, inInclusiveRange(370, 440)); + // e = 0.00075 gives a radial swing of about 10 km, and the Earth's + // oblateness adds as much again; anything far outside that is not this + // orbit. + expect(highest - lowest, lessThan(30), reason: 'near-circular'); + expect(slowest, inInclusiveRange(7.5, 7.8)); + }); + + test('completes exactly as many orbits as the mean motion says', () { + // Over a day the satellite must come back round the number of times the + // element set declares. This catches an error in the recovered mean + // motion, which a single-epoch position check cannot see. + final sgp4 = Sgp4(_iss); + var crossings = 0; + var previous = sgp4.propagate(0).position.$3; + for (var minutes = 1; minutes <= 1440; minutes++) { + final z = sgp4.propagate(minutes.toDouble()).position.$3; + if (previous < 0 && z >= 0) crossings++; + previous = z; + } + expect(crossings, closeTo(15.494, 1)); + }); + + test('look angles put the satellite somewhere on the sky', () { + final sgp4 = Sgp4(_iss); + var sawAbove = false; + for (var minutes = 0; minutes < 1440; minutes += 2) { + final look = sgp4.lookFrom( + _iss.epoch.add(Duration(minutes: minutes)), + latitude: 25.033, + longitude: 121.5654, + ); + expect(look.altitude, inInclusiveRange(-math.pi / 2, math.pi / 2)); + expect(look.azimuth, inInclusiveRange(0, 2 * math.pi)); + if (look.altitude > 0) sawAbove = true; + } + // A 51.6° orbit passes over Taiwan several times a day. + expect(sawAbove, isTrue); + }); + + test('finds passes, and they are ordered and plausible', () { + final sgp4 = Sgp4(_iss); + final passes = SatellitePasses.find( + sgp4, + from: _iss.epoch, + latitude: 25.033, + longitude: 121.5654, + window: const Duration(days: 3), + sunlitOnly: false, + ); + expect(passes, isNotEmpty); + for (final pass in passes) { + expect(pass.rises.isBefore(pass.peaks), isTrue); + expect(pass.peaks.isBefore(pass.sets), isTrue); + // A low-Earth pass is minutes, never hours. + expect(pass.length.inMinutes, inInclusiveRange(1, 15)); + expect(pass.peakAltitude, greaterThanOrEqualTo(10 * degrees)); + } + }); + + test('a sunlit-only search is a subset of the geometric one', () { + // Being above the horizon is necessary; being lit while the ground is + // dark is what makes it visible. The filter can only remove passes. + final sgp4 = Sgp4(_iss); + List search({required bool sunlitOnly}) => + SatellitePasses.find( + sgp4, + from: _iss.epoch, + latitude: 25.033, + longitude: 121.5654, + window: const Duration(days: 3), + sunlitOnly: sunlitOnly, + ); + expect( + search(sunlitOnly: true).length, + lessThanOrEqualTo(search(sunlitOnly: false).length), + ); + }); + }); +} + +extension on TleSet { + /// Position [minutes] after epoch — a shorthand for the vector tests. + (double, double, double) propagatedBy(double minutes) => + Sgp4(this).propagate(minutes).position; +} diff --git a/test/core/astro/sky_features_test.dart b/test/core/astro/sky_features_test.dart new file mode 100644 index 000000000..eaa0cabe0 --- /dev/null +++ b/test/core/astro/sky_features_test.dart @@ -0,0 +1,322 @@ +/// Meteor showers, deep sky, the observing window, and the tide-raising force. +/// +/// These four have no single external table to check against the way the +/// ephemerides do, so each is pinned to the physics it claims. A shower's +/// radiant must be where the sky puts it; the equilibrium tide must have the +/// textbook solar-to-lunar ratio; a dark window must actually be dark. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/deep_sky.dart'; +import 'package:dpip/core/astro/meteor_showers.dart'; +import 'package:dpip/core/astro/moon_phase.dart'; +import 'package:dpip/core/astro/moon_rise_set.dart'; +import 'package:dpip/core/astro/night_window.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; +import 'package:dpip/core/astro/sun_events.dart'; +import 'package:dpip/core/astro/tidal_forcing.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _taipei = (latitude: 25.0330, longitude: 121.5654); +const _taiwan = Duration(hours: 8); + +void main() { + group('meteor showers', () { + test('the Perseid radiant is in Perseus, and it is circumpolar-ish', () { + // RA 3h12m, Dec +58 — high in the northern sky. From Taiwan it clears + // the horizon but never gets near the zenith, which is exactly the sort + // of thing a rate estimate has to account for. + final perseids = meteorShowers.firstWhere((s) => s.id == 'perseids'); + final conditions = MeteorShowerConditions.of( + perseids, + 2026, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(conditions.bestAltitude / degrees, inInclusiveRange(20, 60)); + expect(conditions.visibleRate, greaterThan(0)); + }); + + test('a southern radiant can be unobservable from the north', () { + // A fabricated shower at -70° declination never rises from Taipei, and + // its rate must be zero rather than its ZHR. + const antarctic = MeteorShower( + id: 'test', + peakMonth: 6, + peakDay: 15, + startMonth: 6, + startDay: 1, + endMonth: 6, + endDay: 30, + rightAscensionJ2000: 90, + declinationJ2000: -70, + zenithalRate: 100, + velocityKmS: 50, + ); + final conditions = MeteorShowerConditions.of( + antarctic, + 2026, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(conditions.bestAltitude, lessThan(0)); + expect(conditions.visibleRate, 0); + }); + + test('moonlight cuts the rate, and a new moon does not', () { + // The same shower on the same night, differing only in the Moon. This + // is the factor that decides whether a famous shower disappoints. + final geminids = meteorShowers.firstWhere((s) => s.id == 'geminids'); + final withMoon = ShowerConditions( + shower: geminids, + peak: DateTime.utc(2026, 12, 14), + bestTime: DateTime.utc(2026, 12, 14, 18), + bestAltitude: math.pi / 2, + moonIllumination: 1, + moonIsUp: true, + ); + final darkSky = ShowerConditions( + shower: geminids, + peak: DateTime.utc(2026, 12, 14), + bestTime: DateTime.utc(2026, 12, 14, 18), + bestAltitude: math.pi / 2, + moonIllumination: 0, + moonIsUp: false, + ); + expect(darkSky.visibleRate, geminids.zenithalRate); + expect(withMoon.visibleRate, closeTo(geminids.zenithalRate * 0.2, 1)); + expect(darkSky.isFavourable, isTrue); + expect(withMoon.isFavourable, isFalse); + }); + + test('the active list follows the calendar, wrapping the new year', () { + // The Quadrantids run 28 December to 12 January — a window that a naive + // month comparison drops entirely. + expect( + MeteorShowerConditions.activeOn(DateTime.utc(2026, 1, 3)) + .map((s) => s.id), + contains('quadrantids'), + ); + expect( + MeteorShowerConditions.activeOn(DateTime.utc(2025, 12, 30)) + .map((s) => s.id), + contains('quadrantids'), + ); + expect( + MeteorShowerConditions.activeOn(DateTime.utc(2026, 3, 1)) + .map((s) => s.id), + isEmpty, + ); + }); + }); + + group('deep sky', () { + test('the catalogue is complete and numbered 1-110', () { + expect(messierCatalogue, hasLength(110)); + expect( + messierCatalogue.map((o) => o.messier).toList()..sort(), + List.generate(110, (i) => i + 1), + ); + }); + + test('the famous ones are where they should be', () { + // Spot checks a reader would notice: M31 in Andromeda at +41°, M42 in + // Orion just below the equator, M45 the Pleiades. + final m31 = messierCatalogue.firstWhere((o) => o.messier == 31); + expect(m31.commonName, 'Andromeda'); + expect(m31.declinationJ2000, closeTo(41.27, 0.1)); + expect(m31.type, DeepSkyType.s); + + final m42 = messierCatalogue.firstWhere((o) => o.messier == 42); + expect(m42.commonName, 'Orion Nebula'); + expect(m42.rightAscensionJ2000, closeTo(83.85, 0.1)); + expect(m42.declinationJ2000, closeTo(-5.45, 0.1)); + + final m1 = messierCatalogue.firstWhere((o) => o.messier == 1); + expect(m1.commonName, 'Crab Nebula'); + expect(m1.type, DeepSkyType.snr); + }); + + test('precession moves an object, slightly and in the right direction', () { + // Twenty-six years from J2000 is about 0.36° of general precession — + // small, but the same correction the planets need, applied for the same + // reason. + final m31 = messierCatalogue.firstWhere((o) => o.messier == 31); + final now = m31.positionAt(DateTime.utc(2026)); + final shift = signedTurn( + now.rightAscension - m31.rightAscensionJ2000 * degrees, + ); + expect(shift / degrees, inInclusiveRange(0.05, 0.6)); + }); + }); + + group('night window', () { + test('a dark window is dark: no Sun, no Moon', () { + // Sampled inside the window the code returns, both conditions have to + // hold — this is the check that the intersection was actually taken and + // not just the Sun's part. + final night = NightConditions.of( + DateTime.utc(2026, 8, 15).subtract(_taiwan), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(night.darkWindows, isNotEmpty); + final window = night.best!; + final middle = window.from.add(window.length ~/ 2); + expect( + MoonRiseSet.aboveHorizon( + middle, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ), + isFalse, + reason: 'the Moon must be down inside a dark window', + ); + }); + + test('a full moon leaves no dark window at all', () { + // Full moon rises at sunset and sets at sunrise, so it covers the whole + // astronomical night. Reporting a window here would be the single most + // misleading thing this class could do. + final full = MoonPhase.nextFullMoon(DateTime.utc(2026, 6)); + final night = NightConditions.of( + DateTime.utc(full.year, full.month, full.day).subtract(_taiwan), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(night.moonIllumination, greaterThan(0.97)); + expect(night.totalDark, lessThan(const Duration(hours: 1))); + }); + + test('a new moon leaves the whole night', () { + final newMoon = MoonPhase.nextNewMoon(DateTime.utc(2026, 6)); + final night = NightConditions.of( + DateTime.utc( + newMoon.year, + newMoon.month, + newMoon.day, + ).subtract(_taiwan), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final astronomical = night.astronomicalNight!; + expect( + night.totalDark.inMinutes, + closeTo(astronomical.$2.difference(astronomical.$1).inMinutes, 90), + ); + }); + }); + + group('tidal forcing', () { + test('the Sun raises 46% of the Moon\'s tide', () { + // The one number here checkable against a textbook with no ocean in the + // way. Compared with the geometry and the distance law divided out — + // taking raw peaks instead would compare the Moon near perigee against + // the Sun near aphelion, since the Sun only passes overhead at Taipei in + // June when the Earth is furthest away. + final at = DateTime.utc(2026, 5, 3, 7); + final forcing = TidalForcing.at( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + + double potential(double altitude) { + final cosZenith = math.sin(altitude); + return (3 * cosZenith * cosZenith - 1) / 2; + } + + final moonAltitude = MoonRiseSet.lookFrom( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ).altitude; + final sunAltitude = SunEvents.lookFrom( + at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ).altitude; + final sunDistance = SunEphemeris.at(at).distanceKm; + + final lunarCoefficient = + forcing.lunarMetres / + potential(moonAltitude) / + forcing.distanceFactor; + final solarCoefficient = + forcing.solarMetres / + potential(sunAltitude) / + math.pow(astronomicalUnitKm / sunDistance, 3); + + expect(lunarCoefficient, closeTo(0.358, 0.001)); + expect(solarCoefficient, closeTo(0.164, 0.001)); + expect(solarCoefficient / lunarCoefficient, closeTo(0.46, 0.01)); + }); + + test('spring tides fall at new and full moon', () { + final full = MoonPhase.nextFullMoon(DateTime.utc(2026, 5)); + final quarter = MoonPhase.nextAngle( + DateTime.utc(2026, 5), + target: math.pi / 2, + ); + final spring = TidalForcing.at( + full, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final neap = TidalForcing.at( + quarter, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(spring.phase, TidePhase.spring); + expect(neap.phase, TidePhase.neap); + expect(spring.springNeap, greaterThan(0.98)); + expect(neap.springNeap, lessThan(0.02)); + }); + + test('the cube law makes perigee matter more than it looks', () { + // 14% of distance becomes nearly 50% of force. This is why a perigean + // spring is the one to watch for alongside a surge. + final perigee = TidalForcing.at( + DateTime.utc(2026, 12, 24, 13), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + final apogee = TidalForcing.at( + DateTime.utc(2025, 11, 19, 18), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(perigee.distanceFactor / apogee.distanceFactor, greaterThan(1.4)); + }); + + test('there are two highs and two lows a day', () { + // The (3cos²z − 1) potential peaks under the Moon *and* opposite it, + // which is why the tide is semidiurnal. One peak a day would mean the + // potential was wrong. + final extremes = TidalForcing.extremes( + DateTime.utc(2026, 8, 15).subtract(_taiwan), + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(extremes.where((e) => e.isHigh).length, inInclusiveRange(1, 3)); + expect(extremes.where((e) => !e.isHigh).length, inInclusiveRange(1, 3)); + expect(extremes.length, inInclusiveRange(3, 5)); + }); + + test('a perigean spring is found, and it really is both', () { + final at = TidalForcing.nextPerigeanSpring(DateTime.utc(2026)); + expect(at, isNotNull); + final forcing = TidalForcing.at( + at!, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + ); + expect(forcing.springNeap, greaterThan(0.9)); + expect(forcing.distanceFactor, greaterThan(1.15)); + expect(forcing.isPerigeanSpring, isTrue); + }); + }); +} diff --git a/test/core/astro/sun_test.dart b/test/core/astro/sun_test.dart new file mode 100644 index 000000000..84f953b22 --- /dev/null +++ b/test/core/astro/sun_test.dart @@ -0,0 +1,318 @@ +/// Golden pins for the Sun: position, daylight, and the solar terms. +/// +/// Three authorities, each checking something the others cannot: +/// +/// * **JPL Horizons** for the ecliptic longitude and distance. +/// * **The CWA's published 2026 sunrise/sunset timetable** for Keelung — +/// the table a user in Taiwan would compare this app against. +/// * **The CWA's 中華民國115年日曆資料表** for the twenty-four solar terms. +/// Every term is checked, not a sample, because a term is defined by an +/// exact solar longitude and a wrong one would mean the search, not the +/// data, is broken. +/// +/// The published tables give minutes, so a one-minute disagreement is their +/// rounding. Sunrise and sunset matched Keelung exactly on all six sampled +/// dates; the solar term *dates* all match, while the term *instants* are +/// good to a few minutes — the low-precision solar series is 0.01° in +/// longitude and the Sun takes about 14 minutes to move that far. +library; + +import 'dart:math' as math; + +import 'package:dpip/core/astro/astro_time.dart'; +import 'package:dpip/core/astro/solar_terms.dart'; +import 'package:dpip/core/astro/sun_ephemeris.dart'; +import 'package:dpip/core/astro/sun_events.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _taiwan = Duration(hours: 8); +const _keelung = (latitude: 25.1276, longitude: 121.7392); +const _taipei = (latitude: 25.0330, longitude: 121.5654); +const _reykjavik = (latitude: 64.1466, longitude: -21.9426); + +/// Longyearbyen — inside the Arctic Circle, where the Sun really does stay up. +const _svalbard = (latitude: 78.2232, longitude: 15.6469); + +double _deg(double radians) => radians * 180 / math.pi; +double _wrap180(double d) => ((d + 180) % 360 + 360) % 360 - 180; + +String _clock(DateTime? utc, Duration offset) { + if (utc == null) return '--:--'; + final local = utc.add(offset); + final rounded = local.second >= 30 + ? local.add(const Duration(minutes: 1)) + : local; + return '${rounded.hour.toString().padLeft(2, '0')}:' + '${rounded.minute.toString().padLeft(2, '0')}'; +} + +SunEvents _localDay( + int year, + int month, + int day, + Duration offset, + ({double latitude, double longitude}) place, +) => SunEvents.of( + DateTime.utc(year, month, day).subtract(offset), + latitude: place.latitude, + longitude: place.longitude, +); + +void main() { + group('SunEphemeris', () { + test('tracks JPL Horizons', () { + // Geocentric, ecliptic of date, from ssd.jpl.nasa.gov/api/horizons.api. + const rows = <(String, double, double)>[ + ('2024-01-01T00:00', 280.0389812, 0.98331828), + ('2025-04-12T21:00', 23.1955460, 1.00255629), + ('2026-08-11T04:00', 138.5283440, 1.01355338), + ('2026-12-27T17:00', 275.9471970, 0.98343175), + ]; + for (final (stamp, longitude, distanceAu) in rows) { + final sun = SunEphemeris.at(DateTime.parse('${stamp}Z')); + expect( + _wrap180(_deg(sun.longitude) - longitude).abs() * 3600, + lessThan(35), + reason: 'longitude at $stamp', + ); + expect( + (sun.distanceKm / astronomicalUnitKm - distanceAu).abs(), + lessThan(0.0001), + reason: 'distance at $stamp', + ); + } + }); + + test('the equation of time has its four known turning points', () { + // ~-14.2 min in mid-February, +3.7 in mid-May, -6.5 in late July, + // +16.4 in early November. These are the analemma's corners, and a sign + // error or a swapped term moves them somewhere else entirely. + const points = <(int, int, double)>[ + (2, 11, -14.2), + (5, 14, 3.7), + (7, 26, -6.5), + (11, 3, 16.4), + ]; + for (final (month, day, expected) in points) { + final minutes = + SunEphemeris.at(DateTime.utc(2026, month, day, 12)) + .equationOfTime + .inSeconds / + 60; + expect(minutes, closeTo(expected, 0.3), reason: '$month/$day'); + } + }); + }); + + group('SunEvents', () { + test('matches the CWA 2026 timetable for Keelung', () { + // 中華民國115年日出日沒時刻表. + const rows = <(int, int, String, String)>[ + (1, 1, '06:38', '17:15'), + (2, 1, '06:36', '17:37'), + (3, 1, '06:16', '17:55'), + (4, 1, '05:45', '18:09'), + (5, 1, '05:18', '18:23'), + (6, 1, '05:03', '18:39'), + ]; + for (final (month, day, rise, set) in rows) { + final events = _localDay(2026, month, day, _taiwan, _keelung); + expect(_clock(events.rise, _taiwan), rise, reason: '$month/$day rise'); + expect(_clock(events.set, _taiwan), set, reason: '$month/$day set'); + } + }); + + test('twilight is ordered, and deeper twilights are further out', () { + final events = _localDay(2026, 8, 15, _taiwan, _taipei); + final morning = [ + events.astronomicalDawn!, + events.nauticalDawn!, + events.civilDawn!, + events.rise!, + ]; + for (var i = 1; i < morning.length; i++) { + expect(morning[i].isAfter(morning[i - 1]), isTrue, reason: 'dawn $i'); + } + final evening = [ + events.set!, + events.civilDusk!, + events.nauticalDusk!, + events.astronomicalDusk!, + ]; + for (var i = 1; i < evening.length; i++) { + expect(evening[i].isAfter(evening[i - 1]), isTrue, reason: 'dusk $i'); + } + }); + + test('golden hour brackets sunrise and sunset', () { + // The band is -4° to +6°, so it opens before the Sun is up and closes + // after it. A golden hour entirely after sunrise would mean the + // thresholds were applied to the wrong side of the horizon. + final events = _localDay(2026, 8, 15, _taiwan, _taipei); + expect(events.blueMorningStart!.isBefore(events.rise!), isTrue); + expect(events.goldenMorningEnd!.isAfter(events.rise!), isTrue); + expect(events.goldenEveningStart!.isBefore(events.set!), isTrue); + expect(events.blueEveningEnd!.isAfter(events.set!), isTrue); + }); + + test('day length is longest at the summer solstice', () { + final june = _localDay(2026, 6, 21, _taiwan, _taipei).dayLength; + final december = _localDay(2026, 12, 21, _taiwan, _taipei).dayLength; + final march = _localDay(2026, 3, 20, _taiwan, _taipei).dayLength; + expect(june.inMinutes, greaterThan(december.inMinutes)); + // At Taipei's latitude the swing is about 3 hours; at the equinox the + // day is a little over 12 hours, because "sunrise" is the upper limb + // and refraction lifts it early at both ends. + expect(march.inMinutes / 60, closeTo(12.14, 0.1)); + expect((june - december).inMinutes / 60, closeTo(2.9, 0.3)); + }); + + test('polar day and polar night are answers, not missing ones', () { + // Longyearbyen at midsummer: the Sun does not set. Null rise and null + // set is correct, and the day length has to resolve to the full window + // rather than to zero or to a crash. Midwinter is the mirror. + final midsummer = _localDay(2026, 6, 21, Duration.zero, _svalbard); + expect(midsummer.rise, isNull); + expect(midsummer.set, isNull); + expect(midsummer.startsAbove, isTrue); + expect(midsummer.dayLength, const Duration(hours: 24)); + + final midwinter = _localDay(2026, 12, 21, Duration.zero, _svalbard); + expect(midwinter.rise, isNull); + expect(midwinter.set, isNull); + expect(midwinter.startsAbove, isFalse); + expect(midwinter.dayLength, Duration.zero); + }); + + test('a summer night at 64°N never gets astronomically dark', () { + // Reykjavík is *below* the Arctic Circle, so the Sun sets — but only + // just, and it never reaches 18° down. An observing window computed from + // sunset alone would claim a dark night that does not exist. + final midsummer = _localDay(2026, 6, 21, Duration.zero, _reykjavik); + expect(midsummer.set, isNotNull); + expect(midsummer.astronomicalDusk, isNull); + expect(midsummer.astronomicalNight, isNull); + }); + + test('solar noon is not clock noon', () { + // Taipei sits 1.5° east of the 120°E timezone meridian, worth about 6 + // minutes, and the equation of time adds up to another 16. If solar noon + // came out at 12:00 the calculation would be a clock, not the Sun. + final events = _localDay(2026, 11, 3, _taiwan, _taipei); + final local = events.noon!.add(_taiwan); + final offsetMinutes = local.hour * 60 + local.minute - 12 * 60; + expect(offsetMinutes, lessThan(-15)); + expect(offsetMinutes, greaterThan(-30)); + }); + }); + + group('SolarTerms', () { + test('every 2026 term lands on the date the CWA publishes', () { + // 中華民國115年日曆資料表, all twenty-four. + const expected = <(SolarTerm, int, int)>[ + (SolarTerm.minorCold, 1, 5), + (SolarTerm.majorCold, 1, 20), + (SolarTerm.startOfSpring, 2, 4), + (SolarTerm.rainWater, 2, 18), + (SolarTerm.awakeningOfInsects, 3, 5), + (SolarTerm.vernalEquinox, 3, 20), + (SolarTerm.pureBrightness, 4, 5), + (SolarTerm.grainRain, 4, 20), + (SolarTerm.startOfSummer, 5, 5), + (SolarTerm.grainFull, 5, 21), + (SolarTerm.grainInEar, 6, 5), + (SolarTerm.summerSolstice, 6, 21), + (SolarTerm.minorHeat, 7, 7), + (SolarTerm.majorHeat, 7, 23), + (SolarTerm.startOfAutumn, 8, 7), + (SolarTerm.endOfHeat, 8, 23), + (SolarTerm.whiteDew, 9, 7), + (SolarTerm.autumnalEquinox, 9, 23), + (SolarTerm.coldDew, 10, 8), + (SolarTerm.frostDescent, 10, 23), + (SolarTerm.startOfWinter, 11, 7), + (SolarTerm.minorSnow, 11, 22), + (SolarTerm.majorSnow, 12, 7), + (SolarTerm.winterSolstice, 12, 22), + ]; + final found = { + for (final (term, at) in SolarTerms.ofYear(2026, offset: _taiwan)) + term: at.add(_taiwan), + }; + expect(found, hasLength(24)); + for (final (term, month, day) in expected) { + expect(found[term]!.month, month, reason: '${term.name} month'); + expect(found[term]!.day, day, reason: '${term.name} day'); + } + }); + + test('a term is the instant the Sun reaches an exact longitude', () { + // The definition, and the only way to know the search converged rather + // than merely returned something plausible. + for (final term in SolarTerm.values) { + final at = SolarTerms.next(DateTime.utc(2026), term); + final error = signedTurn( + SunEphemeris.at(at).longitude - term.longitudeDegrees * degrees, + ); + expect( + _deg(error).abs() * 3600, + lessThan(1), + reason: '${term.name} convergence', + ); + } + }); + + test('the cardinal terms are the solstices and equinoxes', () { + expect(SolarTerm.values.where((t) => t.isCardinal).toList(), [ + SolarTerm.vernalEquinox, + SolarTerm.summerSolstice, + SolarTerm.autumnalEquinox, + SolarTerm.winterSolstice, + ]); + // The equinox is where the Sun crosses the celestial equator, so its + // declination is zero there — a check independent of the search itself. + final equinox = SolarTerms.next( + DateTime.utc(2026), + SolarTerm.vernalEquinox, + ); + expect( + _deg(SunEphemeris.at(equinox).equatorial.declination).abs(), + lessThan(0.01), + ); + }); + + test( + 'the major terms are the twelve that anchor the lunisolar calendar', + () { + final major = SolarTerm.values.where((t) => t.isMajor).toList(); + expect(major, hasLength(12)); + expect(major.every((t) => t.longitudeDegrees % 30 == 0), isTrue); + expect(major.contains(SolarTerm.winterSolstice), isTrue); + }, + ); + + test('successive occurrences are a tropical year apart', () { + // Walking the calendar is what a year view does, and a search that + // quietly returned its own input would stall there. + var previous = SolarTerms.next( + DateTime.utc(2026), + SolarTerm.winterSolstice, + ); + for (var year = 0; year < 8; year++) { + final next = SolarTerms.next( + previous.add(const Duration(days: 1)), + SolarTerm.winterSolstice, + ); + expect(next.difference(previous).inHours / 24, closeTo(365.24, 0.6)); + previous = next; + } + }); + + test('every search result is strictly after its starting point', () { + for (final term in SolarTerm.values) { + final at = SolarTerms.next(DateTime.utc(2026), term); + expect(SolarTerms.next(at, term).isAfter(at), isTrue); + } + }); + }); +} diff --git a/test/core/astro/tle_source_test.dart b/test/core/astro/tle_source_test.dart new file mode 100644 index 000000000..4f7670e50 --- /dev/null +++ b/test/core/astro/tle_source_test.dart @@ -0,0 +1,211 @@ +/// The element cache: when it refreshes, and what it refuses. +/// +/// The interesting behaviour is all in the refusals. A feed that is regenerated +/// on every request will hand back identical or *older* elements routinely, and +/// a cache that takes whatever arrives quietly makes its own predictions worse +/// over time. Freshness is therefore decided on the epoch inside the elements, +/// not on the bytes and not on an ETag — which this endpoint does not serve +/// anyway. +library; + +import 'package:dpip/core/astro/satellite.dart'; +import 'package:dpip/core/astro/tle_source.dart'; +import 'package:dpip/core/settings/preference_keys.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// The ISS on day 226 of 2026. +const _older = ''' +ISS (ZARYA) +1 25544U 98067A 26226.43871707 .00004555 00000+0 89427-4 0 9997 +2 25544 51.6329 11.7957 0007493 45.9133 314.2471 15.49439755580788 +'''; + +/// The same object, two days later. +const _newer = ''' +ISS (ZARYA) +1 25544U 98067A 26228.43871707 .00004555 00000+0 89427-4 0 9997 +2 25544 51.6329 11.7957 0007493 45.9133 314.2471 15.49439755580788 +'''; + +/// Something the parser cannot make sense of. +const _garbage = 'not a two-line element set at all'; + +class _Bundled implements TleSource { + const _Bundled(); + @override + Future> load() async => TleSet.parseAll(_older); +} + +class _Never implements TleSource { + const _Never(); + @override + Future> load() async => const []; +} + +Future _prefs([Map initial = const {}]) async { + SharedPreferences.setMockInitialValues(initial); + return Prefs(await SharedPreferences.getInstance()); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + var clock = DateTime.utc(2026, 8, 20); + + CachedTleSource source({ + required Prefs prefs, + TleFetcher? fetch, + TleSource fallback = const _Bundled(), + }) => CachedTleSource( + prefs: prefs, + now: () => clock, + fetch: fetch, + fallback: fallback, + ); + + setUp(() => clock = DateTime.utc(2026, 8, 20)); + + test('with no fetcher it is the bundle, and never touches the network', () { + // The shipping configuration until the app has a route to a feed. + return _prefs().then((prefs) async { + final loaded = await source(prefs: prefs).load(); + expect(loaded.single.catalogNumber, 25544); + expect( + prefs.getInt(PreferenceKeys.satelliteElementsFetchedAt), + isNull, + reason: 'nothing was fetched, so nothing was stamped', + ); + }); + }); + + test('the first load fetches and caches', () async { + final prefs = await _prefs(); + var calls = 0; + final loaded = await source( + prefs: prefs, + fetch: () async { + calls++; + return _newer; + }, + ).load(); + expect(calls, 1); + expect(loaded.single.epoch.day, 16); // day 228 of 2026 is 16 August + expect(prefs.getString(PreferenceKeys.satelliteElements), _newer); + expect(prefs.getInt(PreferenceKeys.satelliteElementsFetchedAt), isNotNull); + }); + + test('a second load inside the interval does not fetch again', () async { + final prefs = await _prefs(); + var calls = 0; + Future fetch() async { + calls++; + return _newer; + } + + await source(prefs: prefs, fetch: fetch).load(); + clock = clock.add(const Duration(hours: 30)); + await source(prefs: prefs, fetch: fetch).load(); + expect(calls, 1, reason: 'still inside the 48-hour window'); + + clock = clock.add(const Duration(hours: 20)); + await source(prefs: prefs, fetch: fetch).load(); + expect(calls, 2, reason: 'past the interval it refreshes'); + }); + + test('older elements are refused — the cache is never downgraded', () async { + // The failure this whole design exists to prevent. A feed regenerated per + // request happily serves an older set, and taking it would make every + // later prediction worse with no visible symptom. + final prefs = await _prefs(); + await source(prefs: prefs, fetch: () async => _newer).load(); + + clock = clock.add(const Duration(days: 3)); + final loaded = await source(prefs: prefs, fetch: () async => _older).load(); + + expect(prefs.getString(PreferenceKeys.satelliteElements), _newer); + expect(loaded.single.epoch.day, 16); + }); + + test('identical elements are accepted as up to date, not as a change', () { + return _prefs().then((prefs) async { + await source(prefs: prefs, fetch: () async => _newer).load(); + final firstStamp = prefs.getInt( + PreferenceKeys.satelliteElementsFetchedAt, + ); + + clock = clock.add(const Duration(days: 3)); + await source(prefs: prefs, fetch: () async => _newer).load(); + + // The stamp moves — we did check — but the stored text is untouched. + expect( + prefs.getInt(PreferenceKeys.satelliteElementsFetchedAt), + greaterThan(firstStamp!), + ); + expect(prefs.getString(PreferenceKeys.satelliteElements), _newer); + }); + }); + + test('a failed fetch falls back and retries next time, not next day', () async { + // Waiting out the whole interval after one dropped connection would mean a + // day of stale elements for a moment of bad signal. + final prefs = await _prefs(); + var calls = 0; + final loaded = await source( + prefs: prefs, + fetch: () async { + calls++; + throw StateError('offline'); + }, + ).load(); + + expect(calls, 1); + expect(loaded.single.catalogNumber, 25544, reason: 'the bundle answered'); + expect(prefs.getInt(PreferenceKeys.satelliteElementsFetchedAt), isNull); + + await source(prefs: prefs, fetch: () async => _newer).load(); + expect(calls, 1); + expect(prefs.getString(PreferenceKeys.satelliteElements), _newer); + }); + + test('garbage from the feed is ignored, not stored', () async { + final prefs = await _prefs(); + await source(prefs: prefs, fetch: () async => _newer).load(); + clock = clock.add(const Duration(days: 3)); + await source(prefs: prefs, fetch: () async => _garbage).load(); + expect(prefs.getString(PreferenceKeys.satelliteElements), _newer); + }); + + test('a corrupt cache falls back instead of taking the page down', () async { + final prefs = await _prefs({ + 'astro:satellite:tle': _garbage, + 'astro:satellite:fetchedAt': clock.millisecondsSinceEpoch, + }); + final loaded = await source(prefs: prefs).load(); + expect(loaded.single.catalogNumber, 25544); + }); + + test('an empty everything is an empty list, not a crash', () async { + final prefs = await _prefs(); + final loaded = await source(prefs: prefs, fallback: const _Never()).load(); + expect(loaded, isEmpty); + }); + + test('a backwards clock still refreshes', () async { + // Device clocks jump. Comparing "now minus last" without allowing for a + // negative would freeze refreshes until real time caught up. + final prefs = await _prefs(); + await source(prefs: prefs, fetch: () async => _newer).load(); + clock = clock.subtract(const Duration(days: 30)); + var calls = 0; + await source( + prefs: prefs, + fetch: () async { + calls++; + return _newer; + }, + ).load(); + expect(calls, 1); + }); +} diff --git a/test/core/meshtastic/dpip_mesh_codec_test.dart b/test/core/meshtastic/dpip_mesh_codec_test.dart new file mode 100644 index 000000000..e78ef6598 --- /dev/null +++ b/test/core/meshtastic/dpip_mesh_codec_test.dart @@ -0,0 +1,97 @@ +import 'dart:typed_data'; + +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + DpipMeshPacket packet( + List body, { + DpipMeshKind kind = DpipMeshKind.eew, + }) => DpipMeshPacket(kind: kind, schema: 2, body: Uint8List.fromList(body)); + + group('wire format', () { + // Pinned bytes: this is a protocol other builds (and other devices) have + // to agree with, so a change here must be a deliberate version bump. + test('is magic, version, kind, schema, body', () { + expect(DpipMeshCodec.encode(packet([0xAA, 0xBB])), [ + 0x44, // 'D' + 0x50, // 'P' + 1, // envelope version + 1, // DpipMeshKind.eew + 2, // body schema + 0xAA, + 0xBB, + ]); + }); + + test('round-trips through decode', () { + final decoded = DpipMeshCodec.decode( + DpipMeshCodec.encode(packet([1, 2, 3], kind: DpipMeshKind.tsunami)), + from: 42, + ); + expect(decoded, isNotNull); + expect(decoded!.kind, DpipMeshKind.tsunami); + expect(decoded.schema, 2); + expect(decoded.body, [1, 2, 3]); + expect(decoded.from, 42); + }); + + test('carries an empty body', () { + final decoded = DpipMeshCodec.decode( + DpipMeshCodec.encode(packet(const [], kind: DpipMeshKind.ping)), + ); + expect(decoded?.kind, DpipMeshKind.ping); + expect(decoded?.body, isEmpty); + }); + }); + + group('rejects', () { + test('anything shorter than the header', () { + expect(DpipMeshCodec.decode([0x44, 0x50, 1, 1]), isNull); + }); + + test('foreign traffic on the private port', () { + expect(DpipMeshCodec.decode([0x01, 0x02, 1, 1, 1, 9]), isNull); + }); + + test('an envelope version this build does not speak', () { + final bytes = DpipMeshCodec.encode(packet([1]))..[2] = 99; + expect(DpipMeshCodec.decode(bytes), isNull); + }); + + test('a kind this build does not know', () { + final bytes = DpipMeshCodec.encode(packet([1]))..[3] = 0x7F; + expect(DpipMeshCodec.decode(bytes), isNull); + }); + }); + + group('frame budget', () { + test('accepts a body that exactly fills the frame', () { + final body = Uint8List(DpipMeshCodec.maxBodyBytes); + expect( + DpipMeshCodec.encode(packet(body)).length, + DpipMeshCodec.headerBytes + DpipMeshCodec.maxBodyBytes, + ); + }); + + test('refuses to truncate an over-long body', () { + final body = Uint8List(DpipMeshCodec.maxBodyBytes + 1); + expect(() => DpipMeshCodec.encode(packet(body)), throwsArgumentError); + }); + }); + + test('kind codes are the protocol and must not drift', () { + expect( + {for (final kind in DpipMeshKind.values) kind.name: kind.code}, + {'eew': 1, 'report': 2, 'tsunami': 3, 'weather': 4, 'ping': 9}, + ); + expect(DpipMeshKind.fromCode(3), DpipMeshKind.tsunami); + expect(DpipMeshKind.fromCode(200), isNull); + }); + + test('the DPIP channel spec is the agreed one', () { + expect(DpipMeshChannel.name, 'DPIP'); + expect(DpipMeshChannel.psk, [0x01]); // base64 AQ== + expect(DpipMeshChannel.region, 'TW'); + }); +} diff --git a/test/core/meshtastic/dpip_mesh_gateway_test.dart b/test/core/meshtastic/dpip_mesh_gateway_test.dart new file mode 100644 index 000000000..e1ed2cd5d --- /dev/null +++ b/test/core/meshtastic/dpip_mesh_gateway_test.dart @@ -0,0 +1,139 @@ +import 'dart:typed_data'; + +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/meshtastic/data/dpip_mesh_gateway_impl.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'fake_mesh_service.dart'; + +void main() { + late FakeMeshService service; + int? channel; + + setUp(() { + service = FakeMeshService()..isConnected = true; + channel = 3; + }); + + DpipMeshGatewayImpl gateway() => DpipMeshGatewayImpl(service, () => channel); + + MeshDataPacket packet( + List payload, { + int port = MeshPorts.private, + int ch = 3, + }) => MeshDataPacket( + from: 7, + channel: ch, + portnum: port, + payload: payload, + timestamp: DateTime.utc(2026), + ); + + final dpipBytes = DpipMeshCodec.encode( + DpipMeshPacket(kind: DpipMeshKind.eew, body: Uint8List.fromList([9])), + ); + + group('inbound', () { + test('delivers a DPIP packet on the DPIP channel', () async { + final received = gateway().inbound.first; + service.data.add(packet(dpipBytes)); + expect((await received).kind, DpipMeshKind.eew); + }); + + test('ignores other app ports', () async { + final seen = []; + final sub = gateway().inbound.listen(seen.add); + service.data + ..add(packet(dpipBytes, port: MeshPorts.text)) + ..add(packet(dpipBytes)); + await Future.delayed(Duration.zero); + await sub.cancel(); + expect(seen, hasLength(1)); + }); + + test( + 'ignores private traffic on another channel once provisioned', + () async { + final seen = []; + final sub = gateway().inbound.listen(seen.add); + service.data.add(packet(dpipBytes, ch: 0)); + await Future.delayed(Duration.zero); + await sub.cancel(); + expect(seen, isEmpty); + }, + ); + + test('accepts any channel before provisioning resolves', () async { + channel = null; + final received = gateway().inbound.first; + service.data.add(packet(dpipBytes, ch: 0)); + expect((await received).kind, DpipMeshKind.eew); + }); + + test('drops payloads that are not DPIP envelopes', () async { + final seen = []; + final sub = gateway().inbound.listen(seen.add); + service.data.add(packet([1, 2, 3, 4, 5, 6])); + await Future.delayed(Duration.zero); + await sub.cancel(); + expect(seen, isEmpty); + }); + }); + + group('broadcast', () { + test('encodes onto the DPIP channel and private port', () async { + final result = await gateway().broadcast( + DpipMeshPacket( + kind: DpipMeshKind.tsunami, + schema: 4, + body: Uint8List.fromList([1, 2]), + ), + ); + expect(result, isA>()); + final sent = service.sentData.single; + expect(sent.portnum, MeshPorts.private); + expect(sent.channel, 3); + expect(sent.payload, [0x44, 0x50, 1, 3, 4, 1, 2]); + }); + + test('fails while the DPIP channel is not provisioned', () async { + channel = null; + final result = await gateway().broadcast( + DpipMeshPacket(kind: DpipMeshKind.ping, body: Uint8List(0)), + ); + expect(result, isA>()); + expect(service.sentData, isEmpty); + }); + + test('fails with no radio', () async { + service.isConnected = false; + final result = await gateway().broadcast( + DpipMeshPacket(kind: DpipMeshKind.ping, body: Uint8List(0)), + ); + expect(result, isA>()); + expect(service.sentData, isEmpty); + }); + + test('refuses an over-long body instead of truncating it', () async { + final result = await gateway().broadcast( + DpipMeshPacket( + kind: DpipMeshKind.report, + body: Uint8List(DpipMeshCodec.maxBodyBytes + 1), + ), + ); + expect(result, isA>()); + expect(service.sentData, isEmpty); + }); + }); + + test('isReady tracks the link and the channel', () { + expect(gateway().isReady, isTrue); + channel = null; + expect(gateway().isReady, isFalse); + channel = 3; + service.isConnected = false; + expect(gateway().isReady, isFalse); + }); +} diff --git a/test/core/meshtastic/fake_mesh_service.dart b/test/core/meshtastic/fake_mesh_service.dart new file mode 100644 index 000000000..7fc08d0bc --- /dev/null +++ b/test/core/meshtastic/fake_mesh_service.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:dpip/core/error/failure.dart'; +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; + +/// A [MeshtasticService] the tests drive by hand: streams they pump, results +/// they queue, and a record of everything the code under test sent. +class FakeMeshService implements MeshtasticService { + final messages = StreamController.broadcast(); + final connections = StreamController.broadcast(); + final nodes = StreamController.broadcast(); + final data = StreamController.broadcast(); + + /// Devices [scanForDevices] yields. + List scanResults = const []; + + /// Consumed one per connect attempt; the last one repeats. + List> connectResults = const [Ok(null)]; + int connectCalls = 0; + final List connectedIds = []; + + MeshLinkOwner owner = MeshLinkOwner.free; + + @override + bool isConnected = false; + + @override + List channels = const []; + + @override + String? region; + + @override + int? myNodeNum = 0x1234; + + /// What [ensureChannel] answers, and what it was asked for. + Result ensureChannelResult = const Ok(3); + final List ensuredChannels = []; + + final List appliedRegions = []; + Result applyRegionResult = const Ok(null); + + final List<({int portnum, int channel, List payload})> sentData = []; + final List sentText = []; + final List sentChannels = []; + Failure? sendFailure; + + @override + Stream get messageStream => messages.stream; + + @override + Stream get connectionStream => connections.stream; + + @override + Stream get nodeStream => nodes.stream; + + @override + Stream get dataStream => data.stream; + + final traffics = StreamController.broadcast(); + + @override + MeshTraffic traffic = const MeshTraffic(); + + @override + Stream get trafficStream => traffics.stream; + + @override + MeshRadioInfo? radioInfo; + + @override + Future> initialize() async => const Ok(null); + + @override + Stream scanForDevices({Duration timeout = Duration.zero}) => + Stream.fromIterable(scanResults); + + @override + Future> connect(MeshDevice device) => connectToId(device.id); + + @override + Future> connectToId(String id) async { + final result = + connectResults[connectCalls.clamp(0, connectResults.length - 1)]; + connectCalls++; + if (result is Ok) { + connectedIds.add(id); + isConnected = true; + } + return result; + } + + @override + Future linkOwner(String deviceId) async => owner; + + @override + Future> disconnect() async { + isConnected = false; + // The real transport reports every teardown on the status stream, and + // code under test relies on that. + connections.add( + const MeshConnectionStatus(state: MeshConnectionState.disconnected), + ); + return const Ok(null); + } + + @override + Future> sendText(String text, {int channel = 0}) async { + final failure = sendFailure; + if (failure != null) return Err(failure); + sentText.add(text); + sentChannels.add(channel); + return const Ok(null); + } + + @override + Future> sendData({ + required int portnum, + required List payload, + int channel = 0, + bool wantAck = false, + }) async { + final failure = sendFailure; + if (failure != null) return Err(failure); + sentData.add((portnum: portnum, channel: channel, payload: payload)); + return const Ok(null); + } + + @override + Future> ensureChannel(MeshChannelSpec spec) async { + ensuredChannels.add(spec); + return ensureChannelResult; + } + + @override + Future> applyRegion(String region) async { + appliedRegions.add(region); + if (applyRegionResult is Ok) this.region = region; + return applyRegionResult; + } +} diff --git a/test/core/meshtastic/mesh_alerts_test.dart b/test/core/meshtastic/mesh_alerts_test.dart new file mode 100644 index 000000000..40c6cb32d --- /dev/null +++ b/test/core/meshtastic/mesh_alerts_test.dart @@ -0,0 +1,230 @@ +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_alerts.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'fake_mesh_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + var clock = DateTime.utc(2026, 1, 1, 12); + late List posted; + + Future<(MeshAlerts, FakeMeshService)> makeAlerts([ + Map initial = const {}, + ]) async { + clock = DateTime.utc(2026, 1, 1, 12); + posted = []; + SharedPreferences.setMockInitialValues(initial); + final service = FakeMeshService(); + final alerts = MeshAlerts( + service, + Prefs(await SharedPreferences.getInstance()), + post: (alert) async => posted.add(alert), + now: () => clock, + )..start(); + return (alerts, service); + } + + MeshMessage message(String text, {int channel = 0, DateTime? at}) => + MeshMessage( + from: 0x1234, + channel: channel, + text: text, + timestamp: at ?? clock, + ); + + MeshNode node(int num) => + MeshNode(num: num, displayName: 'node $num', isOnline: true); + + Future settle() => Future.delayed(Duration.zero); + + /// Brings the link up and moves past the node-DB dump window. + Future linkReadyAndSettled(FakeMeshService service) async { + service.connections.add( + const MeshConnectionStatus(state: MeshConnectionState.connected), + ); + await settle(); + clock = clock.add(const Duration(minutes: 1)); + } + + group('messages', () { + test('raises a notification by default', () async { + final (_, service) = await makeAlerts(); + service.messages.add(message('hello')); + await settle(); + + expect(posted.single.channelKey, 'mesh_message'); + }); + + test('titles with the channel and names the sender', () async { + final (_, service) = await makeAlerts(); + service + ..channels = const [ + MeshChannel(index: 2, name: 'DPIP', psk: [1], enabled: true), + ] + ..nodes.add( + const MeshNode(num: 0x1234, displayName: '保大 node', isOnline: true), + ); + await settle(); + + service.messages.add( + message('地震', channel: 2, at: DateTime.utc(2026, 1, 1, 9, 5, 7)), + ); + await settle(); + + expect(posted.single.title, 'Meshtastic - DPIP'); + expect(posted.single.body, '保大 node - 09:05:07\n地震'); + }); + + test('falls back to the channel index when no name is known', () async { + // Exactly the offline case: channel names live in the radio's table, and + // with no radio attached there is no table. + final (_, service) = await makeAlerts(); + service.messages.add(message('hello', channel: 1)); + await settle(); + + expect(posted.single.title, 'Meshtastic - CH1'); + // Sender, clock, then the message on its own line. + expect(posted.single.body, '0x1234 - 12:00:00\nhello'); + }); + + test('stays quiet for the conversation already on screen', () async { + final (alerts, service) = await makeAlerts(); + alerts.setVisibleChannel(0); + service.messages.add(message('hello')); + await settle(); + + expect(posted, isEmpty); + }); + + test('still announces another channel while one is on screen', () async { + final (alerts, service) = await makeAlerts(); + alerts.setVisibleChannel(0); + service.messages.add(message('hello', channel: 3)); + await settle(); + + expect(posted, hasLength(1)); + }); + + test( + 'announces the visible channel once the app is backgrounded', + () async { + final (alerts, service) = await makeAlerts(); + alerts + ..setVisibleChannel(0) + ..setForeground(foreground: false); + service.messages.add(message('hello')); + await settle(); + + expect(posted, hasLength(1)); + }, + ); + + test('respects the off switch', () async { + final (_, service) = await makeAlerts({ + 'meshtastic.notifyMessages': false, + }); + service.messages.add(message('hello')); + await settle(); + + expect(posted, isEmpty); + }); + + test('ignores an empty body', () async { + final (_, service) = await makeAlerts(); + service.messages.add(message('')); + await settle(); + + expect(posted, isEmpty); + }); + }); + + group('nodes', () { + test('are off by default', () async { + final (_, service) = await makeAlerts(); + await linkReadyAndSettled(service); + service.nodes.add(node(1)); + await settle(); + + expect(posted, isEmpty); + }); + + test('never announce the node DB delivered on connect', () async { + final (_, service) = await makeAlerts({'meshtastic.notifyNodes': true}); + // Config download: nodes arrive before the link reports `connected`. + for (var i = 0; i < 20; i++) { + service.nodes.add(node(i)); + } + await settle(); + + expect(posted, isEmpty); + }); + + test('stay quiet during the settle window after connecting', () async { + final (_, service) = await makeAlerts({'meshtastic.notifyNodes': true}); + service.connections.add( + const MeshConnectionStatus(state: MeshConnectionState.connected), + ); + await settle(); + clock = clock.add(const Duration(seconds: 5)); + + service.nodes.add(node(99)); + await settle(); + expect(posted, isEmpty); + }); + + test('announce a node first heard after things settled', () async { + final (_, service) = await makeAlerts({'meshtastic.notifyNodes': true}); + await linkReadyAndSettled(service); + + service.nodes.add(node(99)); + await settle(); + + expect(posted.single.channelKey, 'mesh_node'); + expect(posted.single.title, 'Meshtastic'); + expect(posted.single.body, 'node 99'); + }); + + test('announce each node only once', () async { + final (_, service) = await makeAlerts({'meshtastic.notifyNodes': true}); + await linkReadyAndSettled(service); + + service.nodes + ..add(node(99)) + ..add(node(99)); + await settle(); + + expect(posted, hasLength(1)); + }); + + test('cap a burst of new neighbours', () async { + final (_, service) = await makeAlerts({'meshtastic.notifyNodes': true}); + await linkReadyAndSettled(service); + + for (var i = 100; i < 110; i++) { + service.nodes.add(node(i)); + } + await settle(); + + expect(posted, hasLength(3)); + }); + + test('a reconnect re-arms the dump suppression', () async { + final (_, service) = await makeAlerts({'meshtastic.notifyNodes': true}); + await linkReadyAndSettled(service); + + service.connections.add( + const MeshConnectionStatus(state: MeshConnectionState.disconnected), + ); + await settle(); + // The next connection dumps the node DB again — nothing in it is new. + service.nodes.add(node(500)); + await settle(); + + expect(posted, isEmpty); + }); + }); +} diff --git a/test/core/meshtastic/mesh_link_liveness_test.dart b/test/core/meshtastic/mesh_link_liveness_test.dart new file mode 100644 index 000000000..8ecb9243e --- /dev/null +++ b/test/core/meshtastic/mesh_link_liveness_test.dart @@ -0,0 +1,44 @@ +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'fake_mesh_service.dart'; + +/// A radio that accepts the link and is gone by the time the connect returns. +/// +/// Every `disconnected` during an attempt is written off as the transport's +/// own disconnect-before-connect, so this drop is invisible to the status +/// listener — without the post-connect liveness check the link would never +/// come back. +class _VanishingService extends FakeMeshService { + @override + Future> connectToId(String id) async { + final result = await super.connectToId(id); + isConnected = false; + return result; + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('retries when the radio drops during the connect itself', () async { + SharedPreferences.setMockInitialValues({}); + final service = _VanishingService(); + final link = MeshLink( + service, + Prefs(await SharedPreferences.getInstance()), + ); + link.start(); + + await link.attach(const MeshDevice(id: 'AA:BB', name: 'radio')); + + expect(link.isConnected, isFalse); + // `reconnecting` stays false — nothing was ever connected — but a retry + // must still be queued, which is the whole point. + expect(link.willRetry, isTrue); + }); +} diff --git a/test/core/meshtastic/mesh_link_supersede_test.dart b/test/core/meshtastic/mesh_link_supersede_test.dart new file mode 100644 index 000000000..b00927238 --- /dev/null +++ b/test/core/meshtastic/mesh_link_supersede_test.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'fake_mesh_service.dart'; + +/// A service whose connect can be released by the test, so `attach` / +/// `detach` can be driven *while an attempt is in flight*. +class _SlowService extends FakeMeshService { + final gate = Completer(); + var gated = true; + + @override + Future> connectToId(String id) async { + if (gated) { + gated = false; + await gate.future; + } + return super.connectToId(id); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const first = MeshDevice(id: 'AA:11', name: 'radio-a'); + const second = MeshDevice(id: 'BB:22', name: 'radio-b'); + + Future<(MeshLink, T)> makeLink(T service) async { + SharedPreferences.setMockInitialValues({}); + final link = MeshLink( + service, + Prefs(await SharedPreferences.getInstance()), + ); + link.start(); + return (link, service); + } + + test('picking a second radio while connected switches to it', () async { + final (link, service) = await makeLink(FakeMeshService()); + await link.attach(first); + service.connections.add( + const MeshConnectionStatus(state: MeshConnectionState.connected), + ); + await Future.delayed(Duration.zero); + + expect(await link.attach(second), isNull); + + // The old link is dropped and the new radio is the one connected and + // remembered — the single-flight guard used to swallow this silently. + expect(service.connectedIds.last, 'BB:22'); + expect(link.savedRadioId, 'BB:22'); + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString('meshtastic.deviceId'), 'BB:22'); + }); + + test('a superseded attempt cannot write its radio back', () async { + final (link, service) = await makeLink(_SlowService()); + final firstAttach = link.attach(first); // parks inside connectToId + await Future.delayed(Duration.zero); + + final secondAttach = link.attach(second); + service.gate.complete(); + await firstAttach; + await secondAttach; + + expect(link.savedRadioId, 'BB:22'); + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString('meshtastic.deviceId'), 'BB:22'); + }); + + test('detach during a connect leaves no phantom link', () async { + final (link, service) = await makeLink(_SlowService()); + final attaching = link.attach(first); + await Future.delayed(Duration.zero); + + await link.detach(); + service.gate.complete(); + await attaching; + await Future.delayed(Duration.zero); + + // The forgotten radio must not stay connected, and a `connected` that + // lands afterwards must not be adopted either. + expect(link.savedRadioId, isNull); + expect(service.isConnected, isFalse); + + service.connections.add( + const MeshConnectionStatus(state: MeshConnectionState.connected), + ); + await Future.delayed(Duration.zero); + expect(link.willRetry, isFalse); + }); +} diff --git a/test/core/meshtastic/mesh_link_test.dart b/test/core/meshtastic/mesh_link_test.dart new file mode 100644 index 000000000..57a0d2eea --- /dev/null +++ b/test/core/meshtastic/mesh_link_test.dart @@ -0,0 +1,229 @@ +import 'package:dpip/core/error/failure.dart'; +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/core/meshtastic/domain/dpip_mesh.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'fake_mesh_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const device = MeshDevice(id: 'AA:BB', name: 'YuYu_7d70'); + + Future<(MeshLink, FakeMeshService)> makeLink([ + Map initial = const {}, + ]) async { + SharedPreferences.setMockInitialValues(initial); + final service = FakeMeshService(); + final prefs = Prefs(await SharedPreferences.getInstance()); + return (MeshLink(service, prefs), service); + } + + void emit(FakeMeshService service, MeshConnectionState state) => + service.connections.add(MeshConnectionStatus(state: state)); + + Future settle() => Future.delayed(Duration.zero); + + group('attach', () { + test('remembers the radio so a restart can pick it up', () async { + final (link, service) = await makeLink(); + expect(await link.attach(device), isNull); + + expect(service.connectedIds, ['AA:BB']); + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString('meshtastic.deviceId'), 'AA:BB'); + expect(prefs.getString('meshtastic.deviceName'), 'YuYu_7d70'); + }); + + test( + 'stops on a radio another app holds, and proceeds when forced', + () async { + final (link, service) = await makeLink(); + service.owner = MeshLinkOwner.otherApp; + + expect(await link.attach(device), MeshLink.busySentinel); + expect(service.connectCalls, 0); + + expect(await link.attach(device, force: true), isNull); + expect(service.connectCalls, 1); + }, + ); + + test('falls back to a scan when the saved id is stale', () async { + final (link, service) = await makeLink(); + service + ..connectResults = const [ + Err(UnexpectedFailure('unknown peripheral')), + Ok(null), + ] + ..scanResults = const [MeshDevice(id: 'AA:BB', name: 'YuYu_7d70')]; + + expect(await link.attach(device), isNull); + expect(service.connectCalls, 2); + }); + + test('does not retry a denied permission', () async { + final (link, service) = await makeLink(); + service.connectResults = const [ + Err(PermissionDeniedFailure('bluetooth denied')), + ]; + + expect(await link.attach(device), 'bluetooth denied'); + expect(link.reconnecting, isFalse); + // No scan fallback either — a scan needs the same permission. + expect(service.connectCalls, 1); + }); + }); + + group('start', () { + test('reconnects to the saved radio', () async { + final (link, service) = await makeLink({ + 'meshtastic.deviceId': 'AA:BB', + 'meshtastic.deviceName': 'YuYu_7d70', + }); + link.start(); + await settle(); + + expect(service.connectedIds, ['AA:BB']); + expect(link.savedRadioName, 'YuYu_7d70'); + }); + + test('does nothing without a saved radio', () async { + final (link, service) = await makeLink(); + link.start(); + await settle(); + + expect(service.connectCalls, 0); + expect(link.reconnecting, isFalse); + }); + }); + + group('link loss', () { + test('schedules a reconnect after an unexpected drop', () async { + final (link, service) = await makeLink(); + link.start(); + await link.attach(device); + emit(service, MeshConnectionState.connected); + await settle(); + + emit(service, MeshConnectionState.disconnected); + await settle(); + expect(link.reconnecting, isTrue); + }); + + test('ignores the disconnect the transport emits mid-connect', () async { + final (link, service) = await makeLink(); + link.start(); + // A connect that reports `disconnected` while it is running — which the + // transport does, because it tears down any previous link first. + service.connectResults = const [Ok(null)]; + final attaching = link.attach(device); + emit(service, MeshConnectionState.disconnected); + await attaching; + await settle(); + + expect(link.reconnecting, isFalse); + }); + + test('does not reconnect after the user detached', () async { + final (link, service) = await makeLink(); + link.start(); + await link.attach(device); + await link.detach(); + + emit(service, MeshConnectionState.disconnected); + await settle(); + + expect(link.reconnecting, isFalse); + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString('meshtastic.deviceId'), isNull); + }); + }); + + group('provisioning', () { + test('creates the DPIP channel once the radio is up', () async { + final (link, service) = await makeLink(); + service + ..region = 'TW' + ..ensureChannelResult = const Ok(2); + link.start(); + await link.attach(device); + + emit(service, MeshConnectionState.connected); + await settle(); + + expect(service.ensuredChannels.single.name, 'DPIP'); + expect(service.ensuredChannels.single.psk, [0x01]); + expect(link.provision, MeshProvisionState.ready); + expect(link.dpipChannel, 2); + }); + + test( + 'reports a radio with no free slot instead of overwriting one', + () async { + final (link, service) = await makeLink(); + service + ..region = 'TW' + ..ensureChannelResult = const Err( + MeshChannelNoSlotFailure('The radio has no free channel slot'), + ); + link.start(); + await link.attach(device); + + emit(service, MeshConnectionState.connected); + await settle(); + + expect(link.provision, MeshProvisionState.noFreeSlot); + expect(link.dpipChannel, isNull); + }, + ); + + test('sets the region on a radio that has never had one', () async { + final (link, service) = await makeLink(); + service.region = 'UNSET'; + link.start(); + await link.attach(device); + + emit(service, MeshConnectionState.connected); + await settle(); + + expect(service.appliedRegions, [DpipMeshChannel.region]); + }); + + test('never changes a region someone else chose', () async { + final (link, service) = await makeLink(); + service.region = 'EU_868'; + link.start(); + await link.attach(device); + + emit(service, MeshConnectionState.connected); + await settle(); + + expect(service.appliedRegions, isEmpty); + expect(link.regionState, MeshRegionState.mismatch); + + // Only an explicit confirmation applies it. + expect(await link.applyRegion(), isNull); + expect(service.appliedRegions, [DpipMeshChannel.region]); + }); + + test('forgets the channel when the link drops', () async { + final (link, service) = await makeLink(); + service.region = 'TW'; + link.start(); + await link.attach(device); + emit(service, MeshConnectionState.connected); + await settle(); + expect(link.dpipChannel, 3); + + emit(service, MeshConnectionState.disconnected); + await settle(); + expect(link.dpipChannel, isNull); + expect(link.provision, MeshProvisionState.idle); + }); + }); +} diff --git a/test/core/meshtastic/mesh_node_store_test.dart b/test/core/meshtastic/mesh_node_store_test.dart new file mode 100644 index 000000000..e8504005f --- /dev/null +++ b/test/core/meshtastic/mesh_node_store_test.dart @@ -0,0 +1,281 @@ +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'fake_mesh_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + var clock = DateTime.utc(2026, 1, 1, 12); + + MeshNode node( + int num, { + String name = 'n', + double? lat, + double? lon, + int? battery, + double snr = 0, + DateTime? heard, + bool viaMqtt = false, + }) => MeshNode( + num: num, + displayName: name, + isOnline: true, + batteryLevel: battery, + lastHeard: heard, + latitude: lat, + longitude: lon, + snr: snr, + viaMqtt: viaMqtt, + ); + + Future<(MeshNodeStore, FakeMeshService)> makeStore([ + Map initial = const {}, + ]) async { + clock = DateTime.utc(2026, 1, 1, 12); + SharedPreferences.setMockInitialValues(initial); + final service = FakeMeshService(); + final store = MeshNodeStore( + service, + Prefs(await SharedPreferences.getInstance()), + now: () => clock, + )..start(); + return (store, service); + } + + /// Lets the debounced write fire. + Future flush() => + Future.delayed(const Duration(seconds: 2, milliseconds: 100)); + + test('collects nodes off the stream', () async { + final (store, service) = await makeStore(); + service.nodes + ..add(node(1, name: 'a')) + ..add(node(2, name: 'b')); + await Future.delayed(Duration.zero); + + expect(store.nodes, hasLength(2)); + expect(store.byNum(1)?.displayName, 'a'); + }); + + test('keeps a position a later update omits', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(1, lat: 23.5, lon: 120.5)); + await Future.delayed(Duration.zero); + // A telemetry-driven re-emit carries fresh metrics but no position. + service.nodes.add(node(1, battery: 80)); + await Future.delayed(Duration.zero); + + expect(store.byNum(1)?.latitude, 23.5); + expect(store.byNum(1)?.batteryLevel, 80); + }); + + test('only offers positioned nodes to the map', () async { + final (store, service) = await makeStore(); + service.nodes + ..add(node(1, lat: 23.5, lon: 120.5)) + ..add(node(2)); + await Future.delayed(Duration.zero); + + expect(store.positioned.single.num, 1); + }); + + test('survives a restart, with freshness recomputed', () async { + final (store, service) = await makeStore(); + service.nodes.add( + node(7, name: 'repeater', lat: 23.5, lon: 120.5, heard: clock), + ); + await flush(); + + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getStringList('meshtastic.nodes'); + expect(stored, hasLength(1)); + + // A day later the same entry must not still claim to be online. + final (restored, _) = await makeStore({'meshtastic.nodes': stored!}); + clock = clock.add(const Duration(days: 1)); + + final node7 = restored.byNum(7); + expect(node7?.displayName, 'repeater'); + expect(node7?.latitude, 23.5); + expect(restored.isOnline(node7!), isFalse); + }); + + test('online is a window on lastHeard, not a stored flag', () async { + final (store, service) = await makeStore(); + service.nodes + ..add(node(1, heard: clock.subtract(const Duration(minutes: 1)))) + ..add(node(2, heard: clock.subtract(const Duration(hours: 3)))) + ..add(node(3)); + await Future.delayed(Duration.zero); + + expect(store.isOnline(store.byNum(1)!), isTrue); + expect(store.isOnline(store.byNum(2)!), isFalse); + expect(store.isOnline(store.byNum(3)!), isFalse); // never heard + }); + + test('drops the least recently heard past the cap', () async { + final (store, service) = await makeStore(); + for (var i = 0; i < MeshNodeStore.maxNodes + 5; i++) { + service.nodes.add(node(i, heard: clock.subtract(Duration(minutes: i)))); + } + await flush(); + + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getStringList('meshtastic.nodes')!; + expect(stored, hasLength(MeshNodeStore.maxNodes)); + + final (restored, _) = await makeStore({'meshtastic.nodes': stored}); + // 0 was heard most recently, the tail is the oldest. + expect(restored.byNum(0), isNotNull); + expect(restored.byNum(MeshNodeStore.maxNodes + 4), isNull); + }); + + group('MQTT nodes', () { + test('are kept off the map by default', () async { + final (store, service) = await makeStore(); + service.nodes + ..add(node(1, lat: 23.5, lon: 120.5)) + ..add(node(2, lat: 35.6, lon: 139.7, viaMqtt: true)); // Tokyo + await Future.delayed(Duration.zero); + + expect(store.excludeMqtt, isTrue); + // The node is still known — it just isn't evidence of radio reach. + expect(store.nodes, hasLength(2)); + expect(store.positioned.single.num, 1); + expect(store.hiddenMqttCount, 1); + }); + + test( + 'come back when the filter is turned off, and that persists', + () async { + final (store, service) = await makeStore(); + service.nodes.add(node(2, lat: 35.6, lon: 139.7, viaMqtt: true)); + await Future.delayed(Duration.zero); + + await store.setExcludeMqtt(exclude: false); + expect(store.positioned, hasLength(1)); + expect(store.hiddenMqttCount, 0); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getBool('map.meshExcludeMqtt'), isFalse); + }, + ); + + test('the flag survives a restart', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(2, lat: 35.6, lon: 139.7, viaMqtt: true)); + await flush(); + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getStringList('meshtastic.nodes')!; + + final (restored, _) = await makeStore({'meshtastic.nodes': stored}); + expect(restored.byNum(2)?.viaMqtt, isTrue); + expect(restored.positioned, isEmpty); + }); + }); + + test('ignores a corrupt entry instead of losing the table', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(1, name: 'good')); + await flush(); + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getStringList('meshtastic.nodes')!; + + final (restored, _) = await makeStore({ + 'meshtastic.nodes': ['not json', ...stored], + }); + expect(restored.nodes.single.displayName, 'good'); + }); + + test('clear empties the table and its storage', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(1)); + await flush(); + + await store.clear(); + expect(store.nodes, isEmpty); + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getStringList('meshtastic.nodes'), isEmpty); + }); + + group('telemetry history', () { + test('records one sample per distinct reading', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(1, snr: -5.0, battery: 80)); + await Future.delayed(Duration.zero); + clock = clock.add(const Duration(minutes: 1)); + service.nodes.add(node(1, snr: -3.5, battery: 79)); + await Future.delayed(Duration.zero); + + final history = store.historyOf(1); + expect(history, hasLength(2)); + expect(history.first.snr, -5.0); + expect(history.last.snr, -3.5); + expect(history.last.battery, 79); + expect( + history.last.time.difference(history.first.time), + const Duration(minutes: 1), + ); + }); + + test('a repeated reading only moves the last sample in time', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(1, snr: -5.0, battery: 80)); + await Future.delayed(Duration.zero); + clock = clock.add(const Duration(minutes: 5)); + // A node burst re-emits the same telemetry; that must not pile up. + service.nodes + ..add(node(1, snr: -5.0, battery: 80)) + ..add(node(1, snr: -5.0, battery: 80)); + await Future.delayed(Duration.zero); + + final history = store.historyOf(1); + expect(history, hasLength(1)); + expect(history.single.time, clock); + }); + + test('capped at the ring size', () async { + final (store, service) = await makeStore(); + for (var i = 0; i < MeshNodeStore.historyLimit + 10; i++) { + service.nodes.add(node(1, snr: -10.0 + i)); + await Future.delayed(Duration.zero); + clock = clock.add(const Duration(seconds: 1)); + } + + final history = store.historyOf(1); + expect(history, hasLength(MeshNodeStore.historyLimit)); + // The newest survives, the oldest is gone. + expect(history.last.snr, -10.0 + MeshNodeStore.historyLimit + 9); + expect(history.first.snr, -10.0 + 10); + }); + }); + + group('distance to my radio', () { + test('measured from the radio node, not the phone', () async { + final (store, service) = await makeStore(); + // My radio sits in Hualien city; the node is 1° north on the same + // meridian (~111 km) and 1° east on it (~100 km). + service.nodes + ..add(node(0x1234, name: 'mine', lat: 24.0, lon: 121.6)) + ..add(node(1, lat: 25.0, lon: 122.6)); + await Future.delayed(Duration.zero); + + final km = store.distanceToMyRadioKm(store.byNum(1)!); + expect(km, isNotNull); + expect(km!, greaterThan(140)); + expect(km, lessThan(160)); + }); + + test('null without a radio position', () async { + final (store, service) = await makeStore(); + service.nodes.add(node(1, lat: 25.0, lon: 122.6)); + await Future.delayed(Duration.zero); + + expect(store.distanceToMyRadioKm(store.byNum(1)!), isNull); + }); + }); +} diff --git a/test/core/meshtastic/mesh_store_test.dart b/test/core/meshtastic/mesh_store_test.dart new file mode 100644 index 000000000..5d0d67800 --- /dev/null +++ b/test/core/meshtastic/mesh_store_test.dart @@ -0,0 +1,161 @@ +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +void main() { + late Database db; + var clock = DateTime.utc(2026, 1, 10, 12); + + setUpAll(sqfliteFfiInit); + + setUp(() async { + clock = DateTime.utc(2026, 1, 10, 12); + db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath); + await MeshStore.createSchema(db); + }); + + tearDown(() async => db.close()); + + MeshStore store() => MeshStore(db, now: () => clock); + + MeshStoredMessage message( + String text, { + int from = 1, + int channel = 0, + DateTime? at, + bool outgoing = false, + }) => MeshStoredMessage( + from: from, + channel: channel, + text: text, + timestamp: at ?? clock, + outgoing: outgoing, + ); + + group('messages', () { + test('round-trip, newest first', () async { + final s = store(); + await s.addMessage( + message('older', at: clock.subtract(const Duration(minutes: 5))), + ); + await s.addMessage(message('newer')); + + final rows = await s.messages(); + expect(rows.map((m) => m.text), ['newer', 'older']); + expect(rows.first.outgoing, isFalse); + }); + + test('the same message twice is stored once', () async { + final s = store(); + expect(await s.addMessage(message('same')), isTrue); + // A reconnect replays packets the log may already hold; the unique index + // is what decides, and the caller is told it was not new. + expect(await s.addMessage(message('same')), isFalse); + expect(await s.messages(), hasLength(1)); + }); + + test('same text on another channel is a different message', () async { + final s = store(); + await s.addMessage(message('hi')); + await s.addMessage(message('hi', channel: 3)); + expect(await s.messages(), hasLength(2)); + }); + + test('narrows to one channel', () async { + final s = store(); + await s.addMessage(message('primary')); + await s.addMessage(message('secondary', channel: 3)); + + expect((await s.messages(channel: 3)).single.text, 'secondary'); + }); + + test('counts per channel', () async { + final s = store(); + await s.addMessage(message('a', at: clock)); + await s.addMessage( + message('b', at: clock.add(const Duration(seconds: 1))), + ); + await s.addMessage(message('c', channel: 2)); + + expect(await s.messageCountsByChannel(), {0: 2, 2: 1}); + }); + + test('prune drops what is past retention and keeps the rest', () async { + final s = store(); + await s.addMessage( + message('ancient', at: clock.subtract(const Duration(days: 31))), + ); + await s.addMessage(message('recent')); + + await s.prune(); + expect((await s.messages()).single.text, 'recent'); + }); + + test('clearMessages empties the table', () async { + final s = store(); + await s.addMessage(message('bye')); + await s.clearMessages(); + expect(await s.messages(), isEmpty); + }); + }); + + group('metrics', () { + test('returns samples oldest first', () async { + final s = store(); + await s.addMetric( + MeshMetricSample( + at: clock.subtract(const Duration(hours: 2)), + channelUtilization: 3, + airUtilTx: 1, + ), + ); + await s.addMetric( + MeshMetricSample(at: clock, channelUtilization: 9, airUtilTx: 2), + ); + + final rows = await s.metrics(); + expect(rows.map((m) => m.channelUtilization), [3, 9]); + expect(rows.last.airUtilTx, 2); + }); + + test('the same reading twice stays one row', () async { + final s = store(); + final sample = MeshMetricSample(at: clock, channelUtilization: 5); + await s.addMetric(sample); + await s.addMetric(sample); + expect(await s.metrics(), hasLength(1)); + }); + + test( + 'only the last 24 hours are returned, and older rows pruned', + () async { + final s = store(); + await s.addMetric( + MeshMetricSample( + at: clock.subtract(const Duration(hours: 30)), + channelUtilization: 1, + ), + ); + await s.addMetric( + MeshMetricSample( + at: clock.subtract(const Duration(hours: 2)), + channelUtilization: 2, + ), + ); + + expect((await s.metrics()).single.channelUtilization, 2); + await s.prune(); + // The window filter and the prune agree — one is not hiding the other. + expect((await s.metrics()).single.channelUtilization, 2); + }, + ); + + test('keeps a null reading distinguishable from zero', () async { + final s = store(); + await s.addMetric(MeshMetricSample(at: clock, airUtilTx: 0)); + final row = (await s.metrics()).single; + expect(row.channelUtilization, isNull); + expect(row.airUtilTx, 0); + }); + }); +} diff --git a/test/core/meshtastic/mesh_traffic_counter_test.dart b/test/core/meshtastic/mesh_traffic_counter_test.dart new file mode 100644 index 000000000..7275c1a12 --- /dev/null +++ b/test/core/meshtastic/mesh_traffic_counter_test.dart @@ -0,0 +1,59 @@ +import 'package:dpip/core/meshtastic/data/mesh_traffic_counter.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + var clock = DateTime.utc(2026, 1, 1); + MeshTrafficCounter counter() => MeshTrafficCounter(now: () => clock); + + setUp(() => clock = DateTime.utc(2026, 1, 1)); + + test('starts empty', () { + final traffic = counter().snapshot; + expect(traffic.isEmpty, isTrue); + expect(traffic.lastRx, isNull); + expect(traffic.lastTx, isNull); + }); + + test('counts packets, bytes and ports in each direction', () { + final c = counter() + ..recordRx(portnum: MeshPorts.text, bytes: 10) + ..recordRx(portnum: MeshPorts.text, bytes: 5) + ..recordRx(portnum: MeshPorts.private, bytes: 20) + ..recordTx(bytes: 7); + + final traffic = c.snapshot; + expect(traffic.rxPackets, 3); + expect(traffic.rxBytes, 35); + expect(traffic.rxByPort, {MeshPorts.text: 2, MeshPorts.private: 1}); + expect(traffic.txPackets, 1); + expect(traffic.txBytes, 7); + expect(traffic.isEmpty, isFalse); + }); + + test('counts a packet the radio could not decrypt', () { + // It carries no port and no readable payload, but it is still proof the + // link is delivering — which is the whole reason the counters exist. + final traffic = (counter()..recordRx(portnum: null, bytes: 32)).snapshot; + expect(traffic.rxPackets, 1); + expect(traffic.rxUndecoded, 1); + expect(traffic.rxBytes, 32); + expect(traffic.rxByPort, isEmpty); + }); + + test('stamps the last packet in each direction', () { + final c = counter()..recordRx(portnum: 1, bytes: 1); + expect(c.snapshot.lastRx, DateTime.utc(2026, 1, 1)); + expect(c.snapshot.lastTx, isNull); + + clock = DateTime.utc(2026, 1, 1, 0, 5); + c.recordTx(bytes: 1); + expect(c.snapshot.lastRx, DateTime.utc(2026, 1, 1)); + expect(c.snapshot.lastTx, DateTime.utc(2026, 1, 1, 0, 5)); + }); + + test('hands out an unmodifiable snapshot', () { + final traffic = (counter()..recordRx(portnum: 1, bytes: 1)).snapshot; + expect(() => traffic.rxByPort[9] = 1, throwsUnsupportedError); + }); +} diff --git a/test/core/network/etag_binary_test.dart b/test/core/network/etag_binary_test.dart index c621938e1..41074d1e4 100644 --- a/test/core/network/etag_binary_test.dart +++ b/test/core/network/etag_binary_test.dart @@ -107,69 +107,63 @@ void main() { expect(second.headers.value('etag'), 'v1'); }); - test( - 'immutable radar tile stores under URL-hash even without server ETag', - () async { - final payload = Uint8List.fromList([0x52, 0x49, 0x46, 0x46]); - final adapter = _BinaryAdapter(bytes: payload); // no etag - final dio = createDio(etagCache: store)..httpClientAdapter = adapter; - const url = - 'https://static.core-tnn1.exptech.dev/api/v2/tiles/radar/1/7/1/1.webp'; - final expectedEtag = EtagInterceptor.etagFromUrl(Uri.parse(url)); - - await dio.get>( - url, - options: Options(responseType: ResponseType.bytes), - ); - CachedBytes? entry; - for (var i = 0; i < 50 && entry == null; i++) { - await Future.delayed(const Duration(milliseconds: 10)); - entry = await store.readBytes(url); - } - expect(entry, isNotNull); - expect(entry!.etag, expectedEtag); - expect(entry.bytes, payload); - - final second = await dio.get>( - url, - options: Options(responseType: ResponseType.bytes), - ); - expect(adapter.calls, 1, reason: 'local URL hit, no revalidation'); - expect(second.data, payload); - }, - ); - - test( - 'immutable wind tile stores under URL-hash even without server ETag', - () async { - final payload = Uint8List.fromList([0x57, 0x49, 0x4e, 0x44]); - final adapter = _BinaryAdapter(bytes: payload); // no etag - final dio = createDio(etagCache: store)..httpClientAdapter = adapter; - const url = - 'https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/1786384800/5/26/14.webp?model=ecmwf'; - final expectedEtag = EtagInterceptor.etagFromUrl(Uri.parse(url)); - - await dio.get>( - url, - options: Options(responseType: ResponseType.bytes), - ); - CachedBytes? entry; - for (var i = 0; i < 50 && entry == null; i++) { - await Future.delayed(const Duration(milliseconds: 10)); - entry = await store.readBytes(url); - } - expect(entry, isNotNull); - expect(entry!.etag, expectedEtag); - expect(entry.bytes, payload); - - final second = await dio.get>( - url, - options: Options(responseType: ResponseType.bytes), - ); - expect(adapter.calls, 1, reason: 'local URL hit, no revalidation'); - expect(second.data, payload); - }, - ); + test('immutable radar tile stores under URL-hash even without server ETag', () async { + final payload = Uint8List.fromList([0x52, 0x49, 0x46, 0x46]); + final adapter = _BinaryAdapter(bytes: payload); // no etag + final dio = createDio(etagCache: store)..httpClientAdapter = adapter; + const url = + 'https://static.core-tnn1.exptech.dev/api/v2/tiles/radar/1/7/1/1.webp'; + final expectedEtag = EtagInterceptor.etagFromUrl(Uri.parse(url)); + + await dio.get>( + url, + options: Options(responseType: ResponseType.bytes), + ); + CachedBytes? entry; + for (var i = 0; i < 50 && entry == null; i++) { + await Future.delayed(const Duration(milliseconds: 10)); + entry = await store.readBytes(url); + } + expect(entry, isNotNull); + expect(entry!.etag, expectedEtag); + expect(entry.bytes, payload); + + final second = await dio.get>( + url, + options: Options(responseType: ResponseType.bytes), + ); + expect(adapter.calls, 1, reason: 'local URL hit, no revalidation'); + expect(second.data, payload); + }); + + test('immutable wind tile stores under URL-hash even without server ETag', () async { + final payload = Uint8List.fromList([0x57, 0x49, 0x4e, 0x44]); + final adapter = _BinaryAdapter(bytes: payload); // no etag + final dio = createDio(etagCache: store)..httpClientAdapter = adapter; + const url = + 'https://static.core-tnn1.exptech.dev/api/v2/tiles/wind/1786384800/5/26/14.webp?model=ecmwf'; + final expectedEtag = EtagInterceptor.etagFromUrl(Uri.parse(url)); + + await dio.get>( + url, + options: Options(responseType: ResponseType.bytes), + ); + CachedBytes? entry; + for (var i = 0; i < 50 && entry == null; i++) { + await Future.delayed(const Duration(milliseconds: 10)); + entry = await store.readBytes(url); + } + expect(entry, isNotNull); + expect(entry!.etag, expectedEtag); + expect(entry.bytes, payload); + + final second = await dio.get>( + url, + options: Options(responseType: ResponseType.bytes), + ); + expect(adapter.calls, 1, reason: 'local URL hit, no revalidation'); + expect(second.data, payload); + }); test('basemap PBF uses URL-hash ETag and hits locally on repeat', () async { final payload = Uint8List.fromList([0x1a, 0x2b, 0x3c]); diff --git a/test/core/network/etag_cache_store_test.dart b/test/core/network/etag_cache_store_test.dart index 1db364a30..135d289e7 100644 --- a/test/core/network/etag_cache_store_test.dart +++ b/test/core/network/etag_cache_store_test.dart @@ -294,7 +294,7 @@ void main() { expect(await store.read('https://x/a'), isNull); }); - test('a write sweeps entries last-used older than maxAge (7 days)', () async { + test('entries never expire by age — only the byte budget trims', () async { await store.write('https://x/old', etag: '1', body: 'A'); final eightDaysAgo = DateTime.now() .subtract(const Duration(days: 8)) @@ -310,34 +310,12 @@ void main() { expect( await store.read('https://x/old'), - isNull, - reason: 'expired evicted', + isNotNull, + reason: 'an old entry that still fits the budget must survive any write', ); expect(await store.read('https://x/new'), isNotNull); }); - test( - 'a recent read keeps an otherwise-old entry past the age sweep', - () async { - await store.write('https://x/kept', etag: '1', body: 'A'); - final eightDaysAgo = DateTime.now() - .subtract(const Duration(days: 8)) - .millisecondsSinceEpoch; - await db.update( - 'http_cache', - {'time': eightDaysAgo}, - where: 'key = ?', - whereArgs: ['https://x/kept'], - ); - expect(await store.read('https://x/kept'), isNotNull); - // Allow async touch to land. - await Future.delayed(const Duration(milliseconds: 20)); - - await store.write('https://x/new', etag: '2', body: 'B'); - expect(await store.read('https://x/kept'), isNotNull); - }, - ); - test('size round-trips', () async { await store.write('https://x/a', etag: '1', body: 'ABCDE', size: 4096); expect((await store.read('https://x/a'))!.size, 4096); @@ -410,8 +388,8 @@ void main() { bytes: fat, contentType: 'image/webp', ); - // Pin last-used near "now" so the 7-day age sweep won't eat them, but b - // stays older than a (buffered touch timers can't scramble the order). + // Pin last-used near "now" so the LRU order is explicit, with b older + // than a (buffered touch timers can't scramble the order). final now = DateTime.now().millisecondsSinceEpoch; await db.update( 'http_cache', @@ -436,4 +414,77 @@ void main() { expect(await tight.readBytes('https://x/a'), isNotNull); expect(await tight.readBytes('https://x/c'), isNotNull); }); + + test( + 'over budget trims oldest-first and stops once back under the ceiling', + () async { + // Three 100-byte rows against a ceiling that fits two: the oldest must + // go, the two newest stay, and the trim must not overshoot (delete more + // than the surplus). + final fat = Uint8List(100); + final tight = EtagCacheStore(db, maxBytes: 250); + + await tight.writeBytes( + 'https://x/a', + etag: '1', + bytes: fat, + contentType: 'image/webp', + ); + await Future.delayed(const Duration(milliseconds: 2)); + await tight.writeBytes( + 'https://x/b', + etag: '2', + bytes: fat, + contentType: 'image/webp', + ); + await Future.delayed(const Duration(milliseconds: 2)); + await tight.writeBytes( + 'https://x/c', + etag: '3', + bytes: fat, + contentType: 'image/webp', + ); + + expect(await tight.readBytes('https://x/a'), isNull, reason: 'oldest'); + expect(await tight.readBytes('https://x/b'), isNotNull); + expect(await tight.readBytes('https://x/c'), isNotNull); + final stats = await tight.stats(); + expect(stats.bytes, lessThanOrEqualTo(250)); + expect( + stats.bytes, + greaterThanOrEqualTo(200), + reason: 'trims only the surplus', + ); + }, + ); + + test('an under-budget write never trims, however old the rows are', () async { + final fat = Uint8List(100); + final tight = EtagCacheStore(db, maxBytes: 10 * 1024 * 1024); + await tight.writeBytes( + 'https://x/old', + etag: '1', + bytes: fat, + contentType: 'image/webp', + ); + final eightDaysAgo = DateTime.now() + .subtract(const Duration(days: 8)) + .millisecondsSinceEpoch; + await db.update( + 'http_cache', + {'time': eightDaysAgo}, + where: 'key = ?', + whereArgs: ['https://x/old'], + ); + + await tight.writeBytes( + 'https://x/new', + etag: '2', + bytes: fat, + contentType: 'image/webp', + ); + + expect(await tight.readBytes('https://x/old'), isNotNull); + expect(await tight.readBytes('https://x/new'), isNotNull); + }); } diff --git a/test/core/network/sse_client_test.dart b/test/core/network/sse_client_test.dart index 2114ee9fc..1cce706c6 100644 --- a/test/core/network/sse_client_test.dart +++ b/test/core/network/sse_client_test.dart @@ -66,36 +66,31 @@ void main() { ); test('strips only a single leading space after the colon', () async { - final events = await HttpSseClient.parse( - _bytes(['data: x\n\n']), - ).toList(); + final events = await HttpSseClient.parse(_bytes(['data: x\n\n'])) + .toList(); expect(events.single.data, ' x'); }); test( 'does not emit a frame that received no fields (double blank)', () async { - final events = await HttpSseClient.parse( - _bytes(['\n\ndata: y\n\n']), - ).toList(); + final events = await HttpSseClient.parse(_bytes(['\n\ndata: y\n\n'])) + .toList(); expect(events, hasLength(1)); expect(events.single.data, 'y'); }, ); - test( - 'accepts a Stream (Dio response.stream runtime type)', - () async { - // Dio delivers the response body as Stream; `Stream.transform` - // reifies its input type from the receiver, so decoding must use - // `utf8.decoder.bind` — this is the type the live feed actually passes, - // and it must not throw a Utf8Decoder/StreamTransformer subtype error. - final stream = Stream.fromIterable([ - Uint8List.fromList(utf8.encode('data: []\n\n')), - ]); - final events = await HttpSseClient.parse(stream).toList(); - expect(events.single.data, '[]'); - }, - ); + test('accepts a Stream (Dio response.stream runtime type)', () async { + // Dio delivers the response body as Stream; `Stream.transform` + // reifies its input type from the receiver, so decoding must use + // `utf8.decoder.bind` — this is the type the live feed actually passes, + // and it must not throw a Utf8Decoder/StreamTransformer subtype error. + final stream = Stream.fromIterable([ + Uint8List.fromList(utf8.encode('data: []\n\n')), + ]); + final events = await HttpSseClient.parse(stream).toList(); + expect(events.single.data, '[]'); + }); }); } diff --git a/test/core/platform/render_tier_test.dart b/test/core/platform/render_tier_test.dart new file mode 100644 index 000000000..5162729bb --- /dev/null +++ b/test/core/platform/render_tier_test.dart @@ -0,0 +1,53 @@ +import 'package:dpip/core/platform/device_info.dart'; +import 'package:dpip/core/platform/render_tier.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + DeviceDetails device({int? totalMemoryMb, int? sdkInt = 33}) => DeviceDetails( + manufacturer: 'Test', + model: 'TestModel', + osVersion: '14', + sdkInt: sdkInt, + totalMemoryMb: totalMemoryMb, + ); + + group('renderTierFor', () { + test('a low-RAM Android phone is downgraded', () { + expect( + renderTierFor(device(totalMemoryMb: 3072), isAndroid: true), + RenderTier.low, + reason: '2–4 GB Android devices are the low-end GPU class', + ); + expect( + renderTierFor(device(totalMemoryMb: 4095), isAndroid: true), + RenderTier.low, + ); + }); + + test('a mid/high-RAM Android phone keeps full quality', () { + expect( + renderTierFor(device(totalMemoryMb: 4096), isAndroid: true), + RenderTier.high, + ); + expect( + renderTierFor(device(totalMemoryMb: 12288), isAndroid: true), + RenderTier.high, + ); + }); + + test('an unknown RAM reading stays high — never degrade on a miss', () { + expect( + renderTierFor(device(totalMemoryMb: null), isAndroid: true), + RenderTier.high, + ); + }); + + test('iOS is never downgraded, even on a low-RAM device', () { + expect( + renderTierFor(device(totalMemoryMb: 2048), isAndroid: false), + RenderTier.high, + reason: 'the oldest supported iPhones still outdraw low-end Androids', + ); + }); + }); +} diff --git a/test/core/storage/app_storage_scan_test.dart b/test/core/storage/app_storage_scan_test.dart new file mode 100644 index 000000000..5c17db6fa --- /dev/null +++ b/test/core/storage/app_storage_scan_test.dart @@ -0,0 +1,198 @@ +import 'package:dpip/core/storage/app_storage_scan.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('storageBreakdown', () { + StorageScan scan({ + int totalBytes = 700 * 1024 * 1024, + List dirs = const [], + List files = const [], + }) => StorageScan(totalBytes: totalBytes, dirs: dirs, files: files); + + test('known big files are pulled out of their directory', () { + final s = scan( + totalBytes: 300 * 1024 * 1024, + dirs: const [ + StorageEntry(path: '/caches', bytes: 200 * 1024 * 1024), + StorageEntry(path: '/support', bytes: 100 * 1024 * 1024), + ], + files: const [ + StorageEntry( + path: '/caches/http_etag_cache.db', + bytes: 180 * 1024 * 1024, + ), + StorageEntry( + path: '/support/MapLibre/cache.db', + bytes: 60 * 1024 * 1024, + ), + ], + ); + final slices = storageBreakdown(s); + expect( + slices, + contains( + predicate((s) => s.label == 'ETag cache (SQLite)'), + ), + ); + expect( + slices.firstWhere((s) => s.label == 'ETag cache (SQLite)').bytes, + 180 * 1024 * 1024, + ); + expect( + slices.firstWhere((s) => s.label == 'MapLibre').bytes, + 60 * 1024 * 1024, + ); + // The cache directory keeps the leftover after the DB is subtracted. + expect( + slices.firstWhere((s) => s.label == 'caches (other)').bytes, + 20 * 1024 * 1024, + ); + }); + + test('a dir that gave up a known file is labelled with (other)', () { + final s = scan( + totalBytes: 210 * 1024 * 1024, + dirs: const [StorageEntry(path: '/caches', bytes: 210 * 1024 * 1024)], + files: const [ + StorageEntry( + path: '/caches/http_etag_cache.db', + bytes: 150 * 1024 * 1024, + ), + ], + ); + final slices = storageBreakdown(s); + expect(slices.any((s) => s.label == 'caches (other)'), isTrue); + // A directory that kept everything keeps its plain name. + expect(slices.any((s) => s.label == 'caches'), isFalse); + }); + + test('/private/var and /var spellings match the same directory', () { + final s = scan( + totalBytes: 300 * 1024 * 1024, + dirs: const [ + StorageEntry(path: '/var/.../Caches', bytes: 300 * 1024 * 1024), + ], + files: const [ + StorageEntry( + path: '/private/var/.../Caches/http_etag_cache.db', + bytes: 200 * 1024 * 1024, + ), + ], + ); + final slices = storageBreakdown(s); + expect( + slices.firstWhere((s) => s.label == 'ETag cache (SQLite)').bytes, + 200 * 1024 * 1024, + ); + expect( + slices.firstWhere((s) => s.label == 'Caches (other)').bytes, + 100 * 1024 * 1024, + ); + }); + + test('the -wal and -shm companions count with the SQLite db', () { + final s = scan( + totalBytes: 210 * 1024 * 1024, + dirs: const [StorageEntry(path: '/caches', bytes: 210 * 1024 * 1024)], + files: const [ + StorageEntry( + path: '/caches/http_etag_cache.db', + bytes: 150 * 1024 * 1024, + ), + StorageEntry( + path: '/caches/http_etag_cache.db-wal', + bytes: 40 * 1024 * 1024, + ), + ], + ); + final slices = storageBreakdown(s); + expect( + slices.firstWhere((s) => s.label == 'ETag cache (SQLite)').bytes, + 190 * 1024 * 1024, + ); + }); + + test('the difference below the file-reporting floor becomes Other', () { + final s = scan( + totalBytes: 100 * 1024 * 1024, + dirs: const [StorageEntry(path: '/caches', bytes: 80 * 1024 * 1024)], + // No large files: everything stays inside the directory bucket… + files: const [], + ); + final slices = storageBreakdown(s); + // …but totalBytes is the whole sandbox, so the unseen 20 MB is Other. + expect( + slices.firstWhere((s) => s.label == 'Other').bytes, + 20 * 1024 * 1024, + ); + // Nothing was subtracted, so the directory keeps its plain name. + expect( + slices.firstWhere((s) => s.label == 'caches').bytes, + 80 * 1024 * 1024, + ); + }); + + test('system HTTP cache and engine caches get their own labels', () { + final s = scan( + totalBytes: 90 * 1024 * 1024, + dirs: const [StorageEntry(path: '/caches', bytes: 90 * 1024 * 1024)], + files: const [ + StorageEntry(path: '/caches/HTTPCache/123', bytes: 50 * 1024 * 1024), + StorageEntry(path: '/caches/io.flutter/x', bytes: 30 * 1024 * 1024), + ], + ); + final slices = storageBreakdown(s); + expect( + slices.firstWhere((s) => s.label == 'System HTTP cache').bytes, + 50 * 1024 * 1024, + ); + expect( + slices.firstWhere((s) => s.label == 'Flutter engine').bytes, + 30 * 1024 * 1024, + ); + }); + + test('debug kernel snapshots (*.dill) count as engine, not tmp', () { + final s = scan( + totalBytes: 190 * 1024 * 1024, + dirs: const [StorageEntry(path: '/tmp', bytes: 190 * 1024 * 1024)], + files: const [ + StorageEntry(path: '/tmp/main.dart.dill', bytes: 90 * 1024 * 1024), + StorageEntry( + path: '/tmp/main.dart.swap.dill', + bytes: 90 * 1024 * 1024, + ), + ], + ); + final slices = storageBreakdown(s); + expect( + slices.firstWhere((s) => s.label == 'Flutter engine').bytes, + 180 * 1024 * 1024, + ); + expect( + slices.firstWhere((s) => s.label == 'tmp (other)').bytes, + 10 * 1024 * 1024, + ); + }); + }); + + group('formatBytes', () { + test('human-friendly units', () { + expect(formatBytes(0), '0 B'); + expect(formatBytes(512), '512 B'); + expect(formatBytes(1024), '1.0 KB'); + expect(formatBytes(5 * 1024 * 1024), '5.0 MB'); + expect(formatBytes(700 * 1024 * 1024), '700 MB'); + }); + }); + + group('StorageEntry.shortPath', () { + test('keeps the containing directory', () { + const entry = StorageEntry( + path: '/var/mobile/.../tmp/main.dart.dill', + bytes: 1, + ); + expect(entry.shortPath, 'tmp/main.dart.dill'); + }); + }); +} diff --git a/test/features/data/data_page_test.dart b/test/features/data/data_page_test.dart new file mode 100644 index 000000000..4fc9625b8 --- /dev/null +++ b/test/features/data/data_page_test.dart @@ -0,0 +1,105 @@ +/// The data hub's catalogue. +/// +/// Written because a tile silently went missing: the astronomy grid was edited +/// by a text substitution that no longer matched, four entries never landed, +/// and nothing failed — not the analyzer, not a gate, not another test. Every +/// route the hub is supposed to offer is now asserted by name, so the next +/// entry that fails to land fails here instead of on a phone. +library; + +import 'package:dpip/features/data/presentation/pages/data_page.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:dpip/shared/navigation/app_routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +/// Every destination the astronomy section must offer, with the English label +/// the tile carries. +const _astronomyTiles = <(String, String)>[ + (AppRoutes.moon, 'Moon'), + (AppRoutes.sun, 'Sun'), + (AppRoutes.planets, 'Planets'), + (AppRoutes.tonight, 'Tonight'), + (AppRoutes.skyChart, 'Sky chart'), + (AppRoutes.almanac, 'Almanac'), + (AppRoutes.tide, 'Tide'), +]; + +/// A router that records where a tap tried to go, without building the target +/// page — the point here is the catalogue, not its destinations. +GoRouter _router(List visited) => GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, _) => const DataPage(), + routes: [ + for (final (name, _) in _astronomyTiles) + GoRoute( + path: name, + name: name, + builder: (_, _) { + visited.add(name); + return const SizedBox.shrink(); + }, + ), + GoRoute( + path: 'weather-ranking', + name: AppRoutes.weatherRanking, + builder: (_, _) => const SizedBox.shrink(), + ), + GoRoute( + path: 'earthquake', + name: AppRoutes.earthquake, + builder: (_, _) => const SizedBox.shrink(), + ), + ], + ), + ], +); + +Future _pump(WidgetTester tester, GoRouter router) async { + // Tall enough to lay out and *hit-test* the whole hub: the seismic card, the + // seven-tile weather grid and the seven-tile astronomy grid below it. On a + // phone the page scrolls; here nothing may fall past the viewport, or a tap + // silently misses. + tester.view.physicalSize = const Size(800, 3000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget( + MaterialApp.router( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: router, + ), + ); + await tester.pump(); +} + +void main() { + testWidgets('offers every astronomy page', (tester) async { + await _pump(tester, _router([])); + for (final (route, label) in _astronomyTiles) { + expect(find.text(label), findsOneWidget, reason: '$route tile missing'); + } + }); + + testWidgets('astronomy sits below weather', (tester) async { + await _pump(tester, _router([])); + final weather = tester.getTopLeft(find.text('Weather')).dy; + final astronomy = tester.getTopLeft(find.text('Astronomy')).dy; + expect(astronomy, greaterThan(weather)); + }); + + for (final (route, label) in _astronomyTiles) { + testWidgets('the $label tile navigates to $route', (tester) async { + // A fresh router per tile: a tile wired to the wrong route would + // otherwise be masked by an earlier tap having already visited it. + final visited = []; + await _pump(tester, _router(visited)); + await tester.tap(find.text(label)); + await tester.pumpAndSettle(); + expect(visited, [route]); + }); + } +} diff --git a/test/features/data/moon_page_test.dart b/test/features/data/moon_page_test.dart new file mode 100644 index 000000000..ef198599b --- /dev/null +++ b/test/features/data/moon_page_test.dart @@ -0,0 +1,136 @@ +/// The moon page's wiring: which instant it shows, and which place the rise +/// and set times belong to. +/// +/// The astronomy is pinned in `test/core/astro/`; what is checked here is +/// everything between it and the screen — that the calendar and the timeline +/// address the same selection, and that a page which names a township names +/// the one it actually computed for. +library; + +import 'package:dpip/core/geo/town_directory.dart'; +import 'package:dpip/core/realtime/app_time.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:dpip/core/settings/region_store.dart'; +import 'package:dpip/features/data/presentation/pages/moon_page.dart'; +import 'package:dpip/features/data/presentation/widgets/moon_calendar.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final _directory = TownDirectory.fromJson({ + '100': { + 'city': '臺北', + 'town': '中正', + 'lat': 25.03, + 'lng': 121.52, + 'cityLevel': '市', + 'townLevel': '區', + }, + '970': { + 'city': '花蓮', + 'town': '花蓮', + 'lat': 23.99, + 'lng': 121.60, + 'cityLevel': '縣', + 'townLevel': '市', + }, +}); + +Future _regions({String? currentCode}) async { + SharedPreferences.setMockInitialValues({}); + final store = RegionStore(Prefs(await SharedPreferences.getInstance())); + if (currentCode != null) store.setCurrentCode(currentCode); + return store; +} + +Future _pumpPage(WidgetTester tester, RegionStore regions) async { + // Tall enough that the whole page is laid out — the sections and the + // calendar live below a 400 px hero, and a lazy list would not build them. + tester.view.physicalSize = const Size(800, 3000); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: _directory), + ChangeNotifierProvider.value(value: regions), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: MoonPage(), + ), + ), + ); + // The shader and the NASA maps load asynchronously; the rest of the page + // does not wait on them, which is the point of pumping rather than settling. + await tester.pump(); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('opens on today and offers no jump-to-now', (tester) async { + await _pumpPage(tester, await _regions(currentCode: '100')); + + final today = AppTime.utc8; + expect(find.text('${today.day}'), findsWidgets); + // The jump-back-to-now action only appears once the selection has left it. + expect(find.byTooltip('Now'), findsNothing); + }); + + testWidgets('names the township the rise and set times are for', ( + tester, + ) async { + await _pumpPage(tester, await _regions(currentCode: '970')); + expect(find.text('花蓮縣 花蓮市'), findsOneWidget); + }); + + testWidgets('falls back to a named township when location is unknown', ( + tester, + ) async { + // No GPS township: the page still has to say *where* the times apply, or + // a rise time is just a number. The fallback is the nearest township to + // Taipei, and it is named like any other. + await _pumpPage(tester, await _regions()); + expect(find.text('臺北市 中正區'), findsOneWidget); + }); + + testWidgets('a calendar day moves the selection, and offers a way back', ( + tester, + ) async { + await _pumpPage(tester, await _regions(currentCode: '100')); + + final today = AppTime.utc8; + // A day in the same month that is definitely not today. + final other = today.day == 1 ? 2 : 1; + await tester.tap( + find.descendant( + of: find.byType(MoonCalendar), + matching: find.text('$other'), + ), + ); + await tester.pump(); + + expect( + find.byTooltip('Now'), + findsOneWidget, + reason: 'the selection left the present, so a way back appears', + ); + + await tester.tap(find.byTooltip('Now')); + await tester.pump(); + expect(find.byTooltip('Now'), findsNothing); + }); + + testWidgets('shows distance, rise and set', (tester) async { + await _pumpPage(tester, await _regions(currentCode: '100')); + for (final label in ['Distance', 'Apparent size', 'Moonrise', 'Moonset']) { + expect(find.text(label), findsOneWidget, reason: label); + } + // The distance is a grouped number of kilometres, never a bare double. + expect(find.textContaining(RegExp(r'^3\d\d,\d\d\d km$')), findsOneWidget); + }); +} diff --git a/test/features/data/tonight_report_test.dart b/test/features/data/tonight_report_test.dart new file mode 100644 index 000000000..4317b1c8b --- /dev/null +++ b/test/features/data/tonight_report_test.dart @@ -0,0 +1,115 @@ +/// The tonight report's failure and empty states. +/// +/// The astronomy is pinned in `test/core/astro/`. What is checked here is the +/// thing that actually went wrong in practice: a missing asset produced a page +/// that rendered nothing, which looks exactly like "no passes tonight" and +/// exactly like "still loading". Each of those is now a distinct, testable +/// outcome. +library; + +import 'package:dpip/core/astro/satellite.dart'; +import 'package:dpip/core/astro/tle_source.dart'; +import 'package:dpip/core/astro/tonight_report.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _taipei = (latitude: 25.0330, longitude: 121.5654); +const _zone = Duration(hours: 8); + +/// The bundled snapshot, inlined so the test needs no asset bundle. +const _bundled = ''' +ISS (ZARYA) +1 25544U 98067A 26226.43871707 .00004555 00000+0 89427-4 0 9997 +2 25544 51.6329 11.7957 0007493 45.9133 314.2471 15.49439755580788 +CSS (TIANHE) +1 48274U 21035A 26224.98627525 .00000101 00000+0 54127-5 0 9991 +2 48274 41.4709 337.2096 0001079 250.4973 109.5748 15.58975796302033 +'''; + +class _FixedSource implements TleSource { + const _FixedSource(this.text); + final String text; + @override + Future> load() async => TleSet.parseAll(text); +} + +class _FailingSource implements TleSource { + const _FailingSource(); + @override + Future> load() async => throw StateError('asset missing'); +} + +Future _report(TleSource source, {DateTime? now}) { + final at = now ?? DateTime.utc(2026, 8, 14, 20); + final local = at.add(_zone); + return TonightReport.build( + DateTime.utc(local.year, local.month, local.day).subtract(_zone), + now: at, + latitude: _taipei.latitude, + longitude: _taipei.longitude, + source: source, + ); +} + +void main() { + test('finds the ISS passes that are actually there', () async { + final report = await _report(const _FixedSource(_bundled)); + expect(report.satellitesFailed, isFalse); + expect(report.passes, isNotEmpty); + expect(report.passes.map((p) => p.name), contains('ISS (ZARYA)')); + // Sorted, and every pass clears the display threshold. + for (var i = 1; i < report.passes.length; i++) { + expect( + report.passes[i].pass.rises.isAfter(report.passes[i - 1].pass.rises), + isTrue, + ); + } + }); + + test('a missing element set is reported, not rendered as "no passes"', () { + // The distinction the page turns into two different sentences. + return _report(const _FailingSource()).then((report) { + expect(report.satellitesFailed, isTrue); + expect(report.passes, isEmpty); + expect(report.elementAge, isNull); + // Everything that does not depend on the elements still resolves. + expect(report.night.astronomicalNight, isNotNull); + expect(report.targets, isNotEmpty); + }); + }); + + test('carries the element age, because that is what decays', () async { + final report = await _report(const _FixedSource(_bundled)); + expect(report.elementAge, isNotNull); + expect(report.elementAge!.inHours, inInclusiveRange(0, 48)); + }); + + test('an empty element file gives no passes and no failure', () async { + // A file that loads but holds nothing is not an error — it is a real, + // if unhelpful, answer, and must not be shown as a broken asset. + final report = await _report(const _FixedSource('')); + expect(report.satellitesFailed, isFalse); + expect(report.passes, isEmpty); + expect(report.elementAge, isNull); + }); + + test( + 'targets are above the usable altitude and sorted by brightness', + () async { + final report = await _report(const _FixedSource(_bundled)); + for (final sighting in report.targets) { + expect(sighting.altitude, greaterThan(usableAltitude)); + } + for (var i = 1; i < report.targets.length; i++) { + expect( + report.targets[i].object.magnitude, + greaterThanOrEqualTo(report.targets[i - 1].object.magnitude), + ); + } + }, + ); + + test('the August Perseids show up as an active shower', () async { + final report = await _report(const _FixedSource(_bundled)); + expect(report.showers.map((s) => s.shower.id), contains('perseids')); + }); +} diff --git a/test/features/earthquake/intensity_icon_renderer_test.dart b/test/features/earthquake/intensity_icon_renderer_test.dart new file mode 100644 index 000000000..68182b0ae --- /dev/null +++ b/test/features/earthquake/intensity_icon_renderer_test.dart @@ -0,0 +1,83 @@ +/// The locally rendered intensity markers must keep the legacy geometry: a +/// full-bleed shell (white, or black in dark mode), an inner rounded square in +/// the discrete intensity colour, and the level digit (black on the yellow / +/// orange badges 4–5, white elsewhere). `cross` is the red × station marker. +library; + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:dpip/features/earthquake/presentation/widgets/intensity_icon_renderer.dart'; +import 'package:dpip/shared/seismic/intensity_colors.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + Future decode(Uint8List bytes) async { + final codec = await ui.instantiateImageCodec(bytes); + final frame = await codec.getNextFrame(); + return frame.image; + } + + (int, int, int, int) px(ui.Image image, ByteData raw, int x, int y) { + final i = (y * image.width + x) * 4; + return ( + raw.getUint8(i), + raw.getUint8(i + 1), + raw.getUint8(i + 2), + raw.getUint8(i + 3), + ); + } + + test('badges keep the legacy geometry', () async { + final icons = await IntensityIconRenderer.renderAll(); + final white = const Color(0xFFFFFFFF).toARGB32(); + final black = const Color(0xFF000000).toARGB32(); + for (final name in IntensityIconRenderer.names) { + final image = await decode(icons[name]!); + final raw = (await image.toByteData())!; + if (name == 'cross') { + expect(image.width, 96, reason: name); + final centre = px(image, raw, 48, 48); + expect( + centre.$1 > 150 && centre.$2 < 120, + isTrue, + reason: 'cross centre should be red', + ); + // Rounded corners stay transparent. + expect(px(image, raw, 0, 0).$4, lessThan(32), reason: 'cross corner'); + continue; + } + expect(image.width, 64, reason: name); + final level = int.parse(name.split('-')[1]); + final dark = name.endsWith('-dark'); + final shell = dark ? black : white; + final badge = IntensityColors.discrete(level).toARGB32(); + final digit = ((level == 4 || level == 5) ? black : white) & 0xFFFFFF; + + // Shell rim, badge fill, digit, and a transparent rounded corner. + final rim = px(image, raw, 2, 32); + expect(rim.$4 > 200, isTrue, reason: '$name rim opaque'); + expect( + (rim.$1 << 16) | (rim.$2 << 8) | rim.$3, + shell & 0xFFFFFF, + reason: '$name rim colour', + ); + final fill = px(image, raw, 10, 32); + expect( + (fill.$1 << 16) | (fill.$2 << 8) | fill.$3, + badge & 0xFFFFFF, + reason: '$name badge colour', + ); + final d = px(image, raw, 32, 32); + expect( + (d.$1 << 16) | (d.$2 << 8) | d.$3, + digit, + reason: '$name digit colour', + ); + expect(px(image, raw, 0, 0).$4, lessThan(32), reason: '$name corner'); + } + }); +} diff --git a/test/features/events/event_mapping_test.dart b/test/features/events/event_mapping_test.dart index 6e247fe4a..cf1c5acc4 100644 --- a/test/features/events/event_mapping_test.dart +++ b/test/features/events/event_mapping_test.dart @@ -46,13 +46,16 @@ void main() { expect(event.description, isNot(contains('三峽'))); }); - test('a township with no entry of its own gets no borrowed description', () { - // 999 is not in the map; `all` is one specific township here, so naming it - // would be wrong. The row degrades to its title instead. - final event = Event.fromJson(_heavyRain(), regionCode: '999')!; - expect(event.description, isNot(contains('永康'))); - expect(event.title, '大雨特報'); - }); + test( + 'a township with no entry of its own gets no borrowed description', + () { + // 999 is not in the map; `all` is one specific township here, so naming it + // would be wrong. The row degrades to its title instead. + final event = Event.fromJson(_heavyRain(), regionCode: '999')!; + expect(event.description, isNot(contains('永康'))); + expect(event.title, '大雨特報'); + }, + ); test('a genuinely global entry is still used as the fallback', () { final json = _heavyRain(); diff --git a/test/features/home/weather_sky/card_water_pipeline_test.dart b/test/features/home/weather_sky/card_water_pipeline_test.dart index 2a5947cf4..c5ba503fd 100644 --- a/test/features/home/weather_sky/card_water_pipeline_test.dart +++ b/test/features/home/weather_sky/card_water_pipeline_test.dart @@ -101,15 +101,15 @@ void main() { ); // rawRgba is premultiplied — the stored channel values, no division. - final outPx = (await image.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); - final posPx = (await pos.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); - final negPx = (await neg.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); + final outPx = (await image.toByteData(format: ui.ImageByteFormat.rawRgba))! + .buffer + .asUint8List(); + final posPx = (await pos.toByteData(format: ui.ImageByteFormat.rawRgba))! + .buffer + .asUint8List(); + final negPx = (await neg.toByteData(format: ui.ImageByteFormat.rawRgba))! + .buffer + .asUint8List(); // Float reference — the reference shader's rain branch with the self-aliased // ambient, evaluated at texel centres where the shader's bilinear @@ -238,12 +238,12 @@ void main() { kernel: kernel, normalMap: normalMap, ); - final posPx = (await pos.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); - final negPx = (await neg.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); + final posPx = (await pos.toByteData(format: ui.ImageByteFormat.rawRgba))! + .buffer + .asUint8List(); + final negPx = (await neg.toByteData(format: ui.ImageByteFormat.rawRgba))! + .buffer + .asUint8List(); int at(Uint8List px, int x, int y, int c) => px[(y * 64 + x) * 4 + c]; const c = 32; diff --git a/test/features/home/weather_sky/weather_sky_background_test.dart b/test/features/home/weather_sky/weather_sky_background_test.dart new file mode 100644 index 000000000..df3392c56 --- /dev/null +++ b/test/features/home/weather_sky/weather_sky_background_test.dart @@ -0,0 +1,59 @@ +import 'package:dpip/features/home/presentation/widgets/weather_sky/weather_sky_background.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// The animated sky must not repaint when it is stopped: the scroll-blur +/// subtree above it rebuilds on every scroll tick while the sky is frozen +/// underneath, and a fresh painter instance every tick would re-render the +/// whole full-screen shader stack for no visible change. +void main() { + testWidgets('a stopped backdrop reuses its painter across rebuilds', ( + tester, + ) async { + await pumpLoaded( + tester, + const MaterialApp(home: WeatherSkyBackground(active: false)), + ); + + CustomPainter painterOf() => tester + .widget( + find.descendant( + of: find.byType(WeatherSkyBackground), + matching: find.byType(CustomPaint), + ), + ) + .painter!; + + final first = painterOf(); + // An identical rebuild of the widget (what a scroll tick above produces) + // must not swap the painter. + await tester.pumpWidget( + const MaterialApp(home: WeatherSkyBackground(active: false)), + ); + await tester.pump(); + expect(identical(first, painterOf()), isTrue); + + // Re-activating the animation advances the clock, so the next frame gets a + // fresh painter again. + await tester.pumpWidget( + const MaterialApp(home: WeatherSkyBackground(active: true)), + ); + await tester.pump(const Duration(milliseconds: 100)); + expect(identical(first, painterOf()), isFalse); + }); +} + +/// Pumps [widget] and waits for the backdrop's shader/sprite load — a real +/// async load that the fake test clock cannot advance on its own. +Future pumpLoaded(WidgetTester tester, Widget widget) async { + await tester.pumpWidget(widget); + final sky = find.byType(WeatherSkyBackground); + final paint = find.descendant(of: sky, matching: find.byType(CustomPaint)); + for (var i = 0; i < 40 && tester.widgetList(paint).isEmpty; i++) { + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 50)), + ); + await tester.pump(); + } + expect(paint, findsOneWidget, reason: 'the sky shaders should load'); +} diff --git a/test/features/map/presentation/layers/typhoon_weather_chrome_test.dart b/test/features/map/presentation/layers/typhoon_weather_chrome_test.dart new file mode 100644 index 000000000..6dcb2fcf2 --- /dev/null +++ b/test/features/map/presentation/layers/typhoon_weather_chrome_test.dart @@ -0,0 +1,229 @@ +/// The typhoon weather underlay's chrome: which border look each underlay +/// draws, and that switching underlays never leaves one behind. +library; + +import 'package:dpip/core/error/result.dart'; +import 'package:dpip/features/map/presentation/layers/typhoon_layer.dart'; +import 'package:dpip/features/map/presentation/layers/typhoon_weather_overlay.dart'; +import 'package:dpip/features/typhoon/domain/meteor_typhoon_repository.dart'; +import 'package:dpip/features/typhoon/domain/typhoon_cyclone.dart'; +import 'package:dpip/features/typhoon/domain/typhoon_potential.dart'; +import 'package:dpip/features/typhoon/domain/typhoon_probability.dart'; +import 'package:dpip/features/typhoon/domain/typhoon_track.dart'; +import 'package:dpip/features/typhoon/domain/typhoon_warning.dart'; +import 'package:dpip/features/weather/domain/radar_repository.dart'; +import 'package:dpip/features/weather/domain/satellite_repository.dart'; +import 'package:dpip/shared/map/admin_outline.dart'; +import 'package:dpip/shared/map/map_style.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../raster_timeline_harness.dart'; + +/// One active cyclone, so the layer gets a bulletin time and a track to draw. +class _FakeTyphoonRepository implements MeteorTyphoonRepository { + @override + Future> cyclones() async => Ok( + const CycloneIndex( + updated: 1700000000, + cyclones: [ + TyphoonCyclone( + name: 'X', + year: 2026, + tdNo: '1', + time: 1700000000, + latitude: 21, + longitude: 121, + ), + ], + ), + ); + + @override + Future> track() async => Ok( + TrackPayload( + updated: 1700000000, + cyclones: [ + const TyphoonTrack( + name: 'X', + year: 2026, + tdNo: '1', + analysis: [ + TrackFix(time: 1700000000 - 3600, latitude: 20, longitude: 120), + ], + forecast: [], + ), + ], + ), + ); + + @override + Future> potential() async => + Ok(const PotentialPayload(updated: 1, cyclones: [])); + + @override + Future> probability() async => + Ok(const TyphoonProbability(updated: 1, cyclones: [])); + + @override + Future> warning() async => + Ok(const WarningPayload(updated: 1, cyclones: [])); + + @override + dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError(); +} + +class _FakeRadarRepository extends FakeRasterFrameSource + implements RadarRepository { + _FakeRadarRepository() : super(['1700000000']); + + @override + String tileUrl(String frame) => 'https://host/radar/$frame/{z}/{x}/{y}.webp'; +} + +class _FakeSatelliteRepository extends FakeRasterFrameSource + implements SatelliteRepository { + _FakeSatelliteRepository() : super(['1700000000']); + + @override + String tileUrl(String frame) => 'https://host/sat/$frame/{z}/{x}/{y}.png'; + + @override + void setStyle(String? style) {} +} + +TyphoonMapLayer _layer() => TyphoonMapLayer( + _FakeTyphoonRepository(), + radar: _FakeRadarRepository(), + satellite: _FakeSatelliteRepository(), +); + +/// Renders the layer (bulletin set, weather overlay defaulted to radar) and +/// lets its queued ops settle. +Future _rendered(TyphoonMapLayer layer) async { + final controller = RecordingMapController(); + await layer.render(controller); + for (var i = 0; i < 8; i++) { + await Future.delayed(Duration.zero); + } + return controller; +} + +Future _drain() async { + for (var i = 0; i < 8; i++) { + await Future.delayed(Duration.zero); + } +} + +void main() { + test('radar underlay draws the shared white cased county frame', () async { + final layer = _layer(); + final controller = await _rendered(layer); + + expect( + controller.calls, + containsAll([ + 'addLineLayer:${AdminBoundary.county.casingLayerId}', + 'addLineLayer:${AdminBoundary.county.lineLayerId}', + ]), + ); + expect( + controller.lineColorOf(AdminBoundary.county.lineLayerId), + AdminOutline.lineColor, + reason: 'the radar underlay keeps the default white core', + ); + expect( + controller.calls, + isNot(contains('addLineLayer:$satelliteCountyOutlineLayerId')), + ); + }); + + test( + 'satellite underlay swaps the county frame for the bare yellow line', + () async { + final layer = _layer(); + final controller = await _rendered(layer); + + controller.calls.clear(); + layer.setWeatherOverlay(TyphoonWeatherOverlay.satellite); + await _drain(); + + expect( + controller.calls, + contains('addLineLayer:$satelliteCountyOutlineLayerId'), + reason: + 'the IR underlay draws its county frame like the standalone ' + 'B13 layer — one bare line, no casing', + ); + expect( + controller.lineColorOf(satelliteCountyOutlineLayerId), + satelliteOutlineColor, + ); + expect( + controller.belowOf(satelliteCountyOutlineLayerId), + isNotNull, + reason: 'the frame still sits under the typhoon vectors', + ); + expect( + controller.calls, + isNot(contains('addLineLayer:${AdminBoundary.county.casingLayerId}')), + reason: 'no black casing is drawn under the IR image', + ); + expect( + controller.calls, + containsAll([ + 'removeLayer:${AdminBoundary.county.casingLayerId}', + 'removeLayer:${AdminBoundary.county.lineLayerId}', + ]), + reason: 'the radar look is taken down when the underlay switches', + ); + }, + ); + + test('switching back to radar removes the satellite county frame', () async { + final layer = _layer(); + final controller = await _rendered(layer); + + layer.setWeatherOverlay(TyphoonWeatherOverlay.satellite); + await _drain(); + controller.calls.clear(); + + layer.setWeatherOverlay(TyphoonWeatherOverlay.radar); + await _drain(); + + expect( + controller.calls, + contains('removeLayer:$satelliteCountyOutlineLayerId'), + reason: 'the bare yellow line must not survive over the radar echo', + ); + expect( + controller.calls, + contains('addLineLayer:${AdminBoundary.county.casingLayerId}'), + reason: 'the cased white frame comes back with the radar underlay', + ); + }); + + test('the county toggle removes whichever look is on', () async { + final layer = _layer(); + final controller = await _rendered(layer); + + layer.setWeatherOverlay(TyphoonWeatherOverlay.satellite); + await _drain(); + controller.calls.clear(); + + layer.setShowCountyOutline(false); + await _drain(); + + expect( + controller.calls, + contains('removeLayer:$satelliteCountyOutlineLayerId'), + reason: 'turning the county frame off over IR takes the bare line down', + ); + expect( + controller.calls, + contains('removeLayer:${AdminBoundary.county.lineLayerId}'), + reason: + 'the cased ids are torn down too — the removal is unconditional ' + 'because the teardown cannot know which look was drawn', + ); + }); +} diff --git a/test/features/map/presentation/widgets/mesh_node_sheet_test.dart b/test/features/map/presentation/widgets/mesh_node_sheet_test.dart new file mode 100644 index 000000000..e10ba5afc --- /dev/null +++ b/test/features/map/presentation/widgets/mesh_node_sheet_test.dart @@ -0,0 +1,106 @@ +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:dpip/features/map/presentation/widgets/mesh_node_sheet.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../../core/meshtastic/fake_mesh_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + var clock = DateTime.utc(2026, 1, 1, 12); + + MeshNode node(int num, {double snr = 0, int? battery}) => MeshNode( + num: num, + displayName: 'repeater', + isOnline: true, + batteryLevel: battery, + lastHeard: clock, + latitude: 24.0, + longitude: 121.6, + snr: snr, + ); + + Future<(MeshNodeStore, FakeMeshService)> makeStore() async { + clock = DateTime.utc(2026, 1, 1, 12); + SharedPreferences.setMockInitialValues({}); + final service = FakeMeshService(); + final store = MeshNodeStore( + service, + Prefs(await SharedPreferences.getInstance()), + now: () => clock, + )..start(); + return (store, service); + } + + Widget wrap(MeshNodeStore store) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: MeshNodeSheet( + store: store, + selected: ValueNotifier(1), + selectionRevision: ValueNotifier(0), + onClose: () {}, + ), + ), + ); + + testWidgets('no trends until two distinct readings exist', (tester) async { + final (store, service) = await makeStore(); + service.nodes.add(node(1, snr: -5)); + await tester.pump(); + + await tester.pumpWidget(wrap(store)); + await tester.pump(); + + expect(find.text('Signal trend (SNR)'), findsNothing); + + // Let the store's debounced persist fire. + await tester.pump(const Duration(seconds: 2, milliseconds: 100)); + }); + + testWidgets('renders SNR and battery trends once history builds', ( + tester, + ) async { + final (store, service) = await makeStore(); + service.nodes + ..add(node(1, snr: -8, battery: 90)) + ..add(node(1, snr: -6, battery: 88)); + await tester.pump(); + + await tester.pumpWidget(wrap(store)); + await tester.pump(); + + expect(find.text('Signal trend (SNR)'), findsOneWidget); + expect(find.text('Battery trend'), findsOneWidget); + // Current-value readouts sit on the trend headers. + expect(find.textContaining(' dB'), findsWidgets); + expect(find.textContaining('%'), findsWidgets); + + // Let the store's debounced persist fire. + await tester.pump(const Duration(seconds: 2, milliseconds: 100)); + }); + + testWidgets('battery trend skips an externally powered node', (tester) async { + final (store, service) = await makeStore(); + // 101 is "plugged in", not a charge — no trend to draw. + service.nodes + ..add(node(1, snr: -8, battery: 101)) + ..add(node(1, snr: -6, battery: 101)); + await tester.pump(); + + await tester.pumpWidget(wrap(store)); + await tester.pump(); + + expect(find.text('Signal trend (SNR)'), findsOneWidget); + expect(find.text('Battery trend'), findsNothing); + + // Let the store's debounced persist fire. + await tester.pump(const Duration(seconds: 2, milliseconds: 100)); + }); +} diff --git a/test/features/map/radar_layer_test.dart b/test/features/map/radar_layer_test.dart index 1aa3c8dd0..a80fb2715 100644 --- a/test/features/map/radar_layer_test.dart +++ b/test/features/map/radar_layer_test.dart @@ -312,11 +312,16 @@ void main() { // The base style draws its own borders under the raster. Leaving the echo // beneath them meant they always showed through, so the switchable copies // on top would have been a second set at a second weight — and switching - // them off would still not have given a clean raster. + // them off would still not have given a clean raster. The raster anchors + // just under the township labels, so place names are never buried. for (final call in controller.calls.where( (c) => c.startsWith('addRasterLayer:'), )) { - expect(controller.belowOf(call.split(':').last), isNull, reason: call); + expect( + controller.belowOf(call.split(':').last), + townLabelLayerId, + reason: call, + ); } }); diff --git a/test/features/map/raster_timeline_harness.dart b/test/features/map/raster_timeline_harness.dart index 69cac0e2e..c10e373e2 100644 --- a/test/features/map/raster_timeline_harness.dart +++ b/test/features/map/raster_timeline_harness.dart @@ -48,6 +48,10 @@ abstract class FakeRasterFrameSource implements RasterFrameSource { /// Records the MapLibre calls a layer makes, and the last state of each layer. class RecordingMapController implements MapLibreMapController { + RecordingMapController({CameraPosition? camera}) + : _camera = + camera ?? const CameraPosition(target: LatLng(23.5, 121), zoom: 7); + final List calls = []; /// Property keys of each `setLayerProperties` call, in order — what actually @@ -91,6 +95,7 @@ class RecordingMapController implements MapLibreMapController { double? maxzoom, }) async { calls.add('addRasterLayer:$layerId'); + below[layerId] = belowLayerId; mountTransitions[layerId] = properties .toJson()['raster-opacity-transition']; _record(layerId, properties); @@ -192,10 +197,14 @@ class RecordingMapController implements MapLibreMapController { northeast: const LatLng(25, 122), ); + /// Camera the [cameraPosition] getter reports — a wind-overlay test can + /// zoom out to put more of the field in view (the default z7 viewport holds + /// only a handful of particles, too few to assert anything about). + final CameraPosition _camera; + @override - CameraPosition? get cameraPosition => - const CameraPosition(target: LatLng(23.5, 121), zoom: 7); + CameraPosition? get cameraPosition => _camera; @override - dynamic noSuchMethod(Invocation invocation) => null; + dynamic noSuchMethod(Invocation invocation) => Future.value(); } diff --git a/test/features/map/satellite_layer_test.dart b/test/features/map/satellite_layer_test.dart index 8f85618a2..0d42e74cb 100644 --- a/test/features/map/satellite_layer_test.dart +++ b/test/features/map/satellite_layer_test.dart @@ -74,47 +74,44 @@ void main() { }, ); - test( - 'bright yellow county and town outlines are added once and removed on clear', - () async { - final layer = SatelliteMapLayer( - _FakeSatelliteRepository(_ids(5)), - channel: SatelliteChannel.irClean, - ); - final frames = (await layer.frames()).valueOrNull!; - final controller = RecordingMapController(); + test('bright yellow county and town outlines are added once and removed on clear', () async { + final layer = SatelliteMapLayer( + _FakeSatelliteRepository(_ids(5)), + channel: SatelliteChannel.irClean, + ); + final frames = (await layer.frames()).valueOrNull!; + final controller = RecordingMapController(); - await layer.prepare(controller, frames); - await layer.show(controller, frames[2]); - await layer.show(controller, frames[0]); + await layer.prepare(controller, frames); + await layer.show(controller, frames[2]); + await layer.show(controller, frames[0]); - expect( - controller.calls - .where((c) => c == 'addLineLayer:$satelliteCountyOutlineLayerId') - .length, - 1, - reason: 'a second settle must not re-add the outlines', - ); - expect( - controller.calls, - isNot(contains('addLineLayer:$satelliteGlobalOutlineLayerId')), - reason: 'the 國界 border ships off by default', - ); + expect( + controller.calls + .where((c) => c == 'addLineLayer:$satelliteCountyOutlineLayerId') + .length, + 1, + reason: 'a second settle must not re-add the outlines', + ); + expect( + controller.calls, + contains('addLineLayer:$satelliteGlobalOutlineLayerId'), + reason: 'the 國界 border ships on by default', + ); - controller.calls.clear(); - await layer.clear(controller); - expect( - controller.calls, - containsAll([ - 'removeLayer:$satelliteTownOutlineLayerId', - 'removeLayer:$satelliteCountyOutlineLayerId', - 'removeLayer:$satelliteGlobalOutlineLayerId', - ]), - ); - }, - ); + controller.calls.clear(); + await layer.clear(controller); + expect( + controller.calls, + containsAll([ + 'removeLayer:$satelliteTownOutlineLayerId', + 'removeLayer:$satelliteCountyOutlineLayerId', + 'removeLayer:$satelliteGlobalOutlineLayerId', + ]), + ); + }); - test('the 國界 border draws only when asked', () async { + test('the 國界 border toggles on and off', () async { final layer = SatelliteMapLayer( _FakeSatelliteRepository(_ids(5)), channel: SatelliteChannel.irClean, @@ -126,23 +123,23 @@ void main() { await layer.show(controller, frames[2]); controller.calls.clear(); - layer.setShowGlobalOutline(true); + layer.setShowGlobalOutline(false); for (var i = 0; i < 5; i++) { await Future.delayed(Duration.zero); } expect( controller.calls, - contains('addLineLayer:$satelliteGlobalOutlineLayerId'), + contains('removeLayer:$satelliteGlobalOutlineLayerId'), ); controller.calls.clear(); - layer.setShowGlobalOutline(false); + layer.setShowGlobalOutline(true); for (var i = 0; i < 5; i++) { await Future.delayed(Duration.zero); } expect( controller.calls, - contains('removeLayer:$satelliteGlobalOutlineLayerId'), + contains('addLineLayer:$satelliteGlobalOutlineLayerId'), ); }); diff --git a/test/features/map/wind_particle_sim_test.dart b/test/features/map/wind_particle_sim_test.dart index 4e1cf7414..332f2305f 100644 --- a/test/features/map/wind_particle_sim_test.dart +++ b/test/features/map/wind_particle_sim_test.dart @@ -108,7 +108,7 @@ void main() { expect(sim.particles.first.x - x0, lessThan(1e-3)); expect(sim.particles.first.y - y0, lessThan(1e-3)); expect( - sim.particles.every((p) => p.screen != null), + sim.particles.every((p) => p.visible), isTrue, reason: 'every seeded particle sits in the viewport, so all stamp', ); @@ -144,8 +144,8 @@ void main() { ..y = -1.5; // above the field's north edge, off the grid sim.step(_camera, _size); expect( - out.screen, - isNull, + out.visible, + isFalse, reason: 'off the grid is nothing to stamp, whatever happens next', ); // Recycling is rejection-sampled, so a candidate in still air is turned @@ -180,7 +180,7 @@ void main() { for (var i = 0; i < 5; i++) { sim.step(_camera, _size); } - return sim.particles.where((p) => p.screen != null).length; + return sim.particles.where((p) => p.visible).length; } expect(streaks(180), streaks(0)); diff --git a/test/features/meshtastic/byte_limit_formatter_test.dart b/test/features/meshtastic/byte_limit_formatter_test.dart new file mode 100644 index 000000000..52a85be0d --- /dev/null +++ b/test/features/meshtastic/byte_limit_formatter_test.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:dpip/features/meshtastic/presentation/pages/meshtastic_page.dart'; + +/// Drives the real composer through a widget, since the formatter it uses is +/// private to the page. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('the budget keeps the documented 5% headroom', () { + expect( + MeshPorts.maxTextBytes, + (MeshPorts.maxPayloadBytes * (1 - MeshPorts.payloadHeadroom)).floor(), + ); + expect(MeshPorts.maxTextBytes, 221); + }); + + testWidgets('caps input by bytes, not characters', (tester) async { + final controller = TextEditingController(); + addTearDown(controller.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TextField( + controller: controller, + inputFormatters: composerInputFormatters, + ), + ), + ), + ); + + // 100 Chinese characters = 300 UTF-8 bytes: a character-based cap would + // have accepted every one of them. + await tester.enterText(find.byType(TextField), '測' * 100); + final kept = controller.text; + expect(utf8.encode(kept).length, lessThanOrEqualTo(MeshPorts.maxTextBytes)); + expect(kept.characters.length, 73); // 73 × 3 bytes = 219 + }); + + testWidgets('fills the budget exactly with single-byte text', (tester) async { + final controller = TextEditingController(); + addTearDown(controller.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TextField( + controller: controller, + inputFormatters: composerInputFormatters, + ), + ), + ), + ); + + await tester.enterText(find.byType(TextField), 'a' * 300); + expect(controller.text.length, MeshPorts.maxTextBytes); + }); + + testWidgets('never splits an emoji in half', (tester) async { + final controller = TextEditingController(); + addTearDown(controller.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TextField( + controller: controller, + inputFormatters: composerInputFormatters, + ), + ), + ), + ); + + // 4 bytes each; 56 of them is 224 bytes, so the cut lands mid-run. + await tester.enterText(find.byType(TextField), '😀' * 56); + expect( + utf8.encode(controller.text).length, + lessThanOrEqualTo(MeshPorts.maxTextBytes), + ); + // A truncation that cut code units would leave a lone surrogate. + expect(controller.text.contains('�'), isFalse); + expect(controller.text.characters.every((c) => c == '😀'), isTrue); + }); +} diff --git a/test/features/meshtastic/mesh_chat_controller_test.dart b/test/features/meshtastic/mesh_chat_controller_test.dart new file mode 100644 index 000000000..68ba44e0f --- /dev/null +++ b/test/features/meshtastic/mesh_chat_controller_test.dart @@ -0,0 +1,157 @@ +import 'package:dpip/core/error/failure.dart'; +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:dpip/features/meshtastic/presentation/mesh_chat_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import '../../core/meshtastic/fake_mesh_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(sqfliteFfiInit); + + late Database db; + + MeshMessage message(String text, {int from = 1, int seconds = 0}) => + MeshMessage( + from: from, + channel: 0, + text: text, + timestamp: DateTime.utc(2026, 1, 1).add(Duration(seconds: seconds)), + ); + + /// A controller over a fresh in-memory database, or over [reuse] to model a + /// restart against the same storage. + Future<(MeshChatController, FakeMeshService, MeshStore)> makeController([ + MeshStore? reuse, + ]) async { + SharedPreferences.setMockInitialValues({}); + final service = FakeMeshService(); + final prefs = Prefs(await SharedPreferences.getInstance()); + MeshStore store; + if (reuse != null) { + store = reuse; + } else { + db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath); + await MeshStore.createSchema(db); + store = MeshStore(db); + } + final controller = MeshChatController( + service, + MeshLink(service, prefs), + MeshNodeStore(service, prefs)..start(), + store, + ); + await Future.delayed(const Duration(milliseconds: 150)); + return (controller, service, store); + } + + tearDown(() async => db.close()); + + /// Lets the fire-and-forget SQLite writes land — they cross an isolate, so + /// a bare microtask drain isn't enough. + Future settle() => + Future.delayed(const Duration(milliseconds: 150)); + + test('keeps the newest messages first', () async { + final (controller, service, _) = await makeController(); + for (var i = 0; i < 10; i++) { + service.messages.add(message('m$i', seconds: i)); + } + await settle(); + + expect(controller.messages, hasLength(10)); + expect(controller.messages.first.text, 'm9'); + expect(controller.messages.last.text, 'm0'); + }); + + test('holds only a window of the log in memory', () async { + final (controller, service, store) = await makeController(); + for (var i = 0; i < MeshChatController.windowSize + 20; i++) { + service.messages.add(message('m$i', seconds: i)); + } + await settle(); + + expect(controller.messages, hasLength(MeshChatController.windowSize)); + // The store keeps everything — the window is a view, not a retention cap. + expect( + await store.messages(limit: 10000), + hasLength(MeshChatController.windowSize + 20), + ); + }); + + test('persists the log and reloads it after a restart', () async { + final (controller, service, store) = await makeController(); + service.messages.add(message('hello')); + await settle(); + controller.dispose(); + + final (restored, _, _) = await makeController(store); + expect(restored.messages.single.text, 'hello'); + expect(restored.messages.single.outgoing, isFalse); + }); + + test('drops a message the log already holds', () async { + final (controller, service, _) = await makeController(); + service.messages + ..add(message('same')) + ..add(message('same')); + await settle(); + + expect(controller.messages, hasLength(1)); + }); + + test('records a sent message as outgoing, and not a failed one', () async { + final (controller, service, _) = await makeController(); + + expect(await controller.send(' hi '), isNull); + await settle(); + expect(service.sentText, ['hi']); + expect(controller.messages.single.text, 'hi'); + expect(controller.messages.single.outgoing, isTrue); + + service.sendFailure = const UnexpectedFailure('radio busy'); + expect(await controller.send('nope'), 'radio busy'); + await settle(); + expect(controller.messages, hasLength(1)); + }); + + test('sends on the channel it is given and records it there', () async { + final (controller, service, _) = await makeController(); + + expect(await controller.send('hi', channel: 3), isNull); + await settle(); + + expect(service.sentChannels, [3]); + expect(controller.messages.single.channel, 3); + }); + + test('counts stored messages per channel', () async { + final (controller, service, _) = await makeController(); + service.messages + ..add(message('a', seconds: 1)) + ..add(message('b', seconds: 2)); + await settle(); + await controller.send('mine', channel: 3); + await settle(); + + expect(controller.messageCountsByChannel, {0: 2, 3: 1}); + }); + + test('clearMessages empties the log and its storage', () async { + final (controller, service, store) = await makeController(); + service.messages.add(message('bye')); + await settle(); + + controller.clearMessages(); + await settle(); + + expect(controller.messages, isEmpty); + expect(await store.messages(), isEmpty); + }); +} diff --git a/test/features/meshtastic/meshtastic_page_channel_test.dart b/test/features/meshtastic/meshtastic_page_channel_test.dart new file mode 100644 index 000000000..a967424e0 --- /dev/null +++ b/test/features/meshtastic/meshtastic_page_channel_test.dart @@ -0,0 +1,120 @@ +/// Regression test for channels bleeding into each other while disconnected. +/// +/// The page used to skip filtering when the radio hadn't reported its channel +/// table — so with no link, every channel's messages appeared in one list. +/// Channels are separate conversations; interleaving them is wrong however +/// little else is known about the radio. +library; + +import 'package:dpip/core/meshtastic/data/mesh_store.dart'; +import 'package:dpip/core/meshtastic/domain/meshtastic_service.dart'; +import 'package:dpip/core/meshtastic/mesh_alerts.dart'; +import 'package:dpip/core/meshtastic/mesh_link.dart'; +import 'package:dpip/core/meshtastic/mesh_node_store.dart'; +import 'package:dpip/core/settings/prefs.dart'; +import 'package:dpip/features/meshtastic/presentation/mesh_chat_controller.dart'; +import 'package:dpip/features/meshtastic/presentation/pages/meshtastic_page.dart'; +import 'package:dpip/l10n/gen/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import '../../core/meshtastic/fake_mesh_service.dart'; + +void main() { + setUpAll(sqfliteFfiInit); + + MeshStoredMessage stored(String text, int channel, int seconds) => + MeshStoredMessage( + from: 1, + channel: channel, + text: text, + timestamp: DateTime.utc(2026, 1, 1).add(Duration(seconds: seconds)), + outgoing: false, + ); + + Future pumpPage(WidgetTester tester) async { + tester.view.physicalSize = const Size(900, 1800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + late MeshChatController controller; + late MeshLink link; + late MeshAlerts alerts; + late FakeMeshService service; + // Real I/O: SQLite runs off the test's fake-async zone, so anything that + // touches it must happen inside `runAsync` or its futures never complete + // and the test simply hangs. + await tester.runAsync(() async { + SharedPreferences.setMockInitialValues({}); + final prefs = Prefs(await SharedPreferences.getInstance()); + final db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath); + addTearDown(db.close); + await MeshStore.createSchema(db); + final store = MeshStore(db); + await store.addMessage(stored('on the primary', 0, 2)); + await store.addMessage(stored('on the secondary', 3, 1)); + + // Disconnected on purpose: no channel table, the case that broke. + service = FakeMeshService(); + link = MeshLink(service, prefs); + alerts = MeshAlerts(service, prefs, post: (_) async {}); + controller = MeshChatController( + service, + link, + MeshNodeStore(service, prefs)..start(), + store, + ); + // Let the controller's initial load land before the first frame. + await Future.delayed(const Duration(milliseconds: 200)); + }); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: service), + ChangeNotifierProvider.value(value: link), + ChangeNotifierProvider.value(value: alerts), + ChangeNotifierProvider.value(value: controller), + ], + child: const MaterialApp( + locale: Locale('zh', 'TW'), + localizationsDelegates: [ + ...AppLocalizations.localizationsDelegates, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: MeshtasticPage(), + ), + ), + ); + await tester.pump(); + } + + testWidgets('shows one channel at a time while disconnected', (tester) async { + await pumpPage(tester); + + expect(find.text('on the primary'), findsOneWidget); + expect(find.text('on the secondary'), findsNothing); + }); + + testWidgets('still offers the channels the log knows about', (tester) async { + await pumpPage(tester); + + // The radio has told us nothing, but the stored log has: two channels. + await tester.tap(find.text('CH0')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + await tester.tap(find.text('CH3').last); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + + expect(find.text('on the secondary'), findsOneWidget); + expect(find.text('on the primary'), findsNothing); + }); +} diff --git a/test/shaders/moon_shader_test.dart b/test/shaders/moon_shader_test.dart new file mode 100644 index 000000000..3b336fe75 --- /dev/null +++ b/test/shaders/moon_shader_test.dart @@ -0,0 +1,207 @@ +/// Raster-level pins for the moon shader. +/// +/// `moon_display.frag` projects NASA's lunar maps onto a sphere and lights +/// them by phase. What must hold, or the whole "moon phase" feature lies: +/// dark at new, bright at full, right-lit at first quarter — the terminator +/// has to land where the phase angle puts it. On top of that, two things that +/// separate a sphere from a printed disc are pinned here because they are easy +/// to lose in a refactor: nothing is drawn outside the disc, and a full moon +/// stays bright out to the limb instead of falling off like a Lambert ball. +library; + +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; + +const int _size = 128; + +Future _asset(String key) async { + final data = await rootBundle.load(key); + final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final frame = await codec.getNextFrame(); + codec.dispose(); + return frame.image; +} + +Future _renderPhase( + ui.Image color, + ui.Image height, + double phase, { + double librationLongitude = 0, +}) async { + final program = await ui.FragmentProgram.fromAsset( + 'shaders/weather/moon_display.frag', + ); + final shader = program.fragmentShader(); + shader.setFloat(0, _size.toDouble()); + shader.setFloat(1, _size.toDouble()); + shader.setFloat(2, phase); + shader.setFloat(3, librationLongitude); + shader.setFloat(4, 0); + // Canonical frame: the Moon's pole straight up, the lit limb to the right — + // the orientation these luminance pins were written against. The observer's + // real tilt is exercised in `moon_orientation_test.dart`. + shader.setFloat(5, 0); + shader.setFloat(6, 3.14159265 / 2); + shader.setImageSampler(0, color); + shader.setImageSampler(1, height); + final recorder = ui.PictureRecorder(); + ui.Canvas(recorder).drawRect( + Offset.zero & Size.square(_size.toDouble()), + Paint()..shader = shader, + ); + final picture = recorder.endRecording(); + final image = picture.toImageSync(_size, _size); + picture.dispose(); + shader.dispose(); + return image; +} + +/// Mean luminance over a rectangle, ignoring fully transparent pixels. +Future _luminance( + ui.Image image, { + double left = 0, + double right = 1, +}) async { + final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + final pixels = bytes!.buffer.asUint8List(); + var total = 0.0; + var count = 0; + final from = (left * _size).round(); + final to = (right * _size).round(); + for (var y = 0; y < _size; y++) { + for (var x = from; x < to; x++) { + final i = (y * _size + x) * 4; + if (pixels[i + 3] == 0) continue; + total += (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; + count++; + } + } + return count == 0 ? 0 : total / count; +} + +/// Mean luminance of a horizontal band, as a fraction of the image height, +/// over the middle half of its width (so the limb's own falloff stays out). +Future _band( + ui.Image image, { + required double top, + required double bottom, +}) async { + final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + final pixels = bytes!.buffer.asUint8List(); + var total = 0.0; + var count = 0; + for (var y = (top * _size).round(); y < (bottom * _size).round(); y++) { + for (var x = (_size * 0.25).round(); x < (_size * 0.75).round(); x++) { + final i = (y * _size + x) * 4; + if (pixels[i + 3] == 0) continue; + total += (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; + count++; + } + } + return count == 0 ? 0 : total / count; +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late ui.Image color; + late ui.Image height; + + setUpAll(() async { + color = await _asset('assets/astro/moon_color_2k.jpg'); + height = await _asset('assets/astro/moon_height_1k.png'); + }); + + tearDownAll(() { + color.dispose(); + height.dispose(); + }); + + test('the bundled NASA maps are equirectangular (2:1)', () { + expect(color.width, color.height * 2); + expect(height.width, height.height * 2); + }); + + test('new moon is dark, full moon is bright', () async { + final newMoon = await _renderPhase(color, height, 0); + final full = await _renderPhase(color, height, 3.14159265); + + final dark = await _luminance(newMoon); + final bright = await _luminance(full); + expect(dark, lessThan(30), reason: 'new moon should be nearly unlit'); + expect(bright, greaterThan(90), reason: 'full moon should be bright'); + newMoon.dispose(); + full.dispose(); + }); + + test('first quarter lights the right half', () async { + final image = await _renderPhase(color, height, 3.14159265 / 2); + final left = await _luminance(image, left: 0.05, right: 0.45); + final right = await _luminance(image, left: 0.55, right: 0.95); + + expect(right, greaterThan(left * 3)); + image.dispose(); + }); + + test('nothing is drawn outside the disc', () async { + final image = await _renderPhase(color, height, 3.14159265); + final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + final pixels = bytes!.buffer.asUint8List(); + // The corners are well outside an inscribed circle. + for (final (x, y) in [(1, 1), (_size - 2, 1), (1, _size - 2)]) { + expect(pixels[(y * _size + x) * 4 + 3], 0, reason: 'corner ($x,$y)'); + } + image.dispose(); + }); + + test('the disc shows the near side, not the far side', () async { + // Orthographic projection folds both hemispheres onto the same circle, so + // the far side renders as a perfectly plausible moon — same shape, same + // brightness, wrong world. What separates them is *where the maria are*: + // the near side's northern half is flooded with dark basalt (Imbrium, + // Serenitatis, Tranquillitatis) while its south is bright highland. On the + // far side that contrast reverses. Rotating the lookup half a turn is + // exactly the bug this catches, so the far side is rendered here as the + // control rather than assumed. + final near = await _renderPhase(color, height, 3.14159265); + final far = await _renderPhase( + color, + height, + 3.14159265, + librationLongitude: 3.14159265, + ); + + final nearNorth = await _band(near, top: 0.18, bottom: 0.42); + final nearSouth = await _band(near, top: 0.58, bottom: 0.82); + final farNorth = await _band(far, top: 0.18, bottom: 0.42); + final farSouth = await _band(far, top: 0.58, bottom: 0.82); + + expect( + nearNorth, + lessThan(nearSouth * 0.92), + reason: 'near-side maria should darken the northern half', + ); + expect( + farNorth, + greaterThan(farSouth), + reason: 'the far side has the opposite contrast — the control', + ); + near.dispose(); + far.dispose(); + }); + + test('a full moon stays bright to the limb', () async { + // Lunar regolith backscatters: the real full Moon is famously flat, not a + // shaded ball. A Lambert sphere would fall off toward the edge — this pins + // the Lommel-Seeliger term that prevents exactly that. + final image = await _renderPhase(color, height, 3.14159265); + final centre = await _luminance(image, left: 0.42, right: 0.58); + final edge = await _luminance(image, left: 0.02, right: 0.14); + + expect(edge, greaterThan(centre * 0.5)); + image.dispose(); + }); +} diff --git a/test/shaders/preview_render_test.dart b/test/shaders/preview_render_test.dart index ea5f3cd8a..6cfa6b7f6 100644 --- a/test/shaders/preview_render_test.dart +++ b/test/shaders/preview_render_test.dart @@ -218,9 +218,8 @@ void main() { ); final png = await image.toByteData(format: ui.ImageByteFormat.png); - File( - '${dir.path}/${c.name}.png', - ).writeAsBytesSync(png!.buffer.asUint8List()); + File('${dir.path}/${c.name}.png') + .writeAsBytesSync(png!.buffer.asUint8List()); final raw = await image.toByteData(format: ui.ImageByteFormat.rawRgba); final px = raw!.buffer.asUint8List(); @@ -443,9 +442,9 @@ Future _renderScene( // The base/haze probes are pixel-independent, so — as in production — their // colours are read from a CPU LUT readback rather than fetched per pixel. - final lutBytes = (await lut.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); + final lutBytes = (await lut.toByteData(format: ui.ImageByteFormat.rawRgba))! + .buffer + .asUint8List(); final lutU = sky.sunAngleY.clamp(0.0, 1.0); final baseSky = _sampleLut( lutBytes, diff --git a/test/shaders/rain_on_glass_test.dart b/test/shaders/rain_on_glass_test.dart index efc0b7b2c..b892e1bdf 100644 --- a/test/shaders/rain_on_glass_test.dart +++ b/test/shaders/rain_on_glass_test.dart @@ -3,6 +3,7 @@ library; import 'dart:io'; import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -59,18 +60,17 @@ void main() { f(refraction); sh.setImageSampler(0, content); final r = ui.PictureRecorder(); - ui.Canvas( - r, - ).drawRect(const Rect.fromLTWH(0, 0, 360, 300), Paint()..shader = sh); + ui.Canvas(r) + .drawRect(const Rect.fromLTWH(0, 0, 360, 300), Paint()..shader = sh); return r.endRecording().toImageSync(w, h); } final off = await render(0.0, 6.0); final on = await render(1.0, 6.0); - Future> px(ui.Image im) async => (await im.toByteData( - format: ui.ImageByteFormat.rawRgba, - ))!.buffer.asUint8List(); + Future> px(ui.Image im) async => + (await im.toByteData(format: ui.ImageByteFormat.rawRgba))!.buffer + .asUint8List(); final a = await px(off), b = await px(on); var diff = 0; for (var k = 0; k < a.length; k += 4) { @@ -86,9 +86,8 @@ void main() { final out = rec.endRecording().toImageSync(730, 300); final png = await out.toByteData(format: ui.ImageByteFormat.png); Directory('build/sky_preview').createSync(recursive: true); - File( - 'build/sky_preview/_rain_glass.png', - ).writeAsBytesSync(png!.buffer.asUint8List()); + File('build/sky_preview/_rain_glass.png') + .writeAsBytesSync(png!.buffer.asUint8List()); expect( diff, greaterThan(500), diff --git a/test/shaders/weather_sky_shader_test.dart b/test/shaders/weather_sky_shader_test.dart index 3b14f0fb4..ff37310f1 100644 --- a/test/shaders/weather_sky_shader_test.dart +++ b/test/shaders/weather_sky_shader_test.dart @@ -128,6 +128,13 @@ void main() { 0.1, // iCloudCover 0.0, // iGalaxy ], + samplers: 1, + ), + ( + asset: 'shaders/weather/night_field.frag', + floats: [ + 512, 512, // iResolution + ], samplers: 0, ), ( @@ -218,6 +225,40 @@ void main() { expect(opaque, greaterThan(0), reason: '${c.asset} painted nothing'); }); } + + test('night_field bakes a star in every layer', () async { + // Each of the four RGBA channels carries its own star layer; a layer that + // is uniformly dark means its grid formula broke (e.g. a hash or scale + // regression) and the night sky would silently lose that tier. + const size = 256; + final program = await ui.FragmentProgram.fromAsset( + 'shaders/weather/night_field.frag', + ); + final shader = program.fragmentShader(); + shader.setFloat(0, size.toDouble()); + shader.setFloat(1, size.toDouble()); + final recorder = ui.PictureRecorder(); + ui.Canvas(recorder).drawRect( + Rect.fromLTWH(0, 0, size.toDouble(), size.toDouble()), + Paint()..shader = shader, + ); + final image = recorder.endRecording().toImageSync(size, size); + final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + final px = bytes!.buffer.asUint8List(); + + final bright = [0, 0, 0, 0]; + for (var i = 0; i < px.length; i += 4) { + for (var c = 0; c < 4; c++) { + // A star core reaches ~2.0·smoothstep → clamps to 255; count pixels + // that are clearly lit, not just noise. + if (px[i + c] > 90) bright[c]++; + } + } + expect(bright[0], greaterThan(0), reason: 'bright-pass core layer dark'); + expect(bright[1], greaterThan(0), reason: 'bright-pass glow layer dark'); + expect(bright[2], greaterThan(0), reason: 'medium-pass layer dark'); + expect(bright[3], greaterThan(0), reason: 'faint-pass layer dark'); + }); } ui.Image _solidImage(Color color, int width, int height) { diff --git a/test/shared/map/base_map_tab_test.dart b/test/shared/map/base_map_tab_test.dart new file mode 100644 index 000000000..1bc23ab31 --- /dev/null +++ b/test/shared/map/base_map_tab_test.dart @@ -0,0 +1,136 @@ +import 'package:dpip/core/geo/town_directory.dart'; +import 'package:dpip/shared/map/base_map.dart'; +import 'package:dpip/shared/navigation/refresh_on_appear.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre_gl_platform_interface/maplibre_gl_platform_interface.dart'; +import 'package:provider/provider.dart'; + +/// A fake platform that completes the map lifecycle (`buildView` → +/// `onPlatformViewCreated` → controller) and records every +/// [MapLibrePlatform.setRenderPaused] call. Everything else a real controller +/// might touch is absorbed by `noSuchMethod` — these tests only exercise the +/// visibility↔render state machine. +class _FakePlatform extends MapLibrePlatform { + final paused = []; + + @override + Future initPlatform(int id) async {} + + @override + Widget buildView( + Map creationParams, + OnPlatformViewCreatedCallback onPlatformViewCreated, + Set>? gestureRecognizers, + ) { + onPlatformViewCreated(0); + return const SizedBox.shrink(); + } + + @override + Future setRenderPaused(bool paused) async { + this.paused.add(paused); + } + + @override + void noSuchMethod(Invocation invocation) {} +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late _FakePlatform platform; + + setUp(() { + platform = _FakePlatform(); + final previous = MapLibrePlatform.createInstance; + addTearDown(() => MapLibrePlatform.createInstance = previous); + MapLibrePlatform.createInstance = () => platform; + }); + + Future pump(WidgetTester tester, Widget child) async { + await tester.pumpWidget( + MaterialApp( + home: Provider.value( + // The style string needs a (empty) directory for its town labels — + // a real one is the app bootstrap's job. + value: TownDirectory.fromJson(const {}), + child: child, + ), + ), + ); + // Let the platform view callback run and the controller report in. + await tester.pump(); + } + + testWidgets('a map owned by another tab pauses once the controller is up', ( + tester, + ) async { + final visibleTab = VisibleTab(0); + await pump( + tester, + VisibleTabScope( + visibleTab: visibleTab, + child: const BaseMap(tabIndex: 2), + ), + ); + expect(platform.paused, [ + true, + ], reason: 'hidden at birth → paused on ready'); + }); + + testWidgets('returning to the owning tab resumes rendering', (tester) async { + final visibleTab = VisibleTab(0); + await pump( + tester, + VisibleTabScope( + visibleTab: visibleTab, + child: const BaseMap(tabIndex: 2), + ), + ); + visibleTab.value = 2; + await tester.pump(); + expect(platform.paused, [true, false]); + }); + + testWidgets('flapping between tabs pauses exactly once per transition', ( + tester, + ) async { + final visibleTab = VisibleTab(0); + await pump( + tester, + VisibleTabScope( + visibleTab: visibleTab, + child: const BaseMap(tabIndex: 2), + ), + ); + visibleTab.value = 2; + await tester.pump(); + visibleTab.value = 2; // Same value — no transition. + await tester.pump(); + visibleTab.value = 5; + await tester.pump(); + expect(platform.paused, [true, false, true]); + }); + + testWidgets('no tabIndex means the map never pauses', (tester) async { + final visibleTab = VisibleTab(0); + await pump( + tester, + VisibleTabScope(visibleTab: visibleTab, child: const BaseMap()), + ); + visibleTab.value = 2; + await tester.pump(); + expect(platform.paused, isEmpty); + }); + + testWidgets('no VisibleTabScope (outside the shell) means never paused', ( + tester, + ) async { + await pump(tester, const BaseMap(tabIndex: 2)); + expect(platform.paused, isEmpty); + }); +} diff --git a/test/shared/map/map_layer_switcher_test.dart b/test/shared/map/map_layer_switcher_test.dart index 882d873ca..bc555e2f8 100644 --- a/test/shared/map/map_layer_switcher_test.dart +++ b/test/shared/map/map_layer_switcher_test.dart @@ -325,38 +325,39 @@ void main() { expect(controller.categoryOrder, isEmpty); }); - testWidgets('reset restores the grouped default and clears both preferences', ( - tester, - ) async { - // A saved order that flips the forecast pair and moves a category — a - // grouped-default order would disable the reset button (nothing to restore). - final controller = await pumpSwitcher(tester, { - 'map.layerOrder': ['qpesums', 'satellite', 'rain', 'typhoon', 'radar'], - 'map.layerCategoryOrder': ['radar', 'typhoon'], - }); - await tester.tap(find.text('Radar echo')); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.tune)); - await tester.pumpAndSettle(); + testWidgets( + 'reset restores the grouped default and clears both preferences', + (tester) async { + // A saved order that flips the forecast pair and moves a category — a + // grouped-default order would disable the reset button (nothing to restore). + final controller = await pumpSwitcher(tester, { + 'map.layerOrder': ['qpesums', 'satellite', 'rain', 'typhoon', 'radar'], + 'map.layerCategoryOrder': ['radar', 'typhoon'], + }); + await tester.tap(find.text('Radar echo')); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.tune)); + await tester.pumpAndSettle(); - final l10n = AppLocalizations.of( - tester.element(find.byType(MapLayerSwitcher)), - ); - await tester.tap(find.text(l10n.mapLayerOrderReset)); - await tester.pumpAndSettle(); + final l10n = AppLocalizations.of( + tester.element(find.byType(MapLayerSwitcher)), + ); + await tester.tap(find.text(l10n.mapLayerOrderReset)); + await tester.pumpAndSettle(); - // The category list falls back to the grouped default… - final inEditor = find.descendant( - of: find.byType(ReorderableListView), - matching: find.text(l10n.mapLayerCategoryForecast), - ); - await tester.tap(inEditor); - await tester.pumpAndSettle(); - // …and precip is back before the wind models within the forecast group. - expect(topOf(tester, 'Precip'), lessThan(topOf(tester, 'ECMWF'))); - expect(topOf(tester, 'ECMWF'), lessThan(topOf(tester, 'GFS'))); - // Both preferences are cleared. - expect(controller.order, isEmpty); - expect(controller.categoryOrder, isEmpty); - }); + // The category list falls back to the grouped default… + final inEditor = find.descendant( + of: find.byType(ReorderableListView), + matching: find.text(l10n.mapLayerCategoryForecast), + ); + await tester.tap(inEditor); + await tester.pumpAndSettle(); + // …and precip is back before the wind models within the forecast group. + expect(topOf(tester, 'Precip'), lessThan(topOf(tester, 'ECMWF'))); + expect(topOf(tester, 'ECMWF'), lessThan(topOf(tester, 'GFS'))); + // Both preferences are cleared. + expect(controller.order, isEmpty); + expect(controller.categoryOrder, isEmpty); + }, + ); } diff --git a/test/shared/map/map_style_test.dart b/test/shared/map/map_style_test.dart index e8e58a6f2..14909f5ce 100644 --- a/test/shared/map/map_style_test.dart +++ b/test/shared/map/map_style_test.dart @@ -6,15 +6,13 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('the vector style parses and layers stack in order', () { - final style = - jsonDecode( - exptechVectorStyle( - MapColors.dark, - basemapTileUrl: 'https://example.com/{z}/{x}/{y}.pbf', - glyphsUrl: 'https://example.com/{fontstack}/{range}.pbf', - ), - ) - as Map; + final style = jsonDecode( + exptechVectorStyle( + MapColors.dark, + basemapTileUrl: 'https://example.com/{z}/{x}/{y}.pbf', + glyphsUrl: 'https://example.com/{fontstack}/{range}.pbf', + ), + ) as Map; final layers = style['layers'] as List; final ids = [for (final l in layers) (l as Map)['id']]; @@ -29,74 +27,67 @@ void main() { ]); }); - test( - 'terrain adds a mapbox-encoded raster-dem source and hillshade between fills and borders', - () { - final style = - jsonDecode( - exptechVectorStyle( - MapColors.dark, - basemapTileUrl: 'https://example.com/{z}/{x}/{y}.pbf', - glyphsUrl: 'https://example.com/{fontstack}/{range}.pbf', - terrainTileUrl: - 'https://static.lb.exptech.dev/api/v1/map/terrain/{z}/{x}/{y}.png', - ), - ) - as Map; + test('terrain adds a mapbox-encoded raster-dem source and hillshade between fills and borders', () { + final style = jsonDecode( + exptechVectorStyle( + MapColors.dark, + basemapTileUrl: 'https://example.com/{z}/{x}/{y}.pbf', + glyphsUrl: 'https://example.com/{fontstack}/{range}.pbf', + terrainTileUrl: + 'https://static.lb.exptech.dev/api/v1/map/terrain/{z}/{x}/{y}.png', + ), + ) as Map; - final terrain = style['sources']['terrain'] as Map; - expect(terrain['type'], 'raster-dem'); - expect( - terrain['encoding'], - 'mapbox', - reason: - 'the server tiles are Mapbox terrain-RGB — MapLibre decodes them ' - 'natively, no app-side rewrite (see satellite-tiles-go/web)', - ); - expect(terrain['tileSize'], 512); - expect(terrain['minzoom'], 0); - expect(terrain['maxzoom'], 12); - expect( - terrain['bounds'], - [110, 10, 132, 35], - reason: - 'the bounds must overshoot the DEM bbox so the hillshade edge ' - 'never meets the plain background on screen', - ); + final terrain = style['sources']['terrain'] as Map; + expect(terrain['type'], 'raster-dem'); + expect( + terrain['encoding'], + 'mapbox', + reason: + 'the server tiles are Mapbox terrain-RGB — MapLibre decodes them ' + 'natively, no app-side rewrite (see satellite-tiles-go/web)', + ); + expect(terrain['tileSize'], 512); + expect(terrain['minzoom'], 0); + expect(terrain['maxzoom'], 12); + expect( + terrain['bounds'], + [110, 10, 132, 35], + reason: + 'the bounds must overshoot the DEM bbox so the hillshade edge ' + 'never meets the plain background on screen', + ); - final hillshade = (style['layers'] as List) - .cast>() - .firstWhere((l) => l['id'] == terrainHillshadeLayerId); - final paint = hillshade['paint'] as Map; - expect(paint['hillshade-illumination-direction'], 335); - expect(paint['hillshade-exaggeration'], 0.3); + final hillshade = (style['layers'] as List) + .cast>() + .firstWhere((l) => l['id'] == terrainHillshadeLayerId); + final paint = hillshade['paint'] as Map; + expect(paint['hillshade-illumination-direction'], 335); + expect(paint['hillshade-exaggeration'], 0.3); - final layers = style['layers'] as List; - final ids = [for (final l in layers) (l as Map)['id']]; - expect(ids, [ - 'bg', - 'land', - 'county', - 'town', - terrainHillshadeLayerId, - 'town-outline', - 'county-outline', - 'town-label', - ]); - }, - ); + final layers = style['layers'] as List; + final ids = [for (final l in layers) (l as Map)['id']]; + expect(ids, [ + 'bg', + 'land', + 'county', + 'town', + terrainHillshadeLayerId, + 'town-outline', + 'county-outline', + 'town-label', + ]); + }); group('town-label layer', () { Map townLabel() { - final style = - jsonDecode( - exptechVectorStyle( - MapColors.light, - basemapTileUrl: 'https://example.com/{z}/{x}/{y}.pbf', - glyphsUrl: 'https://example.com/{fontstack}/{range}.pbf', - ), - ) - as Map; + final style = jsonDecode( + exptechVectorStyle( + MapColors.light, + basemapTileUrl: 'https://example.com/{z}/{x}/{y}.pbf', + glyphsUrl: 'https://example.com/{fontstack}/{range}.pbf', + ), + ) as Map; return (style['layers'] as List) .cast>() .firstWhere((l) => l['id'] == townLabelLayerId); diff --git a/test/shared/map/map_tile_cache_test.dart b/test/shared/map/map_tile_cache_test.dart index e8dba2140..f36d92878 100644 --- a/test/shared/map/map_tile_cache_test.dart +++ b/test/shared/map/map_tile_cache_test.dart @@ -156,11 +156,9 @@ void main() { ], }); - final served = - await fromNative('getBatch', { - 'urls': [hole, glyph], - }) - as Map; + final served = await fromNative('getBatch', { + 'urls': [hole, glyph], + }) as Map; expect( served[hole], isNotNull, @@ -189,11 +187,9 @@ void main() { final stored = await store.readBytes(terrainUrl); expect(stored, isNotNull); expect(stored!.bytes, bytes); - final served = - await fromNative('getBatch', { - 'urls': [terrainUrl], - }) - as Map; + final served = await fromNative('getBatch', { + 'urls': [terrainUrl], + }) as Map; expect((served[terrainUrl] as Map)['data'], bytes); }); } diff --git a/test/shared/navigation/refresh_on_appear_test.dart b/test/shared/navigation/refresh_on_appear_test.dart index 7dd35a326..8803ff404 100644 --- a/test/shared/navigation/refresh_on_appear_test.dart +++ b/test/shared/navigation/refresh_on_appear_test.dart @@ -20,6 +20,29 @@ Widget _host( ); } +/// A dependent that reads the scope in `didChangeDependencies`, the way the +/// home sheet's [TickerMode] gate does. +class _ScopeDependent extends StatefulWidget { + const _ScopeDependent({required this.onDeps}); + + final VoidCallback onDeps; + + @override + State<_ScopeDependent> createState() => _ScopeDependentState(); +} + +class _ScopeDependentState extends State<_ScopeDependent> { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + VisibleTabScope.of(context); + widget.onDeps(); + } + + @override + Widget build(BuildContext context) => const SizedBox.shrink(); +} + void main() { testWidgets('does not fire on first build', (tester) async { var calls = 0; @@ -95,6 +118,52 @@ void main() { expect(calls, 0); }); + testWidgets('scope does not notify on value change — consumers subscribe to ' + 'the notifier instead', (tester) async { + // The shell hands the same notifier instance down for the page's whole + // life, so updateShouldNotify can never fire on a value move (both sides + // of the comparison read the same object). A consumer that only reads the + // scope in didChangeDependencies — as the home sheet's TickerMode gate + // once did — would freeze at its first value and keep animating behind + // hidden tabs. The contract is: subscribe to the notifier itself. + final tab = VisibleTab(0); + var deps = 0; + late StateSetter setParent; + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState) { + setParent = setState; + return VisibleTabScope( + visibleTab: tab, + child: _ScopeDependent(onDeps: () => deps++), + ); + }, + ), + ); + await tester.pumpAndSettle(); + expect(deps, 1, reason: 'first mount always reads the scope'); + + tab.value = 2; + setParent(() {}); + await tester.pumpAndSettle(); + expect(deps, 1, reason: 'a same-instance value move cannot notify'); + + // A different notifier instance (never what the shell does, but what a + // rebuild would need for the inherited mechanism to fire) does notify. + await tester.pumpWidget( + StatefulBuilder( + builder: (context, setState) { + return VisibleTabScope( + visibleTab: VisibleTab(2), + child: _ScopeDependent(onDeps: () => deps++), + ); + }, + ), + ); + await tester.pumpAndSettle(); + expect(deps, 2, reason: 'a swapped instance notifies dependents'); + }); + testWidgets('works with no scope (treated as always visible)', ( tester, ) async { diff --git a/third_party/meshtastic_flutter/CHANGELOG.md b/third_party/meshtastic_flutter/CHANGELOG.md new file mode 100644 index 000000000..fe90e2935 --- /dev/null +++ b/third_party/meshtastic_flutter/CHANGELOG.md @@ -0,0 +1,68 @@ +## 0.0.1 + +* Initial release +* Complete Meshtastic BLE protocol implementation +* Support for device discovery, connection, and communication +* Real-time packet and node information streaming +* Text messaging and position sharing +* Configuration access and node management +* Comprehensive error handling and type safety +* Cross-platform support (Android/iOS) + + +## 0.0.2 + +* Upgraded dependency versions + + +## 0.0.3 + +* Replace deprecated Code + + +## 0.0.3+dpip (local fork) + +* Deliver the packets a radio queues while no phone is connected: `fromradio` + is now drained until the mailbox is empty instead of stopping at + `config_complete_id`. The firmware only replays that backlog *after* the + config handshake (`PhoneAPI` → `STATE_SEND_PACKETS`) and never notifies + `fromnum` for it, so the old read loop dropped every message that arrived + during a disconnect. +* `fromnum` notifications now always trigger a drain; the previous + `fromNum > lastSeen` guard stopped delivering packets after a radio reboot + reset the counter. +* Reads are single-flight — a notification arriving mid-drain no longer starts + a second, interleaved read loop on the same characteristic. +* An unparseable packet is skipped instead of aborting the read loop, and a + failed config download reports an error instead of stalling in `configuring`. +* `_configComplete` is cleared on an unexpected disconnect so a reconnect + re-runs the handshake (and the backlog replay). + +### DPIP data plane + provisioning + +* `sendData(portnum:…)` — arbitrary app ports, any channel, broadcast or + direct. `from` is left unset because the firmware overwrites it, and a zero + `from` is what marks a packet local. +* `sendAdmin(AdminMessage)` + `adminStream` — local administration of the + attached radio (channel table, LoRa config) and its replies. Local admin is + exempt from the remote-admin session key precisely because `from == 0`. +* `connectToId(remoteId)` — reconnect to a known radio without scanning first. +* `myNodeNum` / `channels` / `loraConfig` accessors. +* Fixed: the LoRa config was being lost. `Config` carries its sections in a + protobuf **oneof** and the radio sends one section per packet, so the single + `_config` field only ever kept the last section of the download (bluetooth). + The LoRa section is now captured separately. + +### Cross-platform fixes (adversarial review) + +* `scanForDevices` now **always terminates**. It used to `await for` over + `FlutterBluePlus.scanResults`, which is a broadcast stream that is never + closed and emits nothing on `stopScan` — so with no radio in range the + generator hung forever, and with it every caller waiting on the scan's end + (the picker's spinner, and any reconnect that fell back to a scan). +* Writes to `toradio` use a long write. iOS caps a plain write at the ATT MTU + minus 3 (~182 B) against Android's 509, so full-size payloads sent fine on + Android and threw on iOS. +* Dropped the explicit `requestMtu(512)` — `connect()` already negotiates it on + Android and ignores it elsewhere; this only bought a second round trip. +* `cacheChannel` keeps the channel table current after a write. diff --git a/third_party/meshtastic_flutter/LICENSE b/third_party/meshtastic_flutter/LICENSE new file mode 100644 index 000000000..418d81471 --- /dev/null +++ b/third_party/meshtastic_flutter/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, Madhav Gupta + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/meshtastic_flutter/lib/generated/admin.pb.dart b/third_party/meshtastic_flutter/lib/generated/admin.pb.dart new file mode 100644 index 000000000..8974ab864 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/admin.pb.dart @@ -0,0 +1,1644 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/admin.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'admin.pbenum.dart'; +import 'channel.pb.dart' as $0; +import 'config.pb.dart' as $2; +import 'connection_status.pb.dart' as $4; +import 'device_ui.pb.dart' as $5; +import 'mesh.pb.dart' as $1; +import 'module_config.pb.dart' as $3; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'admin.pbenum.dart'; + +/// +/// Input event message to be sent to the node. +class AdminMessage_InputEvent extends $pb.GeneratedMessage { + factory AdminMessage_InputEvent({ + $core.int? eventCode, + $core.int? kbChar, + $core.int? touchX, + $core.int? touchY, + }) { + final result = create(); + if (eventCode != null) result.eventCode = eventCode; + if (kbChar != null) result.kbChar = kbChar; + if (touchX != null) result.touchX = touchX; + if (touchY != null) result.touchY = touchY; + return result; + } + + AdminMessage_InputEvent._(); + + factory AdminMessage_InputEvent.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory AdminMessage_InputEvent.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'AdminMessage.InputEvent', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'eventCode', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'kbChar', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'touchX', $pb.PbFieldType.OU3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'touchY', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AdminMessage_InputEvent clone() => + AdminMessage_InputEvent()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AdminMessage_InputEvent copyWith( + void Function(AdminMessage_InputEvent) updates) => + super.copyWith((message) => updates(message as AdminMessage_InputEvent)) + as AdminMessage_InputEvent; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AdminMessage_InputEvent create() => AdminMessage_InputEvent._(); + @$core.override + AdminMessage_InputEvent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AdminMessage_InputEvent getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static AdminMessage_InputEvent? _defaultInstance; + + /// + /// The input event code + @$pb.TagNumber(1) + $core.int get eventCode => $_getIZ(0); + @$pb.TagNumber(1) + set eventCode($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasEventCode() => $_has(0); + @$pb.TagNumber(1) + void clearEventCode() => $_clearField(1); + + /// + /// Keyboard character code + @$pb.TagNumber(2) + $core.int get kbChar => $_getIZ(1); + @$pb.TagNumber(2) + set kbChar($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasKbChar() => $_has(1); + @$pb.TagNumber(2) + void clearKbChar() => $_clearField(2); + + /// + /// The touch X coordinate + @$pb.TagNumber(3) + $core.int get touchX => $_getIZ(2); + @$pb.TagNumber(3) + set touchX($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasTouchX() => $_has(2); + @$pb.TagNumber(3) + void clearTouchX() => $_clearField(3); + + /// + /// The touch Y coordinate + @$pb.TagNumber(4) + $core.int get touchY => $_getIZ(3); + @$pb.TagNumber(4) + set touchY($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasTouchY() => $_has(3); + @$pb.TagNumber(4) + void clearTouchY() => $_clearField(4); +} + +enum AdminMessage_PayloadVariant { + getChannelRequest, + getChannelResponse, + getOwnerRequest, + getOwnerResponse, + getConfigRequest, + getConfigResponse, + getModuleConfigRequest, + getModuleConfigResponse, + getCannedMessageModuleMessagesRequest, + getCannedMessageModuleMessagesResponse, + getDeviceMetadataRequest, + getDeviceMetadataResponse, + getRingtoneRequest, + getRingtoneResponse, + getDeviceConnectionStatusRequest, + getDeviceConnectionStatusResponse, + setHamMode, + getNodeRemoteHardwarePinsRequest, + getNodeRemoteHardwarePinsResponse, + enterDfuModeRequest, + deleteFileRequest, + setScale, + backupPreferences, + restorePreferences, + removeBackupPreferences, + sendInputEvent, + setOwner, + setChannel, + setConfig, + setModuleConfig, + setCannedMessageModuleMessages, + setRingtoneMessage, + removeByNodenum, + setFavoriteNode, + removeFavoriteNode, + setFixedPosition, + removeFixedPosition, + setTimeOnly, + getUiConfigRequest, + getUiConfigResponse, + storeUiConfig, + setIgnoredNode, + removeIgnoredNode, + beginEditSettings, + commitEditSettings, + addContact, + keyVerification, + factoryResetDevice, + rebootOtaSeconds, + exitSimulator, + rebootSeconds, + shutdownSeconds, + factoryResetConfig, + nodedbReset, + notSet +} + +/// +/// This message is handled by the Admin module and is responsible for all settings/channel read/write operations. +/// This message is used to do settings operations to both remote AND local nodes. +/// (Prior to 1.2 these operations were done via special ToRadio operations) +class AdminMessage extends $pb.GeneratedMessage { + factory AdminMessage({ + $core.int? getChannelRequest, + $0.Channel? getChannelResponse, + $core.bool? getOwnerRequest, + $1.User? getOwnerResponse, + AdminMessage_ConfigType? getConfigRequest, + $2.Config? getConfigResponse, + AdminMessage_ModuleConfigType? getModuleConfigRequest, + $3.ModuleConfig? getModuleConfigResponse, + $core.bool? getCannedMessageModuleMessagesRequest, + $core.String? getCannedMessageModuleMessagesResponse, + $core.bool? getDeviceMetadataRequest, + $1.DeviceMetadata? getDeviceMetadataResponse, + $core.bool? getRingtoneRequest, + $core.String? getRingtoneResponse, + $core.bool? getDeviceConnectionStatusRequest, + $4.DeviceConnectionStatus? getDeviceConnectionStatusResponse, + HamParameters? setHamMode, + $core.bool? getNodeRemoteHardwarePinsRequest, + NodeRemoteHardwarePinsResponse? getNodeRemoteHardwarePinsResponse, + $core.bool? enterDfuModeRequest, + $core.String? deleteFileRequest, + $core.int? setScale, + AdminMessage_BackupLocation? backupPreferences, + AdminMessage_BackupLocation? restorePreferences, + AdminMessage_BackupLocation? removeBackupPreferences, + AdminMessage_InputEvent? sendInputEvent, + $1.User? setOwner, + $0.Channel? setChannel, + $2.Config? setConfig, + $3.ModuleConfig? setModuleConfig, + $core.String? setCannedMessageModuleMessages, + $core.String? setRingtoneMessage, + $core.int? removeByNodenum, + $core.int? setFavoriteNode, + $core.int? removeFavoriteNode, + $1.Position? setFixedPosition, + $core.bool? removeFixedPosition, + $core.int? setTimeOnly, + $core.bool? getUiConfigRequest, + $5.DeviceUIConfig? getUiConfigResponse, + $5.DeviceUIConfig? storeUiConfig, + $core.int? setIgnoredNode, + $core.int? removeIgnoredNode, + $core.bool? beginEditSettings, + $core.bool? commitEditSettings, + SharedContact? addContact, + KeyVerificationAdmin? keyVerification, + $core.int? factoryResetDevice, + $core.int? rebootOtaSeconds, + $core.bool? exitSimulator, + $core.int? rebootSeconds, + $core.int? shutdownSeconds, + $core.int? factoryResetConfig, + $core.int? nodedbReset, + $core.List<$core.int>? sessionPasskey, + }) { + final result = create(); + if (getChannelRequest != null) result.getChannelRequest = getChannelRequest; + if (getChannelResponse != null) + result.getChannelResponse = getChannelResponse; + if (getOwnerRequest != null) result.getOwnerRequest = getOwnerRequest; + if (getOwnerResponse != null) result.getOwnerResponse = getOwnerResponse; + if (getConfigRequest != null) result.getConfigRequest = getConfigRequest; + if (getConfigResponse != null) result.getConfigResponse = getConfigResponse; + if (getModuleConfigRequest != null) + result.getModuleConfigRequest = getModuleConfigRequest; + if (getModuleConfigResponse != null) + result.getModuleConfigResponse = getModuleConfigResponse; + if (getCannedMessageModuleMessagesRequest != null) + result.getCannedMessageModuleMessagesRequest = + getCannedMessageModuleMessagesRequest; + if (getCannedMessageModuleMessagesResponse != null) + result.getCannedMessageModuleMessagesResponse = + getCannedMessageModuleMessagesResponse; + if (getDeviceMetadataRequest != null) + result.getDeviceMetadataRequest = getDeviceMetadataRequest; + if (getDeviceMetadataResponse != null) + result.getDeviceMetadataResponse = getDeviceMetadataResponse; + if (getRingtoneRequest != null) + result.getRingtoneRequest = getRingtoneRequest; + if (getRingtoneResponse != null) + result.getRingtoneResponse = getRingtoneResponse; + if (getDeviceConnectionStatusRequest != null) + result.getDeviceConnectionStatusRequest = + getDeviceConnectionStatusRequest; + if (getDeviceConnectionStatusResponse != null) + result.getDeviceConnectionStatusResponse = + getDeviceConnectionStatusResponse; + if (setHamMode != null) result.setHamMode = setHamMode; + if (getNodeRemoteHardwarePinsRequest != null) + result.getNodeRemoteHardwarePinsRequest = + getNodeRemoteHardwarePinsRequest; + if (getNodeRemoteHardwarePinsResponse != null) + result.getNodeRemoteHardwarePinsResponse = + getNodeRemoteHardwarePinsResponse; + if (enterDfuModeRequest != null) + result.enterDfuModeRequest = enterDfuModeRequest; + if (deleteFileRequest != null) result.deleteFileRequest = deleteFileRequest; + if (setScale != null) result.setScale = setScale; + if (backupPreferences != null) result.backupPreferences = backupPreferences; + if (restorePreferences != null) + result.restorePreferences = restorePreferences; + if (removeBackupPreferences != null) + result.removeBackupPreferences = removeBackupPreferences; + if (sendInputEvent != null) result.sendInputEvent = sendInputEvent; + if (setOwner != null) result.setOwner = setOwner; + if (setChannel != null) result.setChannel = setChannel; + if (setConfig != null) result.setConfig = setConfig; + if (setModuleConfig != null) result.setModuleConfig = setModuleConfig; + if (setCannedMessageModuleMessages != null) + result.setCannedMessageModuleMessages = setCannedMessageModuleMessages; + if (setRingtoneMessage != null) + result.setRingtoneMessage = setRingtoneMessage; + if (removeByNodenum != null) result.removeByNodenum = removeByNodenum; + if (setFavoriteNode != null) result.setFavoriteNode = setFavoriteNode; + if (removeFavoriteNode != null) + result.removeFavoriteNode = removeFavoriteNode; + if (setFixedPosition != null) result.setFixedPosition = setFixedPosition; + if (removeFixedPosition != null) + result.removeFixedPosition = removeFixedPosition; + if (setTimeOnly != null) result.setTimeOnly = setTimeOnly; + if (getUiConfigRequest != null) + result.getUiConfigRequest = getUiConfigRequest; + if (getUiConfigResponse != null) + result.getUiConfigResponse = getUiConfigResponse; + if (storeUiConfig != null) result.storeUiConfig = storeUiConfig; + if (setIgnoredNode != null) result.setIgnoredNode = setIgnoredNode; + if (removeIgnoredNode != null) result.removeIgnoredNode = removeIgnoredNode; + if (beginEditSettings != null) result.beginEditSettings = beginEditSettings; + if (commitEditSettings != null) + result.commitEditSettings = commitEditSettings; + if (addContact != null) result.addContact = addContact; + if (keyVerification != null) result.keyVerification = keyVerification; + if (factoryResetDevice != null) + result.factoryResetDevice = factoryResetDevice; + if (rebootOtaSeconds != null) result.rebootOtaSeconds = rebootOtaSeconds; + if (exitSimulator != null) result.exitSimulator = exitSimulator; + if (rebootSeconds != null) result.rebootSeconds = rebootSeconds; + if (shutdownSeconds != null) result.shutdownSeconds = shutdownSeconds; + if (factoryResetConfig != null) + result.factoryResetConfig = factoryResetConfig; + if (nodedbReset != null) result.nodedbReset = nodedbReset; + if (sessionPasskey != null) result.sessionPasskey = sessionPasskey; + return result; + } + + AdminMessage._(); + + factory AdminMessage.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory AdminMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, AdminMessage_PayloadVariant> + _AdminMessage_PayloadVariantByTag = { + 1: AdminMessage_PayloadVariant.getChannelRequest, + 2: AdminMessage_PayloadVariant.getChannelResponse, + 3: AdminMessage_PayloadVariant.getOwnerRequest, + 4: AdminMessage_PayloadVariant.getOwnerResponse, + 5: AdminMessage_PayloadVariant.getConfigRequest, + 6: AdminMessage_PayloadVariant.getConfigResponse, + 7: AdminMessage_PayloadVariant.getModuleConfigRequest, + 8: AdminMessage_PayloadVariant.getModuleConfigResponse, + 10: AdminMessage_PayloadVariant.getCannedMessageModuleMessagesRequest, + 11: AdminMessage_PayloadVariant.getCannedMessageModuleMessagesResponse, + 12: AdminMessage_PayloadVariant.getDeviceMetadataRequest, + 13: AdminMessage_PayloadVariant.getDeviceMetadataResponse, + 14: AdminMessage_PayloadVariant.getRingtoneRequest, + 15: AdminMessage_PayloadVariant.getRingtoneResponse, + 16: AdminMessage_PayloadVariant.getDeviceConnectionStatusRequest, + 17: AdminMessage_PayloadVariant.getDeviceConnectionStatusResponse, + 18: AdminMessage_PayloadVariant.setHamMode, + 19: AdminMessage_PayloadVariant.getNodeRemoteHardwarePinsRequest, + 20: AdminMessage_PayloadVariant.getNodeRemoteHardwarePinsResponse, + 21: AdminMessage_PayloadVariant.enterDfuModeRequest, + 22: AdminMessage_PayloadVariant.deleteFileRequest, + 23: AdminMessage_PayloadVariant.setScale, + 24: AdminMessage_PayloadVariant.backupPreferences, + 25: AdminMessage_PayloadVariant.restorePreferences, + 26: AdminMessage_PayloadVariant.removeBackupPreferences, + 27: AdminMessage_PayloadVariant.sendInputEvent, + 32: AdminMessage_PayloadVariant.setOwner, + 33: AdminMessage_PayloadVariant.setChannel, + 34: AdminMessage_PayloadVariant.setConfig, + 35: AdminMessage_PayloadVariant.setModuleConfig, + 36: AdminMessage_PayloadVariant.setCannedMessageModuleMessages, + 37: AdminMessage_PayloadVariant.setRingtoneMessage, + 38: AdminMessage_PayloadVariant.removeByNodenum, + 39: AdminMessage_PayloadVariant.setFavoriteNode, + 40: AdminMessage_PayloadVariant.removeFavoriteNode, + 41: AdminMessage_PayloadVariant.setFixedPosition, + 42: AdminMessage_PayloadVariant.removeFixedPosition, + 43: AdminMessage_PayloadVariant.setTimeOnly, + 44: AdminMessage_PayloadVariant.getUiConfigRequest, + 45: AdminMessage_PayloadVariant.getUiConfigResponse, + 46: AdminMessage_PayloadVariant.storeUiConfig, + 47: AdminMessage_PayloadVariant.setIgnoredNode, + 48: AdminMessage_PayloadVariant.removeIgnoredNode, + 64: AdminMessage_PayloadVariant.beginEditSettings, + 65: AdminMessage_PayloadVariant.commitEditSettings, + 66: AdminMessage_PayloadVariant.addContact, + 67: AdminMessage_PayloadVariant.keyVerification, + 94: AdminMessage_PayloadVariant.factoryResetDevice, + 95: AdminMessage_PayloadVariant.rebootOtaSeconds, + 96: AdminMessage_PayloadVariant.exitSimulator, + 97: AdminMessage_PayloadVariant.rebootSeconds, + 98: AdminMessage_PayloadVariant.shutdownSeconds, + 99: AdminMessage_PayloadVariant.factoryResetConfig, + 100: AdminMessage_PayloadVariant.nodedbReset, + 0: AdminMessage_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'AdminMessage', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 64, + 65, + 66, + 67, + 94, + 95, + 96, + 97, + 98, + 99, + 100 + ]) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'getChannelRequest', $pb.PbFieldType.OU3) + ..aOM<$0.Channel>(2, _omitFieldNames ? '' : 'getChannelResponse', + subBuilder: $0.Channel.create) + ..aOB(3, _omitFieldNames ? '' : 'getOwnerRequest') + ..aOM<$1.User>(4, _omitFieldNames ? '' : 'getOwnerResponse', + subBuilder: $1.User.create) + ..e( + 5, _omitFieldNames ? '' : 'getConfigRequest', $pb.PbFieldType.OE, + defaultOrMaker: AdminMessage_ConfigType.DEVICE_CONFIG, + valueOf: AdminMessage_ConfigType.valueOf, + enumValues: AdminMessage_ConfigType.values) + ..aOM<$2.Config>(6, _omitFieldNames ? '' : 'getConfigResponse', + subBuilder: $2.Config.create) + ..e( + 7, _omitFieldNames ? '' : 'getModuleConfigRequest', $pb.PbFieldType.OE, + defaultOrMaker: AdminMessage_ModuleConfigType.MQTT_CONFIG, + valueOf: AdminMessage_ModuleConfigType.valueOf, + enumValues: AdminMessage_ModuleConfigType.values) + ..aOM<$3.ModuleConfig>(8, _omitFieldNames ? '' : 'getModuleConfigResponse', + subBuilder: $3.ModuleConfig.create) + ..aOB(10, _omitFieldNames ? '' : 'getCannedMessageModuleMessagesRequest') + ..aOS(11, _omitFieldNames ? '' : 'getCannedMessageModuleMessagesResponse') + ..aOB(12, _omitFieldNames ? '' : 'getDeviceMetadataRequest') + ..aOM<$1.DeviceMetadata>( + 13, _omitFieldNames ? '' : 'getDeviceMetadataResponse', + subBuilder: $1.DeviceMetadata.create) + ..aOB(14, _omitFieldNames ? '' : 'getRingtoneRequest') + ..aOS(15, _omitFieldNames ? '' : 'getRingtoneResponse') + ..aOB(16, _omitFieldNames ? '' : 'getDeviceConnectionStatusRequest') + ..aOM<$4.DeviceConnectionStatus>( + 17, _omitFieldNames ? '' : 'getDeviceConnectionStatusResponse', + subBuilder: $4.DeviceConnectionStatus.create) + ..aOM(18, _omitFieldNames ? '' : 'setHamMode', + subBuilder: HamParameters.create) + ..aOB(19, _omitFieldNames ? '' : 'getNodeRemoteHardwarePinsRequest') + ..aOM( + 20, _omitFieldNames ? '' : 'getNodeRemoteHardwarePinsResponse', + subBuilder: NodeRemoteHardwarePinsResponse.create) + ..aOB(21, _omitFieldNames ? '' : 'enterDfuModeRequest') + ..aOS(22, _omitFieldNames ? '' : 'deleteFileRequest') + ..a<$core.int>(23, _omitFieldNames ? '' : 'setScale', $pb.PbFieldType.OU3) + ..e( + 24, _omitFieldNames ? '' : 'backupPreferences', $pb.PbFieldType.OE, + defaultOrMaker: AdminMessage_BackupLocation.FLASH, + valueOf: AdminMessage_BackupLocation.valueOf, + enumValues: AdminMessage_BackupLocation.values) + ..e( + 25, _omitFieldNames ? '' : 'restorePreferences', $pb.PbFieldType.OE, + defaultOrMaker: AdminMessage_BackupLocation.FLASH, + valueOf: AdminMessage_BackupLocation.valueOf, + enumValues: AdminMessage_BackupLocation.values) + ..e(26, + _omitFieldNames ? '' : 'removeBackupPreferences', $pb.PbFieldType.OE, + defaultOrMaker: AdminMessage_BackupLocation.FLASH, + valueOf: AdminMessage_BackupLocation.valueOf, + enumValues: AdminMessage_BackupLocation.values) + ..aOM(27, _omitFieldNames ? '' : 'sendInputEvent', + subBuilder: AdminMessage_InputEvent.create) + ..aOM<$1.User>(32, _omitFieldNames ? '' : 'setOwner', + subBuilder: $1.User.create) + ..aOM<$0.Channel>(33, _omitFieldNames ? '' : 'setChannel', + subBuilder: $0.Channel.create) + ..aOM<$2.Config>(34, _omitFieldNames ? '' : 'setConfig', + subBuilder: $2.Config.create) + ..aOM<$3.ModuleConfig>(35, _omitFieldNames ? '' : 'setModuleConfig', + subBuilder: $3.ModuleConfig.create) + ..aOS(36, _omitFieldNames ? '' : 'setCannedMessageModuleMessages') + ..aOS(37, _omitFieldNames ? '' : 'setRingtoneMessage') + ..a<$core.int>( + 38, _omitFieldNames ? '' : 'removeByNodenum', $pb.PbFieldType.OU3) + ..a<$core.int>( + 39, _omitFieldNames ? '' : 'setFavoriteNode', $pb.PbFieldType.OU3) + ..a<$core.int>( + 40, _omitFieldNames ? '' : 'removeFavoriteNode', $pb.PbFieldType.OU3) + ..aOM<$1.Position>(41, _omitFieldNames ? '' : 'setFixedPosition', + subBuilder: $1.Position.create) + ..aOB(42, _omitFieldNames ? '' : 'removeFixedPosition') + ..a<$core.int>( + 43, _omitFieldNames ? '' : 'setTimeOnly', $pb.PbFieldType.OF3) + ..aOB(44, _omitFieldNames ? '' : 'getUiConfigRequest') + ..aOM<$5.DeviceUIConfig>(45, _omitFieldNames ? '' : 'getUiConfigResponse', + subBuilder: $5.DeviceUIConfig.create) + ..aOM<$5.DeviceUIConfig>(46, _omitFieldNames ? '' : 'storeUiConfig', + subBuilder: $5.DeviceUIConfig.create) + ..a<$core.int>( + 47, _omitFieldNames ? '' : 'setIgnoredNode', $pb.PbFieldType.OU3) + ..a<$core.int>( + 48, _omitFieldNames ? '' : 'removeIgnoredNode', $pb.PbFieldType.OU3) + ..aOB(64, _omitFieldNames ? '' : 'beginEditSettings') + ..aOB(65, _omitFieldNames ? '' : 'commitEditSettings') + ..aOM(66, _omitFieldNames ? '' : 'addContact', + subBuilder: SharedContact.create) + ..aOM(67, _omitFieldNames ? '' : 'keyVerification', + subBuilder: KeyVerificationAdmin.create) + ..a<$core.int>( + 94, _omitFieldNames ? '' : 'factoryResetDevice', $pb.PbFieldType.O3) + ..a<$core.int>( + 95, _omitFieldNames ? '' : 'rebootOtaSeconds', $pb.PbFieldType.O3) + ..aOB(96, _omitFieldNames ? '' : 'exitSimulator') + ..a<$core.int>( + 97, _omitFieldNames ? '' : 'rebootSeconds', $pb.PbFieldType.O3) + ..a<$core.int>( + 98, _omitFieldNames ? '' : 'shutdownSeconds', $pb.PbFieldType.O3) + ..a<$core.int>( + 99, _omitFieldNames ? '' : 'factoryResetConfig', $pb.PbFieldType.O3) + ..a<$core.int>( + 100, _omitFieldNames ? '' : 'nodedbReset', $pb.PbFieldType.O3) + ..a<$core.List<$core.int>>( + 101, _omitFieldNames ? '' : 'sessionPasskey', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AdminMessage clone() => AdminMessage()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AdminMessage copyWith(void Function(AdminMessage) updates) => + super.copyWith((message) => updates(message as AdminMessage)) + as AdminMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AdminMessage create() => AdminMessage._(); + @$core.override + AdminMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AdminMessage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static AdminMessage? _defaultInstance; + + AdminMessage_PayloadVariant whichPayloadVariant() => + _AdminMessage_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// Send the specified channel in the response to this message + /// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) + @$pb.TagNumber(1) + $core.int get getChannelRequest => $_getIZ(0); + @$pb.TagNumber(1) + set getChannelRequest($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasGetChannelRequest() => $_has(0); + @$pb.TagNumber(1) + void clearGetChannelRequest() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + $0.Channel get getChannelResponse => $_getN(1); + @$pb.TagNumber(2) + set getChannelResponse($0.Channel value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasGetChannelResponse() => $_has(1); + @$pb.TagNumber(2) + void clearGetChannelResponse() => $_clearField(2); + @$pb.TagNumber(2) + $0.Channel ensureGetChannelResponse() => $_ensure(1); + + /// + /// Send the current owner data in the response to this message. + @$pb.TagNumber(3) + $core.bool get getOwnerRequest => $_getBF(2); + @$pb.TagNumber(3) + set getOwnerRequest($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasGetOwnerRequest() => $_has(2); + @$pb.TagNumber(3) + void clearGetOwnerRequest() => $_clearField(3); + + /// + /// TODO: REPLACE + @$pb.TagNumber(4) + $1.User get getOwnerResponse => $_getN(3); + @$pb.TagNumber(4) + set getOwnerResponse($1.User value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasGetOwnerResponse() => $_has(3); + @$pb.TagNumber(4) + void clearGetOwnerResponse() => $_clearField(4); + @$pb.TagNumber(4) + $1.User ensureGetOwnerResponse() => $_ensure(3); + + /// + /// Ask for the following config data to be sent + @$pb.TagNumber(5) + AdminMessage_ConfigType get getConfigRequest => $_getN(4); + @$pb.TagNumber(5) + set getConfigRequest(AdminMessage_ConfigType value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasGetConfigRequest() => $_has(4); + @$pb.TagNumber(5) + void clearGetConfigRequest() => $_clearField(5); + + /// + /// Send the current Config in the response to this message. + @$pb.TagNumber(6) + $2.Config get getConfigResponse => $_getN(5); + @$pb.TagNumber(6) + set getConfigResponse($2.Config value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasGetConfigResponse() => $_has(5); + @$pb.TagNumber(6) + void clearGetConfigResponse() => $_clearField(6); + @$pb.TagNumber(6) + $2.Config ensureGetConfigResponse() => $_ensure(5); + + /// + /// Ask for the following config data to be sent + @$pb.TagNumber(7) + AdminMessage_ModuleConfigType get getModuleConfigRequest => $_getN(6); + @$pb.TagNumber(7) + set getModuleConfigRequest(AdminMessage_ModuleConfigType value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasGetModuleConfigRequest() => $_has(6); + @$pb.TagNumber(7) + void clearGetModuleConfigRequest() => $_clearField(7); + + /// + /// Send the current Config in the response to this message. + @$pb.TagNumber(8) + $3.ModuleConfig get getModuleConfigResponse => $_getN(7); + @$pb.TagNumber(8) + set getModuleConfigResponse($3.ModuleConfig value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasGetModuleConfigResponse() => $_has(7); + @$pb.TagNumber(8) + void clearGetModuleConfigResponse() => $_clearField(8); + @$pb.TagNumber(8) + $3.ModuleConfig ensureGetModuleConfigResponse() => $_ensure(7); + + /// + /// Get the Canned Message Module messages in the response to this message. + @$pb.TagNumber(10) + $core.bool get getCannedMessageModuleMessagesRequest => $_getBF(8); + @$pb.TagNumber(10) + set getCannedMessageModuleMessagesRequest($core.bool value) => + $_setBool(8, value); + @$pb.TagNumber(10) + $core.bool hasGetCannedMessageModuleMessagesRequest() => $_has(8); + @$pb.TagNumber(10) + void clearGetCannedMessageModuleMessagesRequest() => $_clearField(10); + + /// + /// Get the Canned Message Module messages in the response to this message. + @$pb.TagNumber(11) + $core.String get getCannedMessageModuleMessagesResponse => $_getSZ(9); + @$pb.TagNumber(11) + set getCannedMessageModuleMessagesResponse($core.String value) => + $_setString(9, value); + @$pb.TagNumber(11) + $core.bool hasGetCannedMessageModuleMessagesResponse() => $_has(9); + @$pb.TagNumber(11) + void clearGetCannedMessageModuleMessagesResponse() => $_clearField(11); + + /// + /// Request the node to send device metadata (firmware, protobuf version, etc) + @$pb.TagNumber(12) + $core.bool get getDeviceMetadataRequest => $_getBF(10); + @$pb.TagNumber(12) + set getDeviceMetadataRequest($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(12) + $core.bool hasGetDeviceMetadataRequest() => $_has(10); + @$pb.TagNumber(12) + void clearGetDeviceMetadataRequest() => $_clearField(12); + + /// + /// Device metadata response + @$pb.TagNumber(13) + $1.DeviceMetadata get getDeviceMetadataResponse => $_getN(11); + @$pb.TagNumber(13) + set getDeviceMetadataResponse($1.DeviceMetadata value) => + $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasGetDeviceMetadataResponse() => $_has(11); + @$pb.TagNumber(13) + void clearGetDeviceMetadataResponse() => $_clearField(13); + @$pb.TagNumber(13) + $1.DeviceMetadata ensureGetDeviceMetadataResponse() => $_ensure(11); + + /// + /// Get the Ringtone in the response to this message. + @$pb.TagNumber(14) + $core.bool get getRingtoneRequest => $_getBF(12); + @$pb.TagNumber(14) + set getRingtoneRequest($core.bool value) => $_setBool(12, value); + @$pb.TagNumber(14) + $core.bool hasGetRingtoneRequest() => $_has(12); + @$pb.TagNumber(14) + void clearGetRingtoneRequest() => $_clearField(14); + + /// + /// Get the Ringtone in the response to this message. + @$pb.TagNumber(15) + $core.String get getRingtoneResponse => $_getSZ(13); + @$pb.TagNumber(15) + set getRingtoneResponse($core.String value) => $_setString(13, value); + @$pb.TagNumber(15) + $core.bool hasGetRingtoneResponse() => $_has(13); + @$pb.TagNumber(15) + void clearGetRingtoneResponse() => $_clearField(15); + + /// + /// Request the node to send it's connection status + @$pb.TagNumber(16) + $core.bool get getDeviceConnectionStatusRequest => $_getBF(14); + @$pb.TagNumber(16) + set getDeviceConnectionStatusRequest($core.bool value) => + $_setBool(14, value); + @$pb.TagNumber(16) + $core.bool hasGetDeviceConnectionStatusRequest() => $_has(14); + @$pb.TagNumber(16) + void clearGetDeviceConnectionStatusRequest() => $_clearField(16); + + /// + /// Device connection status response + @$pb.TagNumber(17) + $4.DeviceConnectionStatus get getDeviceConnectionStatusResponse => $_getN(15); + @$pb.TagNumber(17) + set getDeviceConnectionStatusResponse($4.DeviceConnectionStatus value) => + $_setField(17, value); + @$pb.TagNumber(17) + $core.bool hasGetDeviceConnectionStatusResponse() => $_has(15); + @$pb.TagNumber(17) + void clearGetDeviceConnectionStatusResponse() => $_clearField(17); + @$pb.TagNumber(17) + $4.DeviceConnectionStatus ensureGetDeviceConnectionStatusResponse() => + $_ensure(15); + + /// + /// Setup a node for licensed amateur (ham) radio operation + @$pb.TagNumber(18) + HamParameters get setHamMode => $_getN(16); + @$pb.TagNumber(18) + set setHamMode(HamParameters value) => $_setField(18, value); + @$pb.TagNumber(18) + $core.bool hasSetHamMode() => $_has(16); + @$pb.TagNumber(18) + void clearSetHamMode() => $_clearField(18); + @$pb.TagNumber(18) + HamParameters ensureSetHamMode() => $_ensure(16); + + /// + /// Get the mesh's nodes with their available gpio pins for RemoteHardware module use + @$pb.TagNumber(19) + $core.bool get getNodeRemoteHardwarePinsRequest => $_getBF(17); + @$pb.TagNumber(19) + set getNodeRemoteHardwarePinsRequest($core.bool value) => + $_setBool(17, value); + @$pb.TagNumber(19) + $core.bool hasGetNodeRemoteHardwarePinsRequest() => $_has(17); + @$pb.TagNumber(19) + void clearGetNodeRemoteHardwarePinsRequest() => $_clearField(19); + + /// + /// Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use + @$pb.TagNumber(20) + NodeRemoteHardwarePinsResponse get getNodeRemoteHardwarePinsResponse => + $_getN(18); + @$pb.TagNumber(20) + set getNodeRemoteHardwarePinsResponse(NodeRemoteHardwarePinsResponse value) => + $_setField(20, value); + @$pb.TagNumber(20) + $core.bool hasGetNodeRemoteHardwarePinsResponse() => $_has(18); + @$pb.TagNumber(20) + void clearGetNodeRemoteHardwarePinsResponse() => $_clearField(20); + @$pb.TagNumber(20) + NodeRemoteHardwarePinsResponse ensureGetNodeRemoteHardwarePinsResponse() => + $_ensure(18); + + /// + /// Enter (UF2) DFU mode + /// Only implemented on NRF52 currently + @$pb.TagNumber(21) + $core.bool get enterDfuModeRequest => $_getBF(19); + @$pb.TagNumber(21) + set enterDfuModeRequest($core.bool value) => $_setBool(19, value); + @$pb.TagNumber(21) + $core.bool hasEnterDfuModeRequest() => $_has(19); + @$pb.TagNumber(21) + void clearEnterDfuModeRequest() => $_clearField(21); + + /// + /// Delete the file by the specified path from the device + @$pb.TagNumber(22) + $core.String get deleteFileRequest => $_getSZ(20); + @$pb.TagNumber(22) + set deleteFileRequest($core.String value) => $_setString(20, value); + @$pb.TagNumber(22) + $core.bool hasDeleteFileRequest() => $_has(20); + @$pb.TagNumber(22) + void clearDeleteFileRequest() => $_clearField(22); + + /// + /// Set zero and offset for scale chips + @$pb.TagNumber(23) + $core.int get setScale => $_getIZ(21); + @$pb.TagNumber(23) + set setScale($core.int value) => $_setUnsignedInt32(21, value); + @$pb.TagNumber(23) + $core.bool hasSetScale() => $_has(21); + @$pb.TagNumber(23) + void clearSetScale() => $_clearField(23); + + /// + /// Backup the node's preferences + @$pb.TagNumber(24) + AdminMessage_BackupLocation get backupPreferences => $_getN(22); + @$pb.TagNumber(24) + set backupPreferences(AdminMessage_BackupLocation value) => + $_setField(24, value); + @$pb.TagNumber(24) + $core.bool hasBackupPreferences() => $_has(22); + @$pb.TagNumber(24) + void clearBackupPreferences() => $_clearField(24); + + /// + /// Restore the node's preferences + @$pb.TagNumber(25) + AdminMessage_BackupLocation get restorePreferences => $_getN(23); + @$pb.TagNumber(25) + set restorePreferences(AdminMessage_BackupLocation value) => + $_setField(25, value); + @$pb.TagNumber(25) + $core.bool hasRestorePreferences() => $_has(23); + @$pb.TagNumber(25) + void clearRestorePreferences() => $_clearField(25); + + /// + /// Remove backups of the node's preferences + @$pb.TagNumber(26) + AdminMessage_BackupLocation get removeBackupPreferences => $_getN(24); + @$pb.TagNumber(26) + set removeBackupPreferences(AdminMessage_BackupLocation value) => + $_setField(26, value); + @$pb.TagNumber(26) + $core.bool hasRemoveBackupPreferences() => $_has(24); + @$pb.TagNumber(26) + void clearRemoveBackupPreferences() => $_clearField(26); + + /// + /// Send an input event to the node. + /// This is used to trigger physical input events like button presses, touch events, etc. + @$pb.TagNumber(27) + AdminMessage_InputEvent get sendInputEvent => $_getN(25); + @$pb.TagNumber(27) + set sendInputEvent(AdminMessage_InputEvent value) => $_setField(27, value); + @$pb.TagNumber(27) + $core.bool hasSendInputEvent() => $_has(25); + @$pb.TagNumber(27) + void clearSendInputEvent() => $_clearField(27); + @$pb.TagNumber(27) + AdminMessage_InputEvent ensureSendInputEvent() => $_ensure(25); + + /// + /// Set the owner for this node + @$pb.TagNumber(32) + $1.User get setOwner => $_getN(26); + @$pb.TagNumber(32) + set setOwner($1.User value) => $_setField(32, value); + @$pb.TagNumber(32) + $core.bool hasSetOwner() => $_has(26); + @$pb.TagNumber(32) + void clearSetOwner() => $_clearField(32); + @$pb.TagNumber(32) + $1.User ensureSetOwner() => $_ensure(26); + + /// + /// Set channels (using the new API). + /// A special channel is the "primary channel". + /// The other records are secondary channels. + /// Note: only one channel can be marked as primary. + /// If the client sets a particular channel to be primary, the previous channel will be set to SECONDARY automatically. + @$pb.TagNumber(33) + $0.Channel get setChannel => $_getN(27); + @$pb.TagNumber(33) + set setChannel($0.Channel value) => $_setField(33, value); + @$pb.TagNumber(33) + $core.bool hasSetChannel() => $_has(27); + @$pb.TagNumber(33) + void clearSetChannel() => $_clearField(33); + @$pb.TagNumber(33) + $0.Channel ensureSetChannel() => $_ensure(27); + + /// + /// Set the current Config + @$pb.TagNumber(34) + $2.Config get setConfig => $_getN(28); + @$pb.TagNumber(34) + set setConfig($2.Config value) => $_setField(34, value); + @$pb.TagNumber(34) + $core.bool hasSetConfig() => $_has(28); + @$pb.TagNumber(34) + void clearSetConfig() => $_clearField(34); + @$pb.TagNumber(34) + $2.Config ensureSetConfig() => $_ensure(28); + + /// + /// Set the current Config + @$pb.TagNumber(35) + $3.ModuleConfig get setModuleConfig => $_getN(29); + @$pb.TagNumber(35) + set setModuleConfig($3.ModuleConfig value) => $_setField(35, value); + @$pb.TagNumber(35) + $core.bool hasSetModuleConfig() => $_has(29); + @$pb.TagNumber(35) + void clearSetModuleConfig() => $_clearField(35); + @$pb.TagNumber(35) + $3.ModuleConfig ensureSetModuleConfig() => $_ensure(29); + + /// + /// Set the Canned Message Module messages text. + @$pb.TagNumber(36) + $core.String get setCannedMessageModuleMessages => $_getSZ(30); + @$pb.TagNumber(36) + set setCannedMessageModuleMessages($core.String value) => + $_setString(30, value); + @$pb.TagNumber(36) + $core.bool hasSetCannedMessageModuleMessages() => $_has(30); + @$pb.TagNumber(36) + void clearSetCannedMessageModuleMessages() => $_clearField(36); + + /// + /// Set the ringtone for ExternalNotification. + @$pb.TagNumber(37) + $core.String get setRingtoneMessage => $_getSZ(31); + @$pb.TagNumber(37) + set setRingtoneMessage($core.String value) => $_setString(31, value); + @$pb.TagNumber(37) + $core.bool hasSetRingtoneMessage() => $_has(31); + @$pb.TagNumber(37) + void clearSetRingtoneMessage() => $_clearField(37); + + /// + /// Remove the node by the specified node-num from the NodeDB on the device + @$pb.TagNumber(38) + $core.int get removeByNodenum => $_getIZ(32); + @$pb.TagNumber(38) + set removeByNodenum($core.int value) => $_setUnsignedInt32(32, value); + @$pb.TagNumber(38) + $core.bool hasRemoveByNodenum() => $_has(32); + @$pb.TagNumber(38) + void clearRemoveByNodenum() => $_clearField(38); + + /// + /// Set specified node-num to be favorited on the NodeDB on the device + @$pb.TagNumber(39) + $core.int get setFavoriteNode => $_getIZ(33); + @$pb.TagNumber(39) + set setFavoriteNode($core.int value) => $_setUnsignedInt32(33, value); + @$pb.TagNumber(39) + $core.bool hasSetFavoriteNode() => $_has(33); + @$pb.TagNumber(39) + void clearSetFavoriteNode() => $_clearField(39); + + /// + /// Set specified node-num to be un-favorited on the NodeDB on the device + @$pb.TagNumber(40) + $core.int get removeFavoriteNode => $_getIZ(34); + @$pb.TagNumber(40) + set removeFavoriteNode($core.int value) => $_setUnsignedInt32(34, value); + @$pb.TagNumber(40) + $core.bool hasRemoveFavoriteNode() => $_has(34); + @$pb.TagNumber(40) + void clearRemoveFavoriteNode() => $_clearField(40); + + /// + /// Set fixed position data on the node and then set the position.fixed_position = true + @$pb.TagNumber(41) + $1.Position get setFixedPosition => $_getN(35); + @$pb.TagNumber(41) + set setFixedPosition($1.Position value) => $_setField(41, value); + @$pb.TagNumber(41) + $core.bool hasSetFixedPosition() => $_has(35); + @$pb.TagNumber(41) + void clearSetFixedPosition() => $_clearField(41); + @$pb.TagNumber(41) + $1.Position ensureSetFixedPosition() => $_ensure(35); + + /// + /// Clear fixed position coordinates and then set position.fixed_position = false + @$pb.TagNumber(42) + $core.bool get removeFixedPosition => $_getBF(36); + @$pb.TagNumber(42) + set removeFixedPosition($core.bool value) => $_setBool(36, value); + @$pb.TagNumber(42) + $core.bool hasRemoveFixedPosition() => $_has(36); + @$pb.TagNumber(42) + void clearRemoveFixedPosition() => $_clearField(42); + + /// + /// Set time only on the node + /// Convenience method to set the time on the node (as Net quality) without any other position data + @$pb.TagNumber(43) + $core.int get setTimeOnly => $_getIZ(37); + @$pb.TagNumber(43) + set setTimeOnly($core.int value) => $_setUnsignedInt32(37, value); + @$pb.TagNumber(43) + $core.bool hasSetTimeOnly() => $_has(37); + @$pb.TagNumber(43) + void clearSetTimeOnly() => $_clearField(43); + + /// + /// Tell the node to send the stored ui data. + @$pb.TagNumber(44) + $core.bool get getUiConfigRequest => $_getBF(38); + @$pb.TagNumber(44) + set getUiConfigRequest($core.bool value) => $_setBool(38, value); + @$pb.TagNumber(44) + $core.bool hasGetUiConfigRequest() => $_has(38); + @$pb.TagNumber(44) + void clearGetUiConfigRequest() => $_clearField(44); + + /// + /// Reply stored device ui data. + @$pb.TagNumber(45) + $5.DeviceUIConfig get getUiConfigResponse => $_getN(39); + @$pb.TagNumber(45) + set getUiConfigResponse($5.DeviceUIConfig value) => $_setField(45, value); + @$pb.TagNumber(45) + $core.bool hasGetUiConfigResponse() => $_has(39); + @$pb.TagNumber(45) + void clearGetUiConfigResponse() => $_clearField(45); + @$pb.TagNumber(45) + $5.DeviceUIConfig ensureGetUiConfigResponse() => $_ensure(39); + + /// + /// Tell the node to store UI data persistently. + @$pb.TagNumber(46) + $5.DeviceUIConfig get storeUiConfig => $_getN(40); + @$pb.TagNumber(46) + set storeUiConfig($5.DeviceUIConfig value) => $_setField(46, value); + @$pb.TagNumber(46) + $core.bool hasStoreUiConfig() => $_has(40); + @$pb.TagNumber(46) + void clearStoreUiConfig() => $_clearField(46); + @$pb.TagNumber(46) + $5.DeviceUIConfig ensureStoreUiConfig() => $_ensure(40); + + /// + /// Set specified node-num to be ignored on the NodeDB on the device + @$pb.TagNumber(47) + $core.int get setIgnoredNode => $_getIZ(41); + @$pb.TagNumber(47) + set setIgnoredNode($core.int value) => $_setUnsignedInt32(41, value); + @$pb.TagNumber(47) + $core.bool hasSetIgnoredNode() => $_has(41); + @$pb.TagNumber(47) + void clearSetIgnoredNode() => $_clearField(47); + + /// + /// Set specified node-num to be un-ignored on the NodeDB on the device + @$pb.TagNumber(48) + $core.int get removeIgnoredNode => $_getIZ(42); + @$pb.TagNumber(48) + set removeIgnoredNode($core.int value) => $_setUnsignedInt32(42, value); + @$pb.TagNumber(48) + $core.bool hasRemoveIgnoredNode() => $_has(42); + @$pb.TagNumber(48) + void clearRemoveIgnoredNode() => $_clearField(48); + + /// + /// Begins an edit transaction for config, module config, owner, and channel settings changes + /// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) + @$pb.TagNumber(64) + $core.bool get beginEditSettings => $_getBF(43); + @$pb.TagNumber(64) + set beginEditSettings($core.bool value) => $_setBool(43, value); + @$pb.TagNumber(64) + $core.bool hasBeginEditSettings() => $_has(43); + @$pb.TagNumber(64) + void clearBeginEditSettings() => $_clearField(64); + + /// + /// Commits an open transaction for any edits made to config, module config, owner, and channel settings + @$pb.TagNumber(65) + $core.bool get commitEditSettings => $_getBF(44); + @$pb.TagNumber(65) + set commitEditSettings($core.bool value) => $_setBool(44, value); + @$pb.TagNumber(65) + $core.bool hasCommitEditSettings() => $_has(44); + @$pb.TagNumber(65) + void clearCommitEditSettings() => $_clearField(65); + + /// + /// Add a contact (User) to the nodedb + @$pb.TagNumber(66) + SharedContact get addContact => $_getN(45); + @$pb.TagNumber(66) + set addContact(SharedContact value) => $_setField(66, value); + @$pb.TagNumber(66) + $core.bool hasAddContact() => $_has(45); + @$pb.TagNumber(66) + void clearAddContact() => $_clearField(66); + @$pb.TagNumber(66) + SharedContact ensureAddContact() => $_ensure(45); + + /// + /// Initiate or respond to a key verification request + @$pb.TagNumber(67) + KeyVerificationAdmin get keyVerification => $_getN(46); + @$pb.TagNumber(67) + set keyVerification(KeyVerificationAdmin value) => $_setField(67, value); + @$pb.TagNumber(67) + $core.bool hasKeyVerification() => $_has(46); + @$pb.TagNumber(67) + void clearKeyVerification() => $_clearField(67); + @$pb.TagNumber(67) + KeyVerificationAdmin ensureKeyVerification() => $_ensure(46); + + /// + /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. + @$pb.TagNumber(94) + $core.int get factoryResetDevice => $_getIZ(47); + @$pb.TagNumber(94) + set factoryResetDevice($core.int value) => $_setSignedInt32(47, value); + @$pb.TagNumber(94) + $core.bool hasFactoryResetDevice() => $_has(47); + @$pb.TagNumber(94) + void clearFactoryResetDevice() => $_clearField(94); + + /// + /// Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) + /// Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + @$pb.TagNumber(95) + $core.int get rebootOtaSeconds => $_getIZ(48); + @$pb.TagNumber(95) + set rebootOtaSeconds($core.int value) => $_setSignedInt32(48, value); + @$pb.TagNumber(95) + $core.bool hasRebootOtaSeconds() => $_has(48); + @$pb.TagNumber(95) + void clearRebootOtaSeconds() => $_clearField(95); + + /// + /// This message is only supported for the simulator Portduino build. + /// If received the simulator will exit successfully. + @$pb.TagNumber(96) + $core.bool get exitSimulator => $_getBF(49); + @$pb.TagNumber(96) + set exitSimulator($core.bool value) => $_setBool(49, value); + @$pb.TagNumber(96) + $core.bool hasExitSimulator() => $_has(49); + @$pb.TagNumber(96) + void clearExitSimulator() => $_clearField(96); + + /// + /// Tell the node to reboot in this many seconds (or <0 to cancel reboot) + @$pb.TagNumber(97) + $core.int get rebootSeconds => $_getIZ(50); + @$pb.TagNumber(97) + set rebootSeconds($core.int value) => $_setSignedInt32(50, value); + @$pb.TagNumber(97) + $core.bool hasRebootSeconds() => $_has(50); + @$pb.TagNumber(97) + void clearRebootSeconds() => $_clearField(97); + + /// + /// Tell the node to shutdown in this many seconds (or <0 to cancel shutdown) + @$pb.TagNumber(98) + $core.int get shutdownSeconds => $_getIZ(51); + @$pb.TagNumber(98) + set shutdownSeconds($core.int value) => $_setSignedInt32(51, value); + @$pb.TagNumber(98) + $core.bool hasShutdownSeconds() => $_has(51); + @$pb.TagNumber(98) + void clearShutdownSeconds() => $_clearField(98); + + /// + /// Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. + @$pb.TagNumber(99) + $core.int get factoryResetConfig => $_getIZ(52); + @$pb.TagNumber(99) + set factoryResetConfig($core.int value) => $_setSignedInt32(52, value); + @$pb.TagNumber(99) + $core.bool hasFactoryResetConfig() => $_has(52); + @$pb.TagNumber(99) + void clearFactoryResetConfig() => $_clearField(99); + + /// + /// Tell the node to reset the nodedb. + @$pb.TagNumber(100) + $core.int get nodedbReset => $_getIZ(53); + @$pb.TagNumber(100) + set nodedbReset($core.int value) => $_setSignedInt32(53, value); + @$pb.TagNumber(100) + $core.bool hasNodedbReset() => $_has(53); + @$pb.TagNumber(100) + void clearNodedbReset() => $_clearField(100); + + /// + /// The node generates this key and sends it with any get_x_response packets. + /// The client MUST include the same key with any set_x commands. Key expires after 300 seconds. + /// Prevents replay attacks for admin messages. + @$pb.TagNumber(101) + $core.List<$core.int> get sessionPasskey => $_getN(54); + @$pb.TagNumber(101) + set sessionPasskey($core.List<$core.int> value) => $_setBytes(54, value); + @$pb.TagNumber(101) + $core.bool hasSessionPasskey() => $_has(54); + @$pb.TagNumber(101) + void clearSessionPasskey() => $_clearField(101); +} + +/// +/// Parameters for setting up Meshtastic for ameteur radio usage +class HamParameters extends $pb.GeneratedMessage { + factory HamParameters({ + $core.String? callSign, + $core.int? txPower, + $core.double? frequency, + $core.String? shortName, + }) { + final result = create(); + if (callSign != null) result.callSign = callSign; + if (txPower != null) result.txPower = txPower; + if (frequency != null) result.frequency = frequency; + if (shortName != null) result.shortName = shortName; + return result; + } + + HamParameters._(); + + factory HamParameters.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory HamParameters.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'HamParameters', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'callSign') + ..a<$core.int>(2, _omitFieldNames ? '' : 'txPower', $pb.PbFieldType.O3) + ..a<$core.double>(3, _omitFieldNames ? '' : 'frequency', $pb.PbFieldType.OF) + ..aOS(4, _omitFieldNames ? '' : 'shortName') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HamParameters clone() => HamParameters()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HamParameters copyWith(void Function(HamParameters) updates) => + super.copyWith((message) => updates(message as HamParameters)) + as HamParameters; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HamParameters create() => HamParameters._(); + @$core.override + HamParameters createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static HamParameters getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static HamParameters? _defaultInstance; + + /// + /// Amateur radio call sign, eg. KD2ABC + @$pb.TagNumber(1) + $core.String get callSign => $_getSZ(0); + @$pb.TagNumber(1) + set callSign($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasCallSign() => $_has(0); + @$pb.TagNumber(1) + void clearCallSign() => $_clearField(1); + + /// + /// Transmit power in dBm at the LoRA transceiver, not including any amplification + @$pb.TagNumber(2) + $core.int get txPower => $_getIZ(1); + @$pb.TagNumber(2) + set txPower($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasTxPower() => $_has(1); + @$pb.TagNumber(2) + void clearTxPower() => $_clearField(2); + + /// + /// The selected frequency of LoRA operation + /// Please respect your local laws, regulations, and band plans. + /// Ensure your radio is capable of operating of the selected frequency before setting this. + @$pb.TagNumber(3) + $core.double get frequency => $_getN(2); + @$pb.TagNumber(3) + set frequency($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasFrequency() => $_has(2); + @$pb.TagNumber(3) + void clearFrequency() => $_clearField(3); + + /// + /// Optional short name of user + @$pb.TagNumber(4) + $core.String get shortName => $_getSZ(3); + @$pb.TagNumber(4) + set shortName($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasShortName() => $_has(3); + @$pb.TagNumber(4) + void clearShortName() => $_clearField(4); +} + +/// +/// Response envelope for node_remote_hardware_pins +class NodeRemoteHardwarePinsResponse extends $pb.GeneratedMessage { + factory NodeRemoteHardwarePinsResponse({ + $core.Iterable<$1.NodeRemoteHardwarePin>? nodeRemoteHardwarePins, + }) { + final result = create(); + if (nodeRemoteHardwarePins != null) + result.nodeRemoteHardwarePins.addAll(nodeRemoteHardwarePins); + return result; + } + + NodeRemoteHardwarePinsResponse._(); + + factory NodeRemoteHardwarePinsResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeRemoteHardwarePinsResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeRemoteHardwarePinsResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..pc<$1.NodeRemoteHardwarePin>( + 1, _omitFieldNames ? '' : 'nodeRemoteHardwarePins', $pb.PbFieldType.PM, + subBuilder: $1.NodeRemoteHardwarePin.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeRemoteHardwarePinsResponse clone() => + NodeRemoteHardwarePinsResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeRemoteHardwarePinsResponse copyWith( + void Function(NodeRemoteHardwarePinsResponse) updates) => + super.copyWith( + (message) => updates(message as NodeRemoteHardwarePinsResponse)) + as NodeRemoteHardwarePinsResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeRemoteHardwarePinsResponse create() => + NodeRemoteHardwarePinsResponse._(); + @$core.override + NodeRemoteHardwarePinsResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeRemoteHardwarePinsResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeRemoteHardwarePinsResponse? _defaultInstance; + + /// + /// Nodes and their respective remote hardware GPIO pins + @$pb.TagNumber(1) + $pb.PbList<$1.NodeRemoteHardwarePin> get nodeRemoteHardwarePins => + $_getList(0); +} + +class SharedContact extends $pb.GeneratedMessage { + factory SharedContact({ + $core.int? nodeNum, + $1.User? user, + $core.bool? shouldIgnore, + }) { + final result = create(); + if (nodeNum != null) result.nodeNum = nodeNum; + if (user != null) result.user = user; + if (shouldIgnore != null) result.shouldIgnore = shouldIgnore; + return result; + } + + SharedContact._(); + + factory SharedContact.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SharedContact.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SharedContact', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'nodeNum', $pb.PbFieldType.OU3) + ..aOM<$1.User>(2, _omitFieldNames ? '' : 'user', subBuilder: $1.User.create) + ..aOB(3, _omitFieldNames ? '' : 'shouldIgnore') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SharedContact clone() => SharedContact()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SharedContact copyWith(void Function(SharedContact) updates) => + super.copyWith((message) => updates(message as SharedContact)) + as SharedContact; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SharedContact create() => SharedContact._(); + @$core.override + SharedContact createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SharedContact getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SharedContact? _defaultInstance; + + /// + /// The node number of the contact + @$pb.TagNumber(1) + $core.int get nodeNum => $_getIZ(0); + @$pb.TagNumber(1) + set nodeNum($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNodeNum() => $_has(0); + @$pb.TagNumber(1) + void clearNodeNum() => $_clearField(1); + + /// + /// The User of the contact + @$pb.TagNumber(2) + $1.User get user => $_getN(1); + @$pb.TagNumber(2) + set user($1.User value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasUser() => $_has(1); + @$pb.TagNumber(2) + void clearUser() => $_clearField(2); + @$pb.TagNumber(2) + $1.User ensureUser() => $_ensure(1); + + /// + /// Add this contact to the blocked / ignored list + @$pb.TagNumber(3) + $core.bool get shouldIgnore => $_getBF(2); + @$pb.TagNumber(3) + set shouldIgnore($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasShouldIgnore() => $_has(2); + @$pb.TagNumber(3) + void clearShouldIgnore() => $_clearField(3); +} + +/// +/// This message is used by a client to initiate or complete a key verification +class KeyVerificationAdmin extends $pb.GeneratedMessage { + factory KeyVerificationAdmin({ + KeyVerificationAdmin_MessageType? messageType, + $core.int? remoteNodenum, + $fixnum.Int64? nonce, + $core.int? securityNumber, + }) { + final result = create(); + if (messageType != null) result.messageType = messageType; + if (remoteNodenum != null) result.remoteNodenum = remoteNodenum; + if (nonce != null) result.nonce = nonce; + if (securityNumber != null) result.securityNumber = securityNumber; + return result; + } + + KeyVerificationAdmin._(); + + factory KeyVerificationAdmin.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory KeyVerificationAdmin.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'KeyVerificationAdmin', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e( + 1, _omitFieldNames ? '' : 'messageType', $pb.PbFieldType.OE, + defaultOrMaker: KeyVerificationAdmin_MessageType.INITIATE_VERIFICATION, + valueOf: KeyVerificationAdmin_MessageType.valueOf, + enumValues: KeyVerificationAdmin_MessageType.values) + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'remoteNodenum', $pb.PbFieldType.OU3) + ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'securityNumber', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationAdmin clone() => + KeyVerificationAdmin()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationAdmin copyWith(void Function(KeyVerificationAdmin) updates) => + super.copyWith((message) => updates(message as KeyVerificationAdmin)) + as KeyVerificationAdmin; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static KeyVerificationAdmin create() => KeyVerificationAdmin._(); + @$core.override + KeyVerificationAdmin createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static KeyVerificationAdmin getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static KeyVerificationAdmin? _defaultInstance; + + @$pb.TagNumber(1) + KeyVerificationAdmin_MessageType get messageType => $_getN(0); + @$pb.TagNumber(1) + set messageType(KeyVerificationAdmin_MessageType value) => + $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasMessageType() => $_has(0); + @$pb.TagNumber(1) + void clearMessageType() => $_clearField(1); + + /// + /// The nodenum we're requesting + @$pb.TagNumber(2) + $core.int get remoteNodenum => $_getIZ(1); + @$pb.TagNumber(2) + set remoteNodenum($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasRemoteNodenum() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteNodenum() => $_clearField(2); + + /// + /// The nonce is used to track the connection + @$pb.TagNumber(3) + $fixnum.Int64 get nonce => $_getI64(2); + @$pb.TagNumber(3) + set nonce($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasNonce() => $_has(2); + @$pb.TagNumber(3) + void clearNonce() => $_clearField(3); + + /// + /// The 4 digit code generated by the remote node, and communicated outside the mesh + @$pb.TagNumber(4) + $core.int get securityNumber => $_getIZ(3); + @$pb.TagNumber(4) + set securityNumber($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasSecurityNumber() => $_has(3); + @$pb.TagNumber(4) + void clearSecurityNumber() => $_clearField(4); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/admin.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/admin.pbenum.dart new file mode 100644 index 000000000..23a3c11b0 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/admin.pbenum.dart @@ -0,0 +1,264 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/admin.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// TODO: REPLACE +class AdminMessage_ConfigType extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType DEVICE_CONFIG = + AdminMessage_ConfigType._(0, _omitEnumNames ? '' : 'DEVICE_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType POSITION_CONFIG = + AdminMessage_ConfigType._(1, _omitEnumNames ? '' : 'POSITION_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType POWER_CONFIG = + AdminMessage_ConfigType._(2, _omitEnumNames ? '' : 'POWER_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType NETWORK_CONFIG = + AdminMessage_ConfigType._(3, _omitEnumNames ? '' : 'NETWORK_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType DISPLAY_CONFIG = + AdminMessage_ConfigType._(4, _omitEnumNames ? '' : 'DISPLAY_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType LORA_CONFIG = + AdminMessage_ConfigType._(5, _omitEnumNames ? '' : 'LORA_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType BLUETOOTH_CONFIG = + AdminMessage_ConfigType._(6, _omitEnumNames ? '' : 'BLUETOOTH_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ConfigType SECURITY_CONFIG = + AdminMessage_ConfigType._(7, _omitEnumNames ? '' : 'SECURITY_CONFIG'); + + /// + /// Session key config + static const AdminMessage_ConfigType SESSIONKEY_CONFIG = + AdminMessage_ConfigType._(8, _omitEnumNames ? '' : 'SESSIONKEY_CONFIG'); + + /// + /// device-ui config + static const AdminMessage_ConfigType DEVICEUI_CONFIG = + AdminMessage_ConfigType._(9, _omitEnumNames ? '' : 'DEVICEUI_CONFIG'); + + static const $core.List values = + [ + DEVICE_CONFIG, + POSITION_CONFIG, + POWER_CONFIG, + NETWORK_CONFIG, + DISPLAY_CONFIG, + LORA_CONFIG, + BLUETOOTH_CONFIG, + SECURITY_CONFIG, + SESSIONKEY_CONFIG, + DEVICEUI_CONFIG, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 9); + static AdminMessage_ConfigType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const AdminMessage_ConfigType._(super.value, super.name); +} + +/// +/// TODO: REPLACE +class AdminMessage_ModuleConfigType extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType MQTT_CONFIG = + AdminMessage_ModuleConfigType._(0, _omitEnumNames ? '' : 'MQTT_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType SERIAL_CONFIG = + AdminMessage_ModuleConfigType._(1, _omitEnumNames ? '' : 'SERIAL_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType EXTNOTIF_CONFIG = + AdminMessage_ModuleConfigType._( + 2, _omitEnumNames ? '' : 'EXTNOTIF_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType STOREFORWARD_CONFIG = + AdminMessage_ModuleConfigType._( + 3, _omitEnumNames ? '' : 'STOREFORWARD_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType RANGETEST_CONFIG = + AdminMessage_ModuleConfigType._( + 4, _omitEnumNames ? '' : 'RANGETEST_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType TELEMETRY_CONFIG = + AdminMessage_ModuleConfigType._( + 5, _omitEnumNames ? '' : 'TELEMETRY_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType CANNEDMSG_CONFIG = + AdminMessage_ModuleConfigType._( + 6, _omitEnumNames ? '' : 'CANNEDMSG_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType AUDIO_CONFIG = + AdminMessage_ModuleConfigType._(7, _omitEnumNames ? '' : 'AUDIO_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType REMOTEHARDWARE_CONFIG = + AdminMessage_ModuleConfigType._( + 8, _omitEnumNames ? '' : 'REMOTEHARDWARE_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType NEIGHBORINFO_CONFIG = + AdminMessage_ModuleConfigType._( + 9, _omitEnumNames ? '' : 'NEIGHBORINFO_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType AMBIENTLIGHTING_CONFIG = + AdminMessage_ModuleConfigType._( + 10, _omitEnumNames ? '' : 'AMBIENTLIGHTING_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType DETECTIONSENSOR_CONFIG = + AdminMessage_ModuleConfigType._( + 11, _omitEnumNames ? '' : 'DETECTIONSENSOR_CONFIG'); + + /// + /// TODO: REPLACE + static const AdminMessage_ModuleConfigType PAXCOUNTER_CONFIG = + AdminMessage_ModuleConfigType._( + 12, _omitEnumNames ? '' : 'PAXCOUNTER_CONFIG'); + + static const $core.List values = + [ + MQTT_CONFIG, + SERIAL_CONFIG, + EXTNOTIF_CONFIG, + STOREFORWARD_CONFIG, + RANGETEST_CONFIG, + TELEMETRY_CONFIG, + CANNEDMSG_CONFIG, + AUDIO_CONFIG, + REMOTEHARDWARE_CONFIG, + NEIGHBORINFO_CONFIG, + AMBIENTLIGHTING_CONFIG, + DETECTIONSENSOR_CONFIG, + PAXCOUNTER_CONFIG, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 12); + static AdminMessage_ModuleConfigType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const AdminMessage_ModuleConfigType._(super.value, super.name); +} + +class AdminMessage_BackupLocation extends $pb.ProtobufEnum { + /// + /// Backup to the internal flash + static const AdminMessage_BackupLocation FLASH = + AdminMessage_BackupLocation._(0, _omitEnumNames ? '' : 'FLASH'); + + /// + /// Backup to the SD card + static const AdminMessage_BackupLocation SD = + AdminMessage_BackupLocation._(1, _omitEnumNames ? '' : 'SD'); + + static const $core.List values = + [ + FLASH, + SD, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 1); + static AdminMessage_BackupLocation? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const AdminMessage_BackupLocation._(super.value, super.name); +} + +/// +/// Three stages of this request. +class KeyVerificationAdmin_MessageType extends $pb.ProtobufEnum { + /// + /// This is the first stage, where a client initiates + static const KeyVerificationAdmin_MessageType INITIATE_VERIFICATION = + KeyVerificationAdmin_MessageType._( + 0, _omitEnumNames ? '' : 'INITIATE_VERIFICATION'); + + /// + /// After the nonce has been returned over the mesh, the client prompts for the security number + /// And uses this message to provide it to the node. + static const KeyVerificationAdmin_MessageType PROVIDE_SECURITY_NUMBER = + KeyVerificationAdmin_MessageType._( + 1, _omitEnumNames ? '' : 'PROVIDE_SECURITY_NUMBER'); + + /// + /// Once the user has compared the verification message, this message notifies the node. + static const KeyVerificationAdmin_MessageType DO_VERIFY = + KeyVerificationAdmin_MessageType._(2, _omitEnumNames ? '' : 'DO_VERIFY'); + + /// + /// This is the cancel path, can be taken at any point + static const KeyVerificationAdmin_MessageType DO_NOT_VERIFY = + KeyVerificationAdmin_MessageType._( + 3, _omitEnumNames ? '' : 'DO_NOT_VERIFY'); + + static const $core.List values = + [ + INITIATE_VERIFICATION, + PROVIDE_SECURITY_NUMBER, + DO_VERIFY, + DO_NOT_VERIFY, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 3); + static KeyVerificationAdmin_MessageType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const KeyVerificationAdmin_MessageType._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/admin.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/admin.pbjson.dart new file mode 100644 index 000000000..498db6196 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/admin.pbjson.dart @@ -0,0 +1,731 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/admin.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use adminMessageDescriptor instead') +const AdminMessage$json = { + '1': 'AdminMessage', + '2': [ + {'1': 'session_passkey', '3': 101, '4': 1, '5': 12, '10': 'sessionPasskey'}, + { + '1': 'get_channel_request', + '3': 1, + '4': 1, + '5': 13, + '9': 0, + '10': 'getChannelRequest' + }, + { + '1': 'get_channel_response', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.Channel', + '9': 0, + '10': 'getChannelResponse' + }, + { + '1': 'get_owner_request', + '3': 3, + '4': 1, + '5': 8, + '9': 0, + '10': 'getOwnerRequest' + }, + { + '1': 'get_owner_response', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.User', + '9': 0, + '10': 'getOwnerResponse' + }, + { + '1': 'get_config_request', + '3': 5, + '4': 1, + '5': 14, + '6': '.meshtastic.AdminMessage.ConfigType', + '9': 0, + '10': 'getConfigRequest' + }, + { + '1': 'get_config_response', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.Config', + '9': 0, + '10': 'getConfigResponse' + }, + { + '1': 'get_module_config_request', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.AdminMessage.ModuleConfigType', + '9': 0, + '10': 'getModuleConfigRequest' + }, + { + '1': 'get_module_config_response', + '3': 8, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig', + '9': 0, + '10': 'getModuleConfigResponse' + }, + { + '1': 'get_canned_message_module_messages_request', + '3': 10, + '4': 1, + '5': 8, + '9': 0, + '10': 'getCannedMessageModuleMessagesRequest' + }, + { + '1': 'get_canned_message_module_messages_response', + '3': 11, + '4': 1, + '5': 9, + '9': 0, + '10': 'getCannedMessageModuleMessagesResponse' + }, + { + '1': 'get_device_metadata_request', + '3': 12, + '4': 1, + '5': 8, + '9': 0, + '10': 'getDeviceMetadataRequest' + }, + { + '1': 'get_device_metadata_response', + '3': 13, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceMetadata', + '9': 0, + '10': 'getDeviceMetadataResponse' + }, + { + '1': 'get_ringtone_request', + '3': 14, + '4': 1, + '5': 8, + '9': 0, + '10': 'getRingtoneRequest' + }, + { + '1': 'get_ringtone_response', + '3': 15, + '4': 1, + '5': 9, + '9': 0, + '10': 'getRingtoneResponse' + }, + { + '1': 'get_device_connection_status_request', + '3': 16, + '4': 1, + '5': 8, + '9': 0, + '10': 'getDeviceConnectionStatusRequest' + }, + { + '1': 'get_device_connection_status_response', + '3': 17, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceConnectionStatus', + '9': 0, + '10': 'getDeviceConnectionStatusResponse' + }, + { + '1': 'set_ham_mode', + '3': 18, + '4': 1, + '5': 11, + '6': '.meshtastic.HamParameters', + '9': 0, + '10': 'setHamMode' + }, + { + '1': 'get_node_remote_hardware_pins_request', + '3': 19, + '4': 1, + '5': 8, + '9': 0, + '10': 'getNodeRemoteHardwarePinsRequest' + }, + { + '1': 'get_node_remote_hardware_pins_response', + '3': 20, + '4': 1, + '5': 11, + '6': '.meshtastic.NodeRemoteHardwarePinsResponse', + '9': 0, + '10': 'getNodeRemoteHardwarePinsResponse' + }, + { + '1': 'enter_dfu_mode_request', + '3': 21, + '4': 1, + '5': 8, + '9': 0, + '10': 'enterDfuModeRequest' + }, + { + '1': 'delete_file_request', + '3': 22, + '4': 1, + '5': 9, + '9': 0, + '10': 'deleteFileRequest' + }, + {'1': 'set_scale', '3': 23, '4': 1, '5': 13, '9': 0, '10': 'setScale'}, + { + '1': 'backup_preferences', + '3': 24, + '4': 1, + '5': 14, + '6': '.meshtastic.AdminMessage.BackupLocation', + '9': 0, + '10': 'backupPreferences' + }, + { + '1': 'restore_preferences', + '3': 25, + '4': 1, + '5': 14, + '6': '.meshtastic.AdminMessage.BackupLocation', + '9': 0, + '10': 'restorePreferences' + }, + { + '1': 'remove_backup_preferences', + '3': 26, + '4': 1, + '5': 14, + '6': '.meshtastic.AdminMessage.BackupLocation', + '9': 0, + '10': 'removeBackupPreferences' + }, + { + '1': 'send_input_event', + '3': 27, + '4': 1, + '5': 11, + '6': '.meshtastic.AdminMessage.InputEvent', + '9': 0, + '10': 'sendInputEvent' + }, + { + '1': 'set_owner', + '3': 32, + '4': 1, + '5': 11, + '6': '.meshtastic.User', + '9': 0, + '10': 'setOwner' + }, + { + '1': 'set_channel', + '3': 33, + '4': 1, + '5': 11, + '6': '.meshtastic.Channel', + '9': 0, + '10': 'setChannel' + }, + { + '1': 'set_config', + '3': 34, + '4': 1, + '5': 11, + '6': '.meshtastic.Config', + '9': 0, + '10': 'setConfig' + }, + { + '1': 'set_module_config', + '3': 35, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig', + '9': 0, + '10': 'setModuleConfig' + }, + { + '1': 'set_canned_message_module_messages', + '3': 36, + '4': 1, + '5': 9, + '9': 0, + '10': 'setCannedMessageModuleMessages' + }, + { + '1': 'set_ringtone_message', + '3': 37, + '4': 1, + '5': 9, + '9': 0, + '10': 'setRingtoneMessage' + }, + { + '1': 'remove_by_nodenum', + '3': 38, + '4': 1, + '5': 13, + '9': 0, + '10': 'removeByNodenum' + }, + { + '1': 'set_favorite_node', + '3': 39, + '4': 1, + '5': 13, + '9': 0, + '10': 'setFavoriteNode' + }, + { + '1': 'remove_favorite_node', + '3': 40, + '4': 1, + '5': 13, + '9': 0, + '10': 'removeFavoriteNode' + }, + { + '1': 'set_fixed_position', + '3': 41, + '4': 1, + '5': 11, + '6': '.meshtastic.Position', + '9': 0, + '10': 'setFixedPosition' + }, + { + '1': 'remove_fixed_position', + '3': 42, + '4': 1, + '5': 8, + '9': 0, + '10': 'removeFixedPosition' + }, + { + '1': 'set_time_only', + '3': 43, + '4': 1, + '5': 7, + '9': 0, + '10': 'setTimeOnly' + }, + { + '1': 'get_ui_config_request', + '3': 44, + '4': 1, + '5': 8, + '9': 0, + '10': 'getUiConfigRequest' + }, + { + '1': 'get_ui_config_response', + '3': 45, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceUIConfig', + '9': 0, + '10': 'getUiConfigResponse' + }, + { + '1': 'store_ui_config', + '3': 46, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceUIConfig', + '9': 0, + '10': 'storeUiConfig' + }, + { + '1': 'set_ignored_node', + '3': 47, + '4': 1, + '5': 13, + '9': 0, + '10': 'setIgnoredNode' + }, + { + '1': 'remove_ignored_node', + '3': 48, + '4': 1, + '5': 13, + '9': 0, + '10': 'removeIgnoredNode' + }, + { + '1': 'begin_edit_settings', + '3': 64, + '4': 1, + '5': 8, + '9': 0, + '10': 'beginEditSettings' + }, + { + '1': 'commit_edit_settings', + '3': 65, + '4': 1, + '5': 8, + '9': 0, + '10': 'commitEditSettings' + }, + { + '1': 'add_contact', + '3': 66, + '4': 1, + '5': 11, + '6': '.meshtastic.SharedContact', + '9': 0, + '10': 'addContact' + }, + { + '1': 'key_verification', + '3': 67, + '4': 1, + '5': 11, + '6': '.meshtastic.KeyVerificationAdmin', + '9': 0, + '10': 'keyVerification' + }, + { + '1': 'factory_reset_device', + '3': 94, + '4': 1, + '5': 5, + '9': 0, + '10': 'factoryResetDevice' + }, + { + '1': 'reboot_ota_seconds', + '3': 95, + '4': 1, + '5': 5, + '9': 0, + '10': 'rebootOtaSeconds' + }, + { + '1': 'exit_simulator', + '3': 96, + '4': 1, + '5': 8, + '9': 0, + '10': 'exitSimulator' + }, + { + '1': 'reboot_seconds', + '3': 97, + '4': 1, + '5': 5, + '9': 0, + '10': 'rebootSeconds' + }, + { + '1': 'shutdown_seconds', + '3': 98, + '4': 1, + '5': 5, + '9': 0, + '10': 'shutdownSeconds' + }, + { + '1': 'factory_reset_config', + '3': 99, + '4': 1, + '5': 5, + '9': 0, + '10': 'factoryResetConfig' + }, + { + '1': 'nodedb_reset', + '3': 100, + '4': 1, + '5': 5, + '9': 0, + '10': 'nodedbReset' + }, + ], + '3': [AdminMessage_InputEvent$json], + '4': [ + AdminMessage_ConfigType$json, + AdminMessage_ModuleConfigType$json, + AdminMessage_BackupLocation$json + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +@$core.Deprecated('Use adminMessageDescriptor instead') +const AdminMessage_InputEvent$json = { + '1': 'InputEvent', + '2': [ + {'1': 'event_code', '3': 1, '4': 1, '5': 13, '10': 'eventCode'}, + {'1': 'kb_char', '3': 2, '4': 1, '5': 13, '10': 'kbChar'}, + {'1': 'touch_x', '3': 3, '4': 1, '5': 13, '10': 'touchX'}, + {'1': 'touch_y', '3': 4, '4': 1, '5': 13, '10': 'touchY'}, + ], +}; + +@$core.Deprecated('Use adminMessageDescriptor instead') +const AdminMessage_ConfigType$json = { + '1': 'ConfigType', + '2': [ + {'1': 'DEVICE_CONFIG', '2': 0}, + {'1': 'POSITION_CONFIG', '2': 1}, + {'1': 'POWER_CONFIG', '2': 2}, + {'1': 'NETWORK_CONFIG', '2': 3}, + {'1': 'DISPLAY_CONFIG', '2': 4}, + {'1': 'LORA_CONFIG', '2': 5}, + {'1': 'BLUETOOTH_CONFIG', '2': 6}, + {'1': 'SECURITY_CONFIG', '2': 7}, + {'1': 'SESSIONKEY_CONFIG', '2': 8}, + {'1': 'DEVICEUI_CONFIG', '2': 9}, + ], +}; + +@$core.Deprecated('Use adminMessageDescriptor instead') +const AdminMessage_ModuleConfigType$json = { + '1': 'ModuleConfigType', + '2': [ + {'1': 'MQTT_CONFIG', '2': 0}, + {'1': 'SERIAL_CONFIG', '2': 1}, + {'1': 'EXTNOTIF_CONFIG', '2': 2}, + {'1': 'STOREFORWARD_CONFIG', '2': 3}, + {'1': 'RANGETEST_CONFIG', '2': 4}, + {'1': 'TELEMETRY_CONFIG', '2': 5}, + {'1': 'CANNEDMSG_CONFIG', '2': 6}, + {'1': 'AUDIO_CONFIG', '2': 7}, + {'1': 'REMOTEHARDWARE_CONFIG', '2': 8}, + {'1': 'NEIGHBORINFO_CONFIG', '2': 9}, + {'1': 'AMBIENTLIGHTING_CONFIG', '2': 10}, + {'1': 'DETECTIONSENSOR_CONFIG', '2': 11}, + {'1': 'PAXCOUNTER_CONFIG', '2': 12}, + ], +}; + +@$core.Deprecated('Use adminMessageDescriptor instead') +const AdminMessage_BackupLocation$json = { + '1': 'BackupLocation', + '2': [ + {'1': 'FLASH', '2': 0}, + {'1': 'SD', '2': 1}, + ], +}; + +/// Descriptor for `AdminMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List adminMessageDescriptor = $convert.base64Decode( + 'CgxBZG1pbk1lc3NhZ2USJwoPc2Vzc2lvbl9wYXNza2V5GGUgASgMUg5zZXNzaW9uUGFzc2tleR' + 'IwChNnZXRfY2hhbm5lbF9yZXF1ZXN0GAEgASgNSABSEWdldENoYW5uZWxSZXF1ZXN0EkcKFGdl' + 'dF9jaGFubmVsX3Jlc3BvbnNlGAIgASgLMhMubWVzaHRhc3RpYy5DaGFubmVsSABSEmdldENoYW' + '5uZWxSZXNwb25zZRIsChFnZXRfb3duZXJfcmVxdWVzdBgDIAEoCEgAUg9nZXRPd25lclJlcXVl' + 'c3QSQAoSZ2V0X293bmVyX3Jlc3BvbnNlGAQgASgLMhAubWVzaHRhc3RpYy5Vc2VySABSEGdldE' + '93bmVyUmVzcG9uc2USUwoSZ2V0X2NvbmZpZ19yZXF1ZXN0GAUgASgOMiMubWVzaHRhc3RpYy5B' + 'ZG1pbk1lc3NhZ2UuQ29uZmlnVHlwZUgAUhBnZXRDb25maWdSZXF1ZXN0EkQKE2dldF9jb25maW' + 'dfcmVzcG9uc2UYBiABKAsyEi5tZXNodGFzdGljLkNvbmZpZ0gAUhFnZXRDb25maWdSZXNwb25z' + 'ZRJmChlnZXRfbW9kdWxlX2NvbmZpZ19yZXF1ZXN0GAcgASgOMikubWVzaHRhc3RpYy5BZG1pbk' + '1lc3NhZ2UuTW9kdWxlQ29uZmlnVHlwZUgAUhZnZXRNb2R1bGVDb25maWdSZXF1ZXN0ElcKGmdl' + 'dF9tb2R1bGVfY29uZmlnX3Jlc3BvbnNlGAggASgLMhgubWVzaHRhc3RpYy5Nb2R1bGVDb25maW' + 'dIAFIXZ2V0TW9kdWxlQ29uZmlnUmVzcG9uc2USWwoqZ2V0X2Nhbm5lZF9tZXNzYWdlX21vZHVs' + 'ZV9tZXNzYWdlc19yZXF1ZXN0GAogASgISABSJWdldENhbm5lZE1lc3NhZ2VNb2R1bGVNZXNzYW' + 'dlc1JlcXVlc3QSXQorZ2V0X2Nhbm5lZF9tZXNzYWdlX21vZHVsZV9tZXNzYWdlc19yZXNwb25z' + 'ZRgLIAEoCUgAUiZnZXRDYW5uZWRNZXNzYWdlTW9kdWxlTWVzc2FnZXNSZXNwb25zZRI/ChtnZX' + 'RfZGV2aWNlX21ldGFkYXRhX3JlcXVlc3QYDCABKAhIAFIYZ2V0RGV2aWNlTWV0YWRhdGFSZXF1' + 'ZXN0El0KHGdldF9kZXZpY2VfbWV0YWRhdGFfcmVzcG9uc2UYDSABKAsyGi5tZXNodGFzdGljLk' + 'RldmljZU1ldGFkYXRhSABSGWdldERldmljZU1ldGFkYXRhUmVzcG9uc2USMgoUZ2V0X3Jpbmd0' + 'b25lX3JlcXVlc3QYDiABKAhIAFISZ2V0UmluZ3RvbmVSZXF1ZXN0EjQKFWdldF9yaW5ndG9uZV' + '9yZXNwb25zZRgPIAEoCUgAUhNnZXRSaW5ndG9uZVJlc3BvbnNlElAKJGdldF9kZXZpY2VfY29u' + 'bmVjdGlvbl9zdGF0dXNfcmVxdWVzdBgQIAEoCEgAUiBnZXREZXZpY2VDb25uZWN0aW9uU3RhdH' + 'VzUmVxdWVzdBJ2CiVnZXRfZGV2aWNlX2Nvbm5lY3Rpb25fc3RhdHVzX3Jlc3BvbnNlGBEgASgL' + 'MiIubWVzaHRhc3RpYy5EZXZpY2VDb25uZWN0aW9uU3RhdHVzSABSIWdldERldmljZUNvbm5lY3' + 'Rpb25TdGF0dXNSZXNwb25zZRI9CgxzZXRfaGFtX21vZGUYEiABKAsyGS5tZXNodGFzdGljLkhh' + 'bVBhcmFtZXRlcnNIAFIKc2V0SGFtTW9kZRJRCiVnZXRfbm9kZV9yZW1vdGVfaGFyZHdhcmVfcG' + 'luc19yZXF1ZXN0GBMgASgISABSIGdldE5vZGVSZW1vdGVIYXJkd2FyZVBpbnNSZXF1ZXN0En8K' + 'JmdldF9ub2RlX3JlbW90ZV9oYXJkd2FyZV9waW5zX3Jlc3BvbnNlGBQgASgLMioubWVzaHRhc3' + 'RpYy5Ob2RlUmVtb3RlSGFyZHdhcmVQaW5zUmVzcG9uc2VIAFIhZ2V0Tm9kZVJlbW90ZUhhcmR3' + 'YXJlUGluc1Jlc3BvbnNlEjUKFmVudGVyX2RmdV9tb2RlX3JlcXVlc3QYFSABKAhIAFITZW50ZX' + 'JEZnVNb2RlUmVxdWVzdBIwChNkZWxldGVfZmlsZV9yZXF1ZXN0GBYgASgJSABSEWRlbGV0ZUZp' + 'bGVSZXF1ZXN0Eh0KCXNldF9zY2FsZRgXIAEoDUgAUghzZXRTY2FsZRJYChJiYWNrdXBfcHJlZm' + 'VyZW5jZXMYGCABKA4yJy5tZXNodGFzdGljLkFkbWluTWVzc2FnZS5CYWNrdXBMb2NhdGlvbkgA' + 'UhFiYWNrdXBQcmVmZXJlbmNlcxJaChNyZXN0b3JlX3ByZWZlcmVuY2VzGBkgASgOMicubWVzaH' + 'Rhc3RpYy5BZG1pbk1lc3NhZ2UuQmFja3VwTG9jYXRpb25IAFIScmVzdG9yZVByZWZlcmVuY2Vz' + 'EmUKGXJlbW92ZV9iYWNrdXBfcHJlZmVyZW5jZXMYGiABKA4yJy5tZXNodGFzdGljLkFkbWluTW' + 'Vzc2FnZS5CYWNrdXBMb2NhdGlvbkgAUhdyZW1vdmVCYWNrdXBQcmVmZXJlbmNlcxJPChBzZW5k' + 'X2lucHV0X2V2ZW50GBsgASgLMiMubWVzaHRhc3RpYy5BZG1pbk1lc3NhZ2UuSW5wdXRFdmVudE' + 'gAUg5zZW5kSW5wdXRFdmVudBIvCglzZXRfb3duZXIYICABKAsyEC5tZXNodGFzdGljLlVzZXJI' + 'AFIIc2V0T3duZXISNgoLc2V0X2NoYW5uZWwYISABKAsyEy5tZXNodGFzdGljLkNoYW5uZWxIAF' + 'IKc2V0Q2hhbm5lbBIzCgpzZXRfY29uZmlnGCIgASgLMhIubWVzaHRhc3RpYy5Db25maWdIAFIJ' + 'c2V0Q29uZmlnEkYKEXNldF9tb2R1bGVfY29uZmlnGCMgASgLMhgubWVzaHRhc3RpYy5Nb2R1bG' + 'VDb25maWdIAFIPc2V0TW9kdWxlQ29uZmlnEkwKInNldF9jYW5uZWRfbWVzc2FnZV9tb2R1bGVf' + 'bWVzc2FnZXMYJCABKAlIAFIec2V0Q2FubmVkTWVzc2FnZU1vZHVsZU1lc3NhZ2VzEjIKFHNldF' + '9yaW5ndG9uZV9tZXNzYWdlGCUgASgJSABSEnNldFJpbmd0b25lTWVzc2FnZRIsChFyZW1vdmVf' + 'Ynlfbm9kZW51bRgmIAEoDUgAUg9yZW1vdmVCeU5vZGVudW0SLAoRc2V0X2Zhdm9yaXRlX25vZG' + 'UYJyABKA1IAFIPc2V0RmF2b3JpdGVOb2RlEjIKFHJlbW92ZV9mYXZvcml0ZV9ub2RlGCggASgN' + 'SABSEnJlbW92ZUZhdm9yaXRlTm9kZRJEChJzZXRfZml4ZWRfcG9zaXRpb24YKSABKAsyFC5tZX' + 'NodGFzdGljLlBvc2l0aW9uSABSEHNldEZpeGVkUG9zaXRpb24SNAoVcmVtb3ZlX2ZpeGVkX3Bv' + 'c2l0aW9uGCogASgISABSE3JlbW92ZUZpeGVkUG9zaXRpb24SJAoNc2V0X3RpbWVfb25seRgrIA' + 'EoB0gAUgtzZXRUaW1lT25seRIzChVnZXRfdWlfY29uZmlnX3JlcXVlc3QYLCABKAhIAFISZ2V0' + 'VWlDb25maWdSZXF1ZXN0ElEKFmdldF91aV9jb25maWdfcmVzcG9uc2UYLSABKAsyGi5tZXNodG' + 'FzdGljLkRldmljZVVJQ29uZmlnSABSE2dldFVpQ29uZmlnUmVzcG9uc2USRAoPc3RvcmVfdWlf' + 'Y29uZmlnGC4gASgLMhoubWVzaHRhc3RpYy5EZXZpY2VVSUNvbmZpZ0gAUg1zdG9yZVVpQ29uZm' + 'lnEioKEHNldF9pZ25vcmVkX25vZGUYLyABKA1IAFIOc2V0SWdub3JlZE5vZGUSMAoTcmVtb3Zl' + 'X2lnbm9yZWRfbm9kZRgwIAEoDUgAUhFyZW1vdmVJZ25vcmVkTm9kZRIwChNiZWdpbl9lZGl0X3' + 'NldHRpbmdzGEAgASgISABSEWJlZ2luRWRpdFNldHRpbmdzEjIKFGNvbW1pdF9lZGl0X3NldHRp' + 'bmdzGEEgASgISABSEmNvbW1pdEVkaXRTZXR0aW5ncxI8CgthZGRfY29udGFjdBhCIAEoCzIZLm' + '1lc2h0YXN0aWMuU2hhcmVkQ29udGFjdEgAUgphZGRDb250YWN0Ek0KEGtleV92ZXJpZmljYXRp' + 'b24YQyABKAsyIC5tZXNodGFzdGljLktleVZlcmlmaWNhdGlvbkFkbWluSABSD2tleVZlcmlmaW' + 'NhdGlvbhIyChRmYWN0b3J5X3Jlc2V0X2RldmljZRheIAEoBUgAUhJmYWN0b3J5UmVzZXREZXZp' + 'Y2USLgoScmVib290X290YV9zZWNvbmRzGF8gASgFSABSEHJlYm9vdE90YVNlY29uZHMSJwoOZX' + 'hpdF9zaW11bGF0b3IYYCABKAhIAFINZXhpdFNpbXVsYXRvchInCg5yZWJvb3Rfc2Vjb25kcxhh' + 'IAEoBUgAUg1yZWJvb3RTZWNvbmRzEisKEHNodXRkb3duX3NlY29uZHMYYiABKAVIAFIPc2h1dG' + 'Rvd25TZWNvbmRzEjIKFGZhY3RvcnlfcmVzZXRfY29uZmlnGGMgASgFSABSEmZhY3RvcnlSZXNl' + 'dENvbmZpZxIjCgxub2RlZGJfcmVzZXQYZCABKAVIAFILbm9kZWRiUmVzZXQadgoKSW5wdXRFdm' + 'VudBIdCgpldmVudF9jb2RlGAEgASgNUglldmVudENvZGUSFwoHa2JfY2hhchgCIAEoDVIGa2JD' + 'aGFyEhcKB3RvdWNoX3gYAyABKA1SBnRvdWNoWBIXCgd0b3VjaF95GAQgASgNUgZ0b3VjaFki1g' + 'EKCkNvbmZpZ1R5cGUSEQoNREVWSUNFX0NPTkZJRxAAEhMKD1BPU0lUSU9OX0NPTkZJRxABEhAK' + 'DFBPV0VSX0NPTkZJRxACEhIKDk5FVFdPUktfQ09ORklHEAMSEgoORElTUExBWV9DT05GSUcQBB' + 'IPCgtMT1JBX0NPTkZJRxAFEhQKEEJMVUVUT09USF9DT05GSUcQBhITCg9TRUNVUklUWV9DT05G' + 'SUcQBxIVChFTRVNTSU9OS0VZX0NPTkZJRxAIEhMKD0RFVklDRVVJX0NPTkZJRxAJIrsCChBNb2' + 'R1bGVDb25maWdUeXBlEg8KC01RVFRfQ09ORklHEAASEQoNU0VSSUFMX0NPTkZJRxABEhMKD0VY' + 'VE5PVElGX0NPTkZJRxACEhcKE1NUT1JFRk9SV0FSRF9DT05GSUcQAxIUChBSQU5HRVRFU1RfQ0' + '9ORklHEAQSFAoQVEVMRU1FVFJZX0NPTkZJRxAFEhQKEENBTk5FRE1TR19DT05GSUcQBhIQCgxB' + 'VURJT19DT05GSUcQBxIZChVSRU1PVEVIQVJEV0FSRV9DT05GSUcQCBIXChNORUlHSEJPUklORk' + '9fQ09ORklHEAkSGgoWQU1CSUVOVExJR0hUSU5HX0NPTkZJRxAKEhoKFkRFVEVDVElPTlNFTlNP' + 'Ul9DT05GSUcQCxIVChFQQVhDT1VOVEVSX0NPTkZJRxAMIiMKDkJhY2t1cExvY2F0aW9uEgkKBU' + 'ZMQVNIEAASBgoCU0QQAUIRCg9wYXlsb2FkX3ZhcmlhbnQ='); + +@$core.Deprecated('Use hamParametersDescriptor instead') +const HamParameters$json = { + '1': 'HamParameters', + '2': [ + {'1': 'call_sign', '3': 1, '4': 1, '5': 9, '10': 'callSign'}, + {'1': 'tx_power', '3': 2, '4': 1, '5': 5, '10': 'txPower'}, + {'1': 'frequency', '3': 3, '4': 1, '5': 2, '10': 'frequency'}, + {'1': 'short_name', '3': 4, '4': 1, '5': 9, '10': 'shortName'}, + ], +}; + +/// Descriptor for `HamParameters`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List hamParametersDescriptor = $convert.base64Decode( + 'Cg1IYW1QYXJhbWV0ZXJzEhsKCWNhbGxfc2lnbhgBIAEoCVIIY2FsbFNpZ24SGQoIdHhfcG93ZX' + 'IYAiABKAVSB3R4UG93ZXISHAoJZnJlcXVlbmN5GAMgASgCUglmcmVxdWVuY3kSHQoKc2hvcnRf' + 'bmFtZRgEIAEoCVIJc2hvcnROYW1l'); + +@$core.Deprecated('Use nodeRemoteHardwarePinsResponseDescriptor instead') +const NodeRemoteHardwarePinsResponse$json = { + '1': 'NodeRemoteHardwarePinsResponse', + '2': [ + { + '1': 'node_remote_hardware_pins', + '3': 1, + '4': 3, + '5': 11, + '6': '.meshtastic.NodeRemoteHardwarePin', + '10': 'nodeRemoteHardwarePins' + }, + ], +}; + +/// Descriptor for `NodeRemoteHardwarePinsResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeRemoteHardwarePinsResponseDescriptor = + $convert.base64Decode( + 'Ch5Ob2RlUmVtb3RlSGFyZHdhcmVQaW5zUmVzcG9uc2USXAoZbm9kZV9yZW1vdGVfaGFyZHdhcm' + 'VfcGlucxgBIAMoCzIhLm1lc2h0YXN0aWMuTm9kZVJlbW90ZUhhcmR3YXJlUGluUhZub2RlUmVt' + 'b3RlSGFyZHdhcmVQaW5z'); + +@$core.Deprecated('Use sharedContactDescriptor instead') +const SharedContact$json = { + '1': 'SharedContact', + '2': [ + {'1': 'node_num', '3': 1, '4': 1, '5': 13, '10': 'nodeNum'}, + { + '1': 'user', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.User', + '10': 'user' + }, + {'1': 'should_ignore', '3': 3, '4': 1, '5': 8, '10': 'shouldIgnore'}, + ], +}; + +/// Descriptor for `SharedContact`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sharedContactDescriptor = $convert.base64Decode( + 'Cg1TaGFyZWRDb250YWN0EhkKCG5vZGVfbnVtGAEgASgNUgdub2RlTnVtEiQKBHVzZXIYAiABKA' + 'syEC5tZXNodGFzdGljLlVzZXJSBHVzZXISIwoNc2hvdWxkX2lnbm9yZRgDIAEoCFIMc2hvdWxk' + 'SWdub3Jl'); + +@$core.Deprecated('Use keyVerificationAdminDescriptor instead') +const KeyVerificationAdmin$json = { + '1': 'KeyVerificationAdmin', + '2': [ + { + '1': 'message_type', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.KeyVerificationAdmin.MessageType', + '10': 'messageType' + }, + {'1': 'remote_nodenum', '3': 2, '4': 1, '5': 13, '10': 'remoteNodenum'}, + {'1': 'nonce', '3': 3, '4': 1, '5': 4, '10': 'nonce'}, + { + '1': 'security_number', + '3': 4, + '4': 1, + '5': 13, + '9': 0, + '10': 'securityNumber', + '17': true + }, + ], + '4': [KeyVerificationAdmin_MessageType$json], + '8': [ + {'1': '_security_number'}, + ], +}; + +@$core.Deprecated('Use keyVerificationAdminDescriptor instead') +const KeyVerificationAdmin_MessageType$json = { + '1': 'MessageType', + '2': [ + {'1': 'INITIATE_VERIFICATION', '2': 0}, + {'1': 'PROVIDE_SECURITY_NUMBER', '2': 1}, + {'1': 'DO_VERIFY', '2': 2}, + {'1': 'DO_NOT_VERIFY', '2': 3}, + ], +}; + +/// Descriptor for `KeyVerificationAdmin`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List keyVerificationAdminDescriptor = $convert.base64Decode( + 'ChRLZXlWZXJpZmljYXRpb25BZG1pbhJPCgxtZXNzYWdlX3R5cGUYASABKA4yLC5tZXNodGFzdG' + 'ljLktleVZlcmlmaWNhdGlvbkFkbWluLk1lc3NhZ2VUeXBlUgttZXNzYWdlVHlwZRIlCg5yZW1v' + 'dGVfbm9kZW51bRgCIAEoDVINcmVtb3RlTm9kZW51bRIUCgVub25jZRgDIAEoBFIFbm9uY2USLA' + 'oPc2VjdXJpdHlfbnVtYmVyGAQgASgNSABSDnNlY3VyaXR5TnVtYmVyiAEBImcKC01lc3NhZ2VU' + 'eXBlEhkKFUlOSVRJQVRFX1ZFUklGSUNBVElPThAAEhsKF1BST1ZJREVfU0VDVVJJVFlfTlVNQk' + 'VSEAESDQoJRE9fVkVSSUZZEAISEQoNRE9fTk9UX1ZFUklGWRADQhIKEF9zZWN1cml0eV9udW1i' + 'ZXI='); diff --git a/third_party/meshtastic_flutter/lib/generated/apponly.pb.dart b/third_party/meshtastic_flutter/lib/generated/apponly.pb.dart new file mode 100644 index 000000000..a102d18c9 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/apponly.pb.dart @@ -0,0 +1,100 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/apponly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'channel.pb.dart' as $0; +import 'config.pb.dart' as $1; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// This is the most compact possible representation for a set of channels. +/// It includes only one PRIMARY channel (which must be first) and +/// any SECONDARY channels. +/// No DISABLED channels are included. +/// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL +class ChannelSet extends $pb.GeneratedMessage { + factory ChannelSet({ + $core.Iterable<$0.ChannelSettings>? settings, + $1.Config_LoRaConfig? loraConfig, + }) { + final result = create(); + if (settings != null) result.settings.addAll(settings); + if (loraConfig != null) result.loraConfig = loraConfig; + return result; + } + + ChannelSet._(); + + factory ChannelSet.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ChannelSet.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ChannelSet', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..pc<$0.ChannelSettings>( + 1, _omitFieldNames ? '' : 'settings', $pb.PbFieldType.PM, + subBuilder: $0.ChannelSettings.create) + ..aOM<$1.Config_LoRaConfig>(2, _omitFieldNames ? '' : 'loraConfig', + subBuilder: $1.Config_LoRaConfig.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChannelSet clone() => ChannelSet()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChannelSet copyWith(void Function(ChannelSet) updates) => + super.copyWith((message) => updates(message as ChannelSet)) as ChannelSet; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChannelSet create() => ChannelSet._(); + @$core.override + ChannelSet createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChannelSet getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ChannelSet? _defaultInstance; + + /// + /// Channel list with settings + @$pb.TagNumber(1) + $pb.PbList<$0.ChannelSettings> get settings => $_getList(0); + + /// + /// LoRa config + @$pb.TagNumber(2) + $1.Config_LoRaConfig get loraConfig => $_getN(1); + @$pb.TagNumber(2) + set loraConfig($1.Config_LoRaConfig value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasLoraConfig() => $_has(1); + @$pb.TagNumber(2) + void clearLoraConfig() => $_clearField(2); + @$pb.TagNumber(2) + $1.Config_LoRaConfig ensureLoraConfig() => $_ensure(1); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/apponly.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/apponly.pbenum.dart new file mode 100644 index 000000000..7f59d1c5e --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/apponly.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/apponly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/apponly.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/apponly.pbjson.dart new file mode 100644 index 000000000..9820c1ff4 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/apponly.pbjson.dart @@ -0,0 +1,44 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/apponly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use channelSetDescriptor instead') +const ChannelSet$json = { + '1': 'ChannelSet', + '2': [ + { + '1': 'settings', + '3': 1, + '4': 3, + '5': 11, + '6': '.meshtastic.ChannelSettings', + '10': 'settings' + }, + { + '1': 'lora_config', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.LoRaConfig', + '10': 'loraConfig' + }, + ], +}; + +/// Descriptor for `ChannelSet`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List channelSetDescriptor = $convert.base64Decode( + 'CgpDaGFubmVsU2V0EjcKCHNldHRpbmdzGAEgAygLMhsubWVzaHRhc3RpYy5DaGFubmVsU2V0dG' + 'luZ3NSCHNldHRpbmdzEj4KC2xvcmFfY29uZmlnGAIgASgLMh0ubWVzaHRhc3RpYy5Db25maWcu' + 'TG9SYUNvbmZpZ1IKbG9yYUNvbmZpZw=='); diff --git a/third_party/meshtastic_flutter/lib/generated/atak.pb.dart b/third_party/meshtastic_flutter/lib/generated/atak.pb.dart new file mode 100644 index 000000000..298db14b6 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/atak.pb.dart @@ -0,0 +1,609 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/atak.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'atak.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'atak.pbenum.dart'; + +enum TAKPacket_PayloadVariant { pli, chat, detail, notSet } + +/// +/// Packets for the official ATAK Plugin +class TAKPacket extends $pb.GeneratedMessage { + factory TAKPacket({ + $core.bool? isCompressed, + Contact? contact, + Group? group, + Status? status, + PLI? pli, + GeoChat? chat, + $core.List<$core.int>? detail, + }) { + final result = create(); + if (isCompressed != null) result.isCompressed = isCompressed; + if (contact != null) result.contact = contact; + if (group != null) result.group = group; + if (status != null) result.status = status; + if (pli != null) result.pli = pli; + if (chat != null) result.chat = chat; + if (detail != null) result.detail = detail; + return result; + } + + TAKPacket._(); + + factory TAKPacket.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory TAKPacket.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, TAKPacket_PayloadVariant> + _TAKPacket_PayloadVariantByTag = { + 5: TAKPacket_PayloadVariant.pli, + 6: TAKPacket_PayloadVariant.chat, + 7: TAKPacket_PayloadVariant.detail, + 0: TAKPacket_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'TAKPacket', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [5, 6, 7]) + ..aOB(1, _omitFieldNames ? '' : 'isCompressed') + ..aOM(2, _omitFieldNames ? '' : 'contact', + subBuilder: Contact.create) + ..aOM(3, _omitFieldNames ? '' : 'group', subBuilder: Group.create) + ..aOM(4, _omitFieldNames ? '' : 'status', subBuilder: Status.create) + ..aOM(5, _omitFieldNames ? '' : 'pli', subBuilder: PLI.create) + ..aOM(6, _omitFieldNames ? '' : 'chat', subBuilder: GeoChat.create) + ..a<$core.List<$core.int>>( + 7, _omitFieldNames ? '' : 'detail', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + TAKPacket clone() => TAKPacket()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + TAKPacket copyWith(void Function(TAKPacket) updates) => + super.copyWith((message) => updates(message as TAKPacket)) as TAKPacket; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TAKPacket create() => TAKPacket._(); + @$core.override + TAKPacket createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TAKPacket getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TAKPacket? _defaultInstance; + + TAKPacket_PayloadVariant whichPayloadVariant() => + _TAKPacket_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// Are the payloads strings compressed for LoRA transport? + @$pb.TagNumber(1) + $core.bool get isCompressed => $_getBF(0); + @$pb.TagNumber(1) + set isCompressed($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasIsCompressed() => $_has(0); + @$pb.TagNumber(1) + void clearIsCompressed() => $_clearField(1); + + /// + /// The contact / callsign for ATAK user + @$pb.TagNumber(2) + Contact get contact => $_getN(1); + @$pb.TagNumber(2) + set contact(Contact value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasContact() => $_has(1); + @$pb.TagNumber(2) + void clearContact() => $_clearField(2); + @$pb.TagNumber(2) + Contact ensureContact() => $_ensure(1); + + /// + /// The group for ATAK user + @$pb.TagNumber(3) + Group get group => $_getN(2); + @$pb.TagNumber(3) + set group(Group value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasGroup() => $_has(2); + @$pb.TagNumber(3) + void clearGroup() => $_clearField(3); + @$pb.TagNumber(3) + Group ensureGroup() => $_ensure(2); + + /// + /// The status of the ATAK EUD + @$pb.TagNumber(4) + Status get status => $_getN(3); + @$pb.TagNumber(4) + set status(Status value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasStatus() => $_has(3); + @$pb.TagNumber(4) + void clearStatus() => $_clearField(4); + @$pb.TagNumber(4) + Status ensureStatus() => $_ensure(3); + + /// + /// TAK position report + @$pb.TagNumber(5) + PLI get pli => $_getN(4); + @$pb.TagNumber(5) + set pli(PLI value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasPli() => $_has(4); + @$pb.TagNumber(5) + void clearPli() => $_clearField(5); + @$pb.TagNumber(5) + PLI ensurePli() => $_ensure(4); + + /// + /// ATAK GeoChat message + @$pb.TagNumber(6) + GeoChat get chat => $_getN(5); + @$pb.TagNumber(6) + set chat(GeoChat value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasChat() => $_has(5); + @$pb.TagNumber(6) + void clearChat() => $_clearField(6); + @$pb.TagNumber(6) + GeoChat ensureChat() => $_ensure(5); + + /// + /// Generic CoT detail XML + /// May be compressed / truncated by the sender (EUD) + @$pb.TagNumber(7) + $core.List<$core.int> get detail => $_getN(6); + @$pb.TagNumber(7) + set detail($core.List<$core.int> value) => $_setBytes(6, value); + @$pb.TagNumber(7) + $core.bool hasDetail() => $_has(6); + @$pb.TagNumber(7) + void clearDetail() => $_clearField(7); +} + +/// +/// ATAK GeoChat message +class GeoChat extends $pb.GeneratedMessage { + factory GeoChat({ + $core.String? message, + $core.String? to, + $core.String? toCallsign, + }) { + final result = create(); + if (message != null) result.message = message; + if (to != null) result.to = to; + if (toCallsign != null) result.toCallsign = toCallsign; + return result; + } + + GeoChat._(); + + factory GeoChat.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GeoChat.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GeoChat', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'message') + ..aOS(2, _omitFieldNames ? '' : 'to') + ..aOS(3, _omitFieldNames ? '' : 'toCallsign') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GeoChat clone() => GeoChat()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GeoChat copyWith(void Function(GeoChat) updates) => + super.copyWith((message) => updates(message as GeoChat)) as GeoChat; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GeoChat create() => GeoChat._(); + @$core.override + GeoChat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GeoChat getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GeoChat? _defaultInstance; + + /// + /// The text message + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => $_clearField(1); + + /// + /// Uid recipient of the message + @$pb.TagNumber(2) + $core.String get to => $_getSZ(1); + @$pb.TagNumber(2) + set to($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasTo() => $_has(1); + @$pb.TagNumber(2) + void clearTo() => $_clearField(2); + + /// + /// Callsign of the recipient for the message + @$pb.TagNumber(3) + $core.String get toCallsign => $_getSZ(2); + @$pb.TagNumber(3) + set toCallsign($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasToCallsign() => $_has(2); + @$pb.TagNumber(3) + void clearToCallsign() => $_clearField(3); +} + +/// +/// ATAK Group +/// <__group role='Team Member' name='Cyan'/> +class Group extends $pb.GeneratedMessage { + factory Group({ + MemberRole? role, + Team? team, + }) { + final result = create(); + if (role != null) result.role = role; + if (team != null) result.team = team; + return result; + } + + Group._(); + + factory Group.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Group.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Group', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: MemberRole.Unspecifed, + valueOf: MemberRole.valueOf, + enumValues: MemberRole.values) + ..e(2, _omitFieldNames ? '' : 'team', $pb.PbFieldType.OE, + defaultOrMaker: Team.Unspecifed_Color, + valueOf: Team.valueOf, + enumValues: Team.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Group clone() => Group()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Group copyWith(void Function(Group) updates) => + super.copyWith((message) => updates(message as Group)) as Group; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Group create() => Group._(); + @$core.override + Group createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Group getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Group? _defaultInstance; + + /// + /// Role of the group member + @$pb.TagNumber(1) + MemberRole get role => $_getN(0); + @$pb.TagNumber(1) + set role(MemberRole value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasRole() => $_has(0); + @$pb.TagNumber(1) + void clearRole() => $_clearField(1); + + /// + /// Team (color) + /// Default Cyan + @$pb.TagNumber(2) + Team get team => $_getN(1); + @$pb.TagNumber(2) + set team(Team value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasTeam() => $_has(1); + @$pb.TagNumber(2) + void clearTeam() => $_clearField(2); +} + +/// +/// ATAK EUD Status +/// status battery='100' +class Status extends $pb.GeneratedMessage { + factory Status({ + $core.int? battery, + }) { + final result = create(); + if (battery != null) result.battery = battery; + return result; + } + + Status._(); + + factory Status.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Status.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Status', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'battery', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Status clone() => Status()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Status copyWith(void Function(Status) updates) => + super.copyWith((message) => updates(message as Status)) as Status; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Status create() => Status._(); + @$core.override + Status createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Status getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Status? _defaultInstance; + + /// + /// Battery level + @$pb.TagNumber(1) + $core.int get battery => $_getIZ(0); + @$pb.TagNumber(1) + set battery($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasBattery() => $_has(0); + @$pb.TagNumber(1) + void clearBattery() => $_clearField(1); +} + +/// +/// ATAK Contact +/// contact endpoint='0.0.0.0:4242:tcp' phone='+12345678' callsign='FALKE' +class Contact extends $pb.GeneratedMessage { + factory Contact({ + $core.String? callsign, + $core.String? deviceCallsign, + }) { + final result = create(); + if (callsign != null) result.callsign = callsign; + if (deviceCallsign != null) result.deviceCallsign = deviceCallsign; + return result; + } + + Contact._(); + + factory Contact.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Contact.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Contact', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'callsign') + ..aOS(2, _omitFieldNames ? '' : 'deviceCallsign') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Contact clone() => Contact()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Contact copyWith(void Function(Contact) updates) => + super.copyWith((message) => updates(message as Contact)) as Contact; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Contact create() => Contact._(); + @$core.override + Contact createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Contact getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Contact? _defaultInstance; + + /// + /// Callsign + @$pb.TagNumber(1) + $core.String get callsign => $_getSZ(0); + @$pb.TagNumber(1) + set callsign($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasCallsign() => $_has(0); + @$pb.TagNumber(1) + void clearCallsign() => $_clearField(1); + + /// + /// Device callsign + @$pb.TagNumber(2) + $core.String get deviceCallsign => $_getSZ(1); + @$pb.TagNumber(2) + set deviceCallsign($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasDeviceCallsign() => $_has(1); + @$pb.TagNumber(2) + void clearDeviceCallsign() => $_clearField(2); +} + +/// +/// Position Location Information from ATAK +class PLI extends $pb.GeneratedMessage { + factory PLI({ + $core.int? latitudeI, + $core.int? longitudeI, + $core.int? altitude, + $core.int? speed, + $core.int? course, + }) { + final result = create(); + if (latitudeI != null) result.latitudeI = latitudeI; + if (longitudeI != null) result.longitudeI = longitudeI; + if (altitude != null) result.altitude = altitude; + if (speed != null) result.speed = speed; + if (course != null) result.course = course; + return result; + } + + PLI._(); + + factory PLI.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory PLI.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'PLI', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'latitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'longitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'altitude', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'speed', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'course', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PLI clone() => PLI()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PLI copyWith(void Function(PLI) updates) => + super.copyWith((message) => updates(message as PLI)) as PLI; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PLI create() => PLI._(); + @$core.override + PLI createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PLI getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PLI? _defaultInstance; + + /// + /// The new preferred location encoding, multiply by 1e-7 to get degrees + /// in floating point + @$pb.TagNumber(1) + $core.int get latitudeI => $_getIZ(0); + @$pb.TagNumber(1) + set latitudeI($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasLatitudeI() => $_has(0); + @$pb.TagNumber(1) + void clearLatitudeI() => $_clearField(1); + + /// + /// The new preferred location encoding, multiply by 1e-7 to get degrees + /// in floating point + @$pb.TagNumber(2) + $core.int get longitudeI => $_getIZ(1); + @$pb.TagNumber(2) + set longitudeI($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasLongitudeI() => $_has(1); + @$pb.TagNumber(2) + void clearLongitudeI() => $_clearField(2); + + /// + /// Altitude (ATAK prefers HAE) + @$pb.TagNumber(3) + $core.int get altitude => $_getIZ(2); + @$pb.TagNumber(3) + set altitude($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasAltitude() => $_has(2); + @$pb.TagNumber(3) + void clearAltitude() => $_clearField(3); + + /// + /// Speed + @$pb.TagNumber(4) + $core.int get speed => $_getIZ(3); + @$pb.TagNumber(4) + set speed($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasSpeed() => $_has(3); + @$pb.TagNumber(4) + void clearSpeed() => $_clearField(4); + + /// + /// Course in degrees + @$pb.TagNumber(5) + $core.int get course => $_getIZ(4); + @$pb.TagNumber(5) + set course($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasCourse() => $_has(4); + @$pb.TagNumber(5) + void clearCourse() => $_clearField(5); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/atak.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/atak.pbenum.dart new file mode 100644 index 000000000..f9b996c42 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/atak.pbenum.dart @@ -0,0 +1,171 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/atak.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Team extends $pb.ProtobufEnum { + /// + /// Unspecifed + static const Team Unspecifed_Color = + Team._(0, _omitEnumNames ? '' : 'Unspecifed_Color'); + + /// + /// White + static const Team White = Team._(1, _omitEnumNames ? '' : 'White'); + + /// + /// Yellow + static const Team Yellow = Team._(2, _omitEnumNames ? '' : 'Yellow'); + + /// + /// Orange + static const Team Orange = Team._(3, _omitEnumNames ? '' : 'Orange'); + + /// + /// Magenta + static const Team Magenta = Team._(4, _omitEnumNames ? '' : 'Magenta'); + + /// + /// Red + static const Team Red = Team._(5, _omitEnumNames ? '' : 'Red'); + + /// + /// Maroon + static const Team Maroon = Team._(6, _omitEnumNames ? '' : 'Maroon'); + + /// + /// Purple + static const Team Purple = Team._(7, _omitEnumNames ? '' : 'Purple'); + + /// + /// Dark Blue + static const Team Dark_Blue = Team._(8, _omitEnumNames ? '' : 'Dark_Blue'); + + /// + /// Blue + static const Team Blue = Team._(9, _omitEnumNames ? '' : 'Blue'); + + /// + /// Cyan + static const Team Cyan = Team._(10, _omitEnumNames ? '' : 'Cyan'); + + /// + /// Teal + static const Team Teal = Team._(11, _omitEnumNames ? '' : 'Teal'); + + /// + /// Green + static const Team Green = Team._(12, _omitEnumNames ? '' : 'Green'); + + /// + /// Dark Green + static const Team Dark_Green = Team._(13, _omitEnumNames ? '' : 'Dark_Green'); + + /// + /// Brown + static const Team Brown = Team._(14, _omitEnumNames ? '' : 'Brown'); + + static const $core.List values = [ + Unspecifed_Color, + White, + Yellow, + Orange, + Magenta, + Red, + Maroon, + Purple, + Dark_Blue, + Blue, + Cyan, + Teal, + Green, + Dark_Green, + Brown, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 14); + static Team? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Team._(super.value, super.name); +} + +/// +/// Role of the group member +class MemberRole extends $pb.ProtobufEnum { + /// + /// Unspecifed + static const MemberRole Unspecifed = + MemberRole._(0, _omitEnumNames ? '' : 'Unspecifed'); + + /// + /// Team Member + static const MemberRole TeamMember = + MemberRole._(1, _omitEnumNames ? '' : 'TeamMember'); + + /// + /// Team Lead + static const MemberRole TeamLead = + MemberRole._(2, _omitEnumNames ? '' : 'TeamLead'); + + /// + /// Headquarters + static const MemberRole HQ = MemberRole._(3, _omitEnumNames ? '' : 'HQ'); + + /// + /// Airsoft enthusiast + static const MemberRole Sniper = + MemberRole._(4, _omitEnumNames ? '' : 'Sniper'); + + /// + /// Medic + static const MemberRole Medic = + MemberRole._(5, _omitEnumNames ? '' : 'Medic'); + + /// + /// ForwardObserver + static const MemberRole ForwardObserver = + MemberRole._(6, _omitEnumNames ? '' : 'ForwardObserver'); + + /// + /// Radio Telephone Operator + static const MemberRole RTO = MemberRole._(7, _omitEnumNames ? '' : 'RTO'); + + /// + /// Doggo + static const MemberRole K9 = MemberRole._(8, _omitEnumNames ? '' : 'K9'); + + static const $core.List values = [ + Unspecifed, + TeamMember, + TeamLead, + HQ, + Sniper, + Medic, + ForwardObserver, + RTO, + K9, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 8); + static MemberRole? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const MemberRole._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/atak.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/atak.pbjson.dart new file mode 100644 index 000000000..31ea59776 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/atak.pbjson.dart @@ -0,0 +1,229 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/atak.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use teamDescriptor instead') +const Team$json = { + '1': 'Team', + '2': [ + {'1': 'Unspecifed_Color', '2': 0}, + {'1': 'White', '2': 1}, + {'1': 'Yellow', '2': 2}, + {'1': 'Orange', '2': 3}, + {'1': 'Magenta', '2': 4}, + {'1': 'Red', '2': 5}, + {'1': 'Maroon', '2': 6}, + {'1': 'Purple', '2': 7}, + {'1': 'Dark_Blue', '2': 8}, + {'1': 'Blue', '2': 9}, + {'1': 'Cyan', '2': 10}, + {'1': 'Teal', '2': 11}, + {'1': 'Green', '2': 12}, + {'1': 'Dark_Green', '2': 13}, + {'1': 'Brown', '2': 14}, + ], +}; + +/// Descriptor for `Team`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List teamDescriptor = $convert.base64Decode( + 'CgRUZWFtEhQKEFVuc3BlY2lmZWRfQ29sb3IQABIJCgVXaGl0ZRABEgoKBlllbGxvdxACEgoKBk' + '9yYW5nZRADEgsKB01hZ2VudGEQBBIHCgNSZWQQBRIKCgZNYXJvb24QBhIKCgZQdXJwbGUQBxIN' + 'CglEYXJrX0JsdWUQCBIICgRCbHVlEAkSCAoEQ3lhbhAKEggKBFRlYWwQCxIJCgVHcmVlbhAMEg' + '4KCkRhcmtfR3JlZW4QDRIJCgVCcm93bhAO'); + +@$core.Deprecated('Use memberRoleDescriptor instead') +const MemberRole$json = { + '1': 'MemberRole', + '2': [ + {'1': 'Unspecifed', '2': 0}, + {'1': 'TeamMember', '2': 1}, + {'1': 'TeamLead', '2': 2}, + {'1': 'HQ', '2': 3}, + {'1': 'Sniper', '2': 4}, + {'1': 'Medic', '2': 5}, + {'1': 'ForwardObserver', '2': 6}, + {'1': 'RTO', '2': 7}, + {'1': 'K9', '2': 8}, + ], +}; + +/// Descriptor for `MemberRole`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List memberRoleDescriptor = $convert.base64Decode( + 'CgpNZW1iZXJSb2xlEg4KClVuc3BlY2lmZWQQABIOCgpUZWFtTWVtYmVyEAESDAoIVGVhbUxlYW' + 'QQAhIGCgJIURADEgoKBlNuaXBlchAEEgkKBU1lZGljEAUSEwoPRm9yd2FyZE9ic2VydmVyEAYS' + 'BwoDUlRPEAcSBgoCSzkQCA=='); + +@$core.Deprecated('Use tAKPacketDescriptor instead') +const TAKPacket$json = { + '1': 'TAKPacket', + '2': [ + {'1': 'is_compressed', '3': 1, '4': 1, '5': 8, '10': 'isCompressed'}, + { + '1': 'contact', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.Contact', + '10': 'contact' + }, + { + '1': 'group', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.Group', + '10': 'group' + }, + { + '1': 'status', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.Status', + '10': 'status' + }, + { + '1': 'pli', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.PLI', + '9': 0, + '10': 'pli' + }, + { + '1': 'chat', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.GeoChat', + '9': 0, + '10': 'chat' + }, + {'1': 'detail', '3': 7, '4': 1, '5': 12, '9': 0, '10': 'detail'}, + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +/// Descriptor for `TAKPacket`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List tAKPacketDescriptor = $convert.base64Decode( + 'CglUQUtQYWNrZXQSIwoNaXNfY29tcHJlc3NlZBgBIAEoCFIMaXNDb21wcmVzc2VkEi0KB2Nvbn' + 'RhY3QYAiABKAsyEy5tZXNodGFzdGljLkNvbnRhY3RSB2NvbnRhY3QSJwoFZ3JvdXAYAyABKAsy' + 'ES5tZXNodGFzdGljLkdyb3VwUgVncm91cBIqCgZzdGF0dXMYBCABKAsyEi5tZXNodGFzdGljLl' + 'N0YXR1c1IGc3RhdHVzEiMKA3BsaRgFIAEoCzIPLm1lc2h0YXN0aWMuUExJSABSA3BsaRIpCgRj' + 'aGF0GAYgASgLMhMubWVzaHRhc3RpYy5HZW9DaGF0SABSBGNoYXQSGAoGZGV0YWlsGAcgASgMSA' + 'BSBmRldGFpbEIRCg9wYXlsb2FkX3ZhcmlhbnQ='); + +@$core.Deprecated('Use geoChatDescriptor instead') +const GeoChat$json = { + '1': 'GeoChat', + '2': [ + {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + {'1': 'to', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'to', '17': true}, + { + '1': 'to_callsign', + '3': 3, + '4': 1, + '5': 9, + '9': 1, + '10': 'toCallsign', + '17': true + }, + ], + '8': [ + {'1': '_to'}, + {'1': '_to_callsign'}, + ], +}; + +/// Descriptor for `GeoChat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List geoChatDescriptor = $convert.base64Decode( + 'CgdHZW9DaGF0EhgKB21lc3NhZ2UYASABKAlSB21lc3NhZ2USEwoCdG8YAiABKAlIAFICdG+IAQ' + 'ESJAoLdG9fY2FsbHNpZ24YAyABKAlIAVIKdG9DYWxsc2lnbogBAUIFCgNfdG9CDgoMX3RvX2Nh' + 'bGxzaWdu'); + +@$core.Deprecated('Use groupDescriptor instead') +const Group$json = { + '1': 'Group', + '2': [ + { + '1': 'role', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.MemberRole', + '10': 'role' + }, + { + '1': 'team', + '3': 2, + '4': 1, + '5': 14, + '6': '.meshtastic.Team', + '10': 'team' + }, + ], +}; + +/// Descriptor for `Group`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List groupDescriptor = $convert.base64Decode( + 'CgVHcm91cBIqCgRyb2xlGAEgASgOMhYubWVzaHRhc3RpYy5NZW1iZXJSb2xlUgRyb2xlEiQKBH' + 'RlYW0YAiABKA4yEC5tZXNodGFzdGljLlRlYW1SBHRlYW0='); + +@$core.Deprecated('Use statusDescriptor instead') +const Status$json = { + '1': 'Status', + '2': [ + {'1': 'battery', '3': 1, '4': 1, '5': 13, '10': 'battery'}, + ], +}; + +/// Descriptor for `Status`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List statusDescriptor = + $convert.base64Decode('CgZTdGF0dXMSGAoHYmF0dGVyeRgBIAEoDVIHYmF0dGVyeQ=='); + +@$core.Deprecated('Use contactDescriptor instead') +const Contact$json = { + '1': 'Contact', + '2': [ + {'1': 'callsign', '3': 1, '4': 1, '5': 9, '10': 'callsign'}, + {'1': 'device_callsign', '3': 2, '4': 1, '5': 9, '10': 'deviceCallsign'}, + ], +}; + +/// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( + 'CgdDb250YWN0EhoKCGNhbGxzaWduGAEgASgJUghjYWxsc2lnbhInCg9kZXZpY2VfY2FsbHNpZ2' + '4YAiABKAlSDmRldmljZUNhbGxzaWdu'); + +@$core.Deprecated('Use pLIDescriptor instead') +const PLI$json = { + '1': 'PLI', + '2': [ + {'1': 'latitude_i', '3': 1, '4': 1, '5': 15, '10': 'latitudeI'}, + {'1': 'longitude_i', '3': 2, '4': 1, '5': 15, '10': 'longitudeI'}, + {'1': 'altitude', '3': 3, '4': 1, '5': 5, '10': 'altitude'}, + {'1': 'speed', '3': 4, '4': 1, '5': 13, '10': 'speed'}, + {'1': 'course', '3': 5, '4': 1, '5': 13, '10': 'course'}, + ], +}; + +/// Descriptor for `PLI`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pLIDescriptor = $convert.base64Decode( + 'CgNQTEkSHQoKbGF0aXR1ZGVfaRgBIAEoD1IJbGF0aXR1ZGVJEh8KC2xvbmdpdHVkZV9pGAIgAS' + 'gPUgpsb25naXR1ZGVJEhoKCGFsdGl0dWRlGAMgASgFUghhbHRpdHVkZRIUCgVzcGVlZBgEIAEo' + 'DVIFc3BlZWQSFgoGY291cnNlGAUgASgNUgZjb3Vyc2U='); diff --git a/third_party/meshtastic_flutter/lib/generated/cannedmessages.pb.dart b/third_party/meshtastic_flutter/lib/generated/cannedmessages.pb.dart new file mode 100644 index 000000000..ff80e9014 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/cannedmessages.pb.dart @@ -0,0 +1,84 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/cannedmessages.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// Canned message module configuration. +class CannedMessageModuleConfig extends $pb.GeneratedMessage { + factory CannedMessageModuleConfig({ + $core.String? messages, + }) { + final result = create(); + if (messages != null) result.messages = messages; + return result; + } + + CannedMessageModuleConfig._(); + + factory CannedMessageModuleConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory CannedMessageModuleConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'CannedMessageModuleConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'messages') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CannedMessageModuleConfig clone() => + CannedMessageModuleConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CannedMessageModuleConfig copyWith( + void Function(CannedMessageModuleConfig) updates) => + super.copyWith((message) => updates(message as CannedMessageModuleConfig)) + as CannedMessageModuleConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CannedMessageModuleConfig create() => CannedMessageModuleConfig._(); + @$core.override + CannedMessageModuleConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CannedMessageModuleConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static CannedMessageModuleConfig? _defaultInstance; + + /// + /// Predefined messages for canned message module separated by '|' characters. + @$pb.TagNumber(1) + $core.String get messages => $_getSZ(0); + @$pb.TagNumber(1) + set messages($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMessages() => $_has(0); + @$pb.TagNumber(1) + void clearMessages() => $_clearField(1); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/cannedmessages.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/cannedmessages.pbenum.dart new file mode 100644 index 000000000..6a93e9a4e --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/cannedmessages.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/cannedmessages.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/cannedmessages.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/cannedmessages.pbjson.dart new file mode 100644 index 000000000..582b455b7 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/cannedmessages.pbjson.dart @@ -0,0 +1,29 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/cannedmessages.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use cannedMessageModuleConfigDescriptor instead') +const CannedMessageModuleConfig$json = { + '1': 'CannedMessageModuleConfig', + '2': [ + {'1': 'messages', '3': 1, '4': 1, '5': 9, '10': 'messages'}, + ], +}; + +/// Descriptor for `CannedMessageModuleConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List cannedMessageModuleConfigDescriptor = + $convert.base64Decode( + 'ChlDYW5uZWRNZXNzYWdlTW9kdWxlQ29uZmlnEhoKCG1lc3NhZ2VzGAEgASgJUghtZXNzYWdlcw' + '=='); diff --git a/third_party/meshtastic_flutter/lib/generated/channel.pb.dart b/third_party/meshtastic_flutter/lib/generated/channel.pb.dart new file mode 100644 index 000000000..c8d6aec39 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/channel.pb.dart @@ -0,0 +1,388 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/channel.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'channel.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'channel.pbenum.dart'; + +/// +/// This information can be encoded as a QRcode/url so that other users can configure +/// their radio to join the same channel. +/// A note about how channel names are shown to users: channelname-X +/// poundsymbol is a prefix used to indicate this is a channel name (idea from @professr). +/// Where X is a letter from A-Z (base 26) representing a hash of the PSK for this +/// channel - so that if the user changes anything about the channel (which does +/// force a new PSK) this letter will also change. Thus preventing user confusion if +/// two friends try to type in a channel name of "BobsChan" and then can't talk +/// because their PSKs will be different. +/// The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26" +/// This also allows the option of someday if people have the PSK off (zero), the +/// users COULD type in a channel name and be able to talk. +/// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. +/// FIXME: explain how apps use channels for security. +/// explain how remote settings and remote gpio are managed as an example +class ChannelSettings extends $pb.GeneratedMessage { + factory ChannelSettings({ + @$core.Deprecated('This field is deprecated.') $core.int? channelNum, + $core.List<$core.int>? psk, + $core.String? name, + $core.int? id, + $core.bool? uplinkEnabled, + $core.bool? downlinkEnabled, + ModuleSettings? moduleSettings, + }) { + final result = create(); + if (channelNum != null) result.channelNum = channelNum; + if (psk != null) result.psk = psk; + if (name != null) result.name = name; + if (id != null) result.id = id; + if (uplinkEnabled != null) result.uplinkEnabled = uplinkEnabled; + if (downlinkEnabled != null) result.downlinkEnabled = downlinkEnabled; + if (moduleSettings != null) result.moduleSettings = moduleSettings; + return result; + } + + ChannelSettings._(); + + factory ChannelSettings.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ChannelSettings.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ChannelSettings', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'channelNum', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'psk', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'name') + ..a<$core.int>(4, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OF3) + ..aOB(5, _omitFieldNames ? '' : 'uplinkEnabled') + ..aOB(6, _omitFieldNames ? '' : 'downlinkEnabled') + ..aOM(7, _omitFieldNames ? '' : 'moduleSettings', + subBuilder: ModuleSettings.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChannelSettings clone() => ChannelSettings()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChannelSettings copyWith(void Function(ChannelSettings) updates) => + super.copyWith((message) => updates(message as ChannelSettings)) + as ChannelSettings; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChannelSettings create() => ChannelSettings._(); + @$core.override + ChannelSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChannelSettings getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ChannelSettings? _defaultInstance; + + /// + /// Deprecated in favor of LoraConfig.channel_num + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + $core.int get channelNum => $_getIZ(0); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + set channelNum($core.int value) => $_setUnsignedInt32(0, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + $core.bool hasChannelNum() => $_has(0); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + void clearChannelNum() => $_clearField(1); + + /// + /// A simple pre-shared key for now for crypto. + /// Must be either 0 bytes (no crypto), 16 bytes (AES128), or 32 bytes (AES256). + /// A special shorthand is used for 1 byte long psks. + /// These psks should be treated as only minimally secure, + /// because they are listed in this source code. + /// Those bytes are mapped using the following scheme: + /// `0` = No crypto + /// `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01} + /// `2` through 10 = The default channel key, except with 1 through 9 added to the last byte. + /// Shown to user as simple1 through 10 + @$pb.TagNumber(2) + $core.List<$core.int> get psk => $_getN(1); + @$pb.TagNumber(2) + set psk($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasPsk() => $_has(1); + @$pb.TagNumber(2) + void clearPsk() => $_clearField(2); + + /// + /// A SHORT name that will be packed into the URL. + /// Less than 12 bytes. + /// Something for end users to call the channel + /// If this is the empty string it is assumed that this channel + /// is the special (minimally secure) "Default"channel. + /// In user interfaces it should be rendered as a local language translation of "X". + /// For channel_num hashing empty string will be treated as "X". + /// Where "X" is selected based on the English words listed above for ModemPreset + @$pb.TagNumber(3) + $core.String get name => $_getSZ(2); + @$pb.TagNumber(3) + set name($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasName() => $_has(2); + @$pb.TagNumber(3) + void clearName() => $_clearField(3); + + /// + /// Used to construct a globally unique channel ID. + /// The full globally unique ID will be: "name.id" where ID is shown as base36. + /// Assuming that the number of meshtastic users is below 20K (true for a long time) + /// the chance of this 64 bit random number colliding with anyone else is super low. + /// And the penalty for collision is low as well, it just means that anyone trying to decrypt channel messages might need to + /// try multiple candidate channels. + /// Any time a non wire compatible change is made to a channel, this field should be regenerated. + /// There are a small number of 'special' globally known (and fairly) insecure standard channels. + /// Those channels do not have a numeric id included in the settings, but instead it is pulled from + /// a table of well known IDs. + /// (see Well Known Channels FIXME) + @$pb.TagNumber(4) + $core.int get id => $_getIZ(3); + @$pb.TagNumber(4) + set id($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasId() => $_has(3); + @$pb.TagNumber(4) + void clearId() => $_clearField(4); + + /// + /// If true, messages on the mesh will be sent to the *public* internet by any gateway ndoe + @$pb.TagNumber(5) + $core.bool get uplinkEnabled => $_getBF(4); + @$pb.TagNumber(5) + set uplinkEnabled($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasUplinkEnabled() => $_has(4); + @$pb.TagNumber(5) + void clearUplinkEnabled() => $_clearField(5); + + /// + /// If true, messages seen on the internet will be forwarded to the local mesh. + @$pb.TagNumber(6) + $core.bool get downlinkEnabled => $_getBF(5); + @$pb.TagNumber(6) + set downlinkEnabled($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasDownlinkEnabled() => $_has(5); + @$pb.TagNumber(6) + void clearDownlinkEnabled() => $_clearField(6); + + /// + /// Per-channel module settings. + @$pb.TagNumber(7) + ModuleSettings get moduleSettings => $_getN(6); + @$pb.TagNumber(7) + set moduleSettings(ModuleSettings value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasModuleSettings() => $_has(6); + @$pb.TagNumber(7) + void clearModuleSettings() => $_clearField(7); + @$pb.TagNumber(7) + ModuleSettings ensureModuleSettings() => $_ensure(6); +} + +/// +/// This message is specifically for modules to store per-channel configuration data. +class ModuleSettings extends $pb.GeneratedMessage { + factory ModuleSettings({ + $core.int? positionPrecision, + $core.bool? isClientMuted, + }) { + final result = create(); + if (positionPrecision != null) result.positionPrecision = positionPrecision; + if (isClientMuted != null) result.isClientMuted = isClientMuted; + return result; + } + + ModuleSettings._(); + + factory ModuleSettings.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleSettings.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleSettings', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'positionPrecision', $pb.PbFieldType.OU3) + ..aOB(2, _omitFieldNames ? '' : 'isClientMuted') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleSettings clone() => ModuleSettings()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleSettings copyWith(void Function(ModuleSettings) updates) => + super.copyWith((message) => updates(message as ModuleSettings)) + as ModuleSettings; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleSettings create() => ModuleSettings._(); + @$core.override + ModuleSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleSettings getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleSettings? _defaultInstance; + + /// + /// Bits of precision for the location sent in position packets. + @$pb.TagNumber(1) + $core.int get positionPrecision => $_getIZ(0); + @$pb.TagNumber(1) + set positionPrecision($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPositionPrecision() => $_has(0); + @$pb.TagNumber(1) + void clearPositionPrecision() => $_clearField(1); + + /// + /// Controls whether or not the phone / clients should mute the current channel + /// Useful for noisy public channels you don't necessarily want to disable + @$pb.TagNumber(2) + $core.bool get isClientMuted => $_getBF(1); + @$pb.TagNumber(2) + set isClientMuted($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasIsClientMuted() => $_has(1); + @$pb.TagNumber(2) + void clearIsClientMuted() => $_clearField(2); +} + +/// +/// A pair of a channel number, mode and the (sharable) settings for that channel +class Channel extends $pb.GeneratedMessage { + factory Channel({ + $core.int? index, + ChannelSettings? settings, + Channel_Role? role, + }) { + final result = create(); + if (index != null) result.index = index; + if (settings != null) result.settings = settings; + if (role != null) result.role = role; + return result; + } + + Channel._(); + + factory Channel.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Channel.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Channel', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'index', $pb.PbFieldType.O3) + ..aOM(2, _omitFieldNames ? '' : 'settings', + subBuilder: ChannelSettings.create) + ..e(3, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: Channel_Role.DISABLED, + valueOf: Channel_Role.valueOf, + enumValues: Channel_Role.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Channel clone() => Channel()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Channel copyWith(void Function(Channel) updates) => + super.copyWith((message) => updates(message as Channel)) as Channel; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Channel create() => Channel._(); + @$core.override + Channel createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Channel getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Channel? _defaultInstance; + + /// + /// The index of this channel in the channel table (from 0 to MAX_NUM_CHANNELS-1) + /// (Someday - not currently implemented) An index of -1 could be used to mean "set by name", + /// in which case the target node will find and set the channel by settings.name. + @$pb.TagNumber(1) + $core.int get index => $_getIZ(0); + @$pb.TagNumber(1) + set index($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasIndex() => $_has(0); + @$pb.TagNumber(1) + void clearIndex() => $_clearField(1); + + /// + /// The new settings, or NULL to disable that channel + @$pb.TagNumber(2) + ChannelSettings get settings => $_getN(1); + @$pb.TagNumber(2) + set settings(ChannelSettings value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasSettings() => $_has(1); + @$pb.TagNumber(2) + void clearSettings() => $_clearField(2); + @$pb.TagNumber(2) + ChannelSettings ensureSettings() => $_ensure(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(3) + Channel_Role get role => $_getN(2); + @$pb.TagNumber(3) + set role(Channel_Role value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasRole() => $_has(2); + @$pb.TagNumber(3) + void clearRole() => $_clearField(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/channel.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/channel.pbenum.dart new file mode 100644 index 000000000..82db79b76 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/channel.pbenum.dart @@ -0,0 +1,59 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/channel.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// How this channel is being used (or not). +/// Note: this field is an enum to give us options for the future. +/// In particular, someday we might make a 'SCANNING' option. +/// SCANNING channels could have different frequencies and the radio would +/// occasionally check that freq to see if anything is being transmitted. +/// For devices that have multiple physical radios attached, we could keep multiple PRIMARY/SCANNING channels active at once to allow +/// cross band routing as needed. +/// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time +/// (but any number of SECONDARY channels can't be sent received on that common frequency) +class Channel_Role extends $pb.ProtobufEnum { + /// + /// This channel is not in use right now + static const Channel_Role DISABLED = + Channel_Role._(0, _omitEnumNames ? '' : 'DISABLED'); + + /// + /// This channel is used to set the frequency for the radio - all other enabled channels must be SECONDARY + static const Channel_Role PRIMARY = + Channel_Role._(1, _omitEnumNames ? '' : 'PRIMARY'); + + /// + /// Secondary channels are only used for encryption/decryption/authentication purposes. + /// Their radio settings (freq etc) are ignored, only psk is used. + static const Channel_Role SECONDARY = + Channel_Role._(2, _omitEnumNames ? '' : 'SECONDARY'); + + static const $core.List values = [ + DISABLED, + PRIMARY, + SECONDARY, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static Channel_Role? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Channel_Role._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/channel.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/channel.pbjson.dart new file mode 100644 index 000000000..062ddd0fe --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/channel.pbjson.dart @@ -0,0 +1,113 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/channel.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use channelSettingsDescriptor instead') +const ChannelSettings$json = { + '1': 'ChannelSettings', + '2': [ + { + '1': 'channel_num', + '3': 1, + '4': 1, + '5': 13, + '8': {'3': true}, + '10': 'channelNum', + }, + {'1': 'psk', '3': 2, '4': 1, '5': 12, '10': 'psk'}, + {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'}, + {'1': 'id', '3': 4, '4': 1, '5': 7, '10': 'id'}, + {'1': 'uplink_enabled', '3': 5, '4': 1, '5': 8, '10': 'uplinkEnabled'}, + {'1': 'downlink_enabled', '3': 6, '4': 1, '5': 8, '10': 'downlinkEnabled'}, + { + '1': 'module_settings', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleSettings', + '10': 'moduleSettings' + }, + ], +}; + +/// Descriptor for `ChannelSettings`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List channelSettingsDescriptor = $convert.base64Decode( + 'Cg9DaGFubmVsU2V0dGluZ3MSIwoLY2hhbm5lbF9udW0YASABKA1CAhgBUgpjaGFubmVsTnVtEh' + 'AKA3BzaxgCIAEoDFIDcHNrEhIKBG5hbWUYAyABKAlSBG5hbWUSDgoCaWQYBCABKAdSAmlkEiUK' + 'DnVwbGlua19lbmFibGVkGAUgASgIUg11cGxpbmtFbmFibGVkEikKEGRvd25saW5rX2VuYWJsZW' + 'QYBiABKAhSD2Rvd25saW5rRW5hYmxlZBJDCg9tb2R1bGVfc2V0dGluZ3MYByABKAsyGi5tZXNo' + 'dGFzdGljLk1vZHVsZVNldHRpbmdzUg5tb2R1bGVTZXR0aW5ncw=='); + +@$core.Deprecated('Use moduleSettingsDescriptor instead') +const ModuleSettings$json = { + '1': 'ModuleSettings', + '2': [ + { + '1': 'position_precision', + '3': 1, + '4': 1, + '5': 13, + '10': 'positionPrecision' + }, + {'1': 'is_client_muted', '3': 2, '4': 1, '5': 8, '10': 'isClientMuted'}, + ], +}; + +/// Descriptor for `ModuleSettings`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List moduleSettingsDescriptor = $convert.base64Decode( + 'Cg5Nb2R1bGVTZXR0aW5ncxItChJwb3NpdGlvbl9wcmVjaXNpb24YASABKA1SEXBvc2l0aW9uUH' + 'JlY2lzaW9uEiYKD2lzX2NsaWVudF9tdXRlZBgCIAEoCFINaXNDbGllbnRNdXRlZA=='); + +@$core.Deprecated('Use channelDescriptor instead') +const Channel$json = { + '1': 'Channel', + '2': [ + {'1': 'index', '3': 1, '4': 1, '5': 5, '10': 'index'}, + { + '1': 'settings', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.ChannelSettings', + '10': 'settings' + }, + { + '1': 'role', + '3': 3, + '4': 1, + '5': 14, + '6': '.meshtastic.Channel.Role', + '10': 'role' + }, + ], + '4': [Channel_Role$json], +}; + +@$core.Deprecated('Use channelDescriptor instead') +const Channel_Role$json = { + '1': 'Role', + '2': [ + {'1': 'DISABLED', '2': 0}, + {'1': 'PRIMARY', '2': 1}, + {'1': 'SECONDARY', '2': 2}, + ], +}; + +/// Descriptor for `Channel`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List channelDescriptor = $convert.base64Decode( + 'CgdDaGFubmVsEhQKBWluZGV4GAEgASgFUgVpbmRleBI3CghzZXR0aW5ncxgCIAEoCzIbLm1lc2' + 'h0YXN0aWMuQ2hhbm5lbFNldHRpbmdzUghzZXR0aW5ncxIsCgRyb2xlGAMgASgOMhgubWVzaHRh' + 'c3RpYy5DaGFubmVsLlJvbGVSBHJvbGUiMAoEUm9sZRIMCghESVNBQkxFRBAAEgsKB1BSSU1BUl' + 'kQARINCglTRUNPTkRBUlkQAg=='); diff --git a/third_party/meshtastic_flutter/lib/generated/clientonly.pb.dart b/third_party/meshtastic_flutter/lib/generated/clientonly.pb.dart new file mode 100644 index 000000000..e3259f627 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/clientonly.pb.dart @@ -0,0 +1,193 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/clientonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'localonly.pb.dart' as $0; +import 'mesh.pb.dart' as $1; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// This abstraction is used to contain any configuration for provisioning a node on any client. +/// It is useful for importing and exporting configurations. +class DeviceProfile extends $pb.GeneratedMessage { + factory DeviceProfile({ + $core.String? longName, + $core.String? shortName, + $core.String? channelUrl, + $0.LocalConfig? config, + $0.LocalModuleConfig? moduleConfig, + $1.Position? fixedPosition, + $core.String? ringtone, + $core.String? cannedMessages, + }) { + final result = create(); + if (longName != null) result.longName = longName; + if (shortName != null) result.shortName = shortName; + if (channelUrl != null) result.channelUrl = channelUrl; + if (config != null) result.config = config; + if (moduleConfig != null) result.moduleConfig = moduleConfig; + if (fixedPosition != null) result.fixedPosition = fixedPosition; + if (ringtone != null) result.ringtone = ringtone; + if (cannedMessages != null) result.cannedMessages = cannedMessages; + return result; + } + + DeviceProfile._(); + + factory DeviceProfile.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeviceProfile.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeviceProfile', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'longName') + ..aOS(2, _omitFieldNames ? '' : 'shortName') + ..aOS(3, _omitFieldNames ? '' : 'channelUrl') + ..aOM<$0.LocalConfig>(4, _omitFieldNames ? '' : 'config', + subBuilder: $0.LocalConfig.create) + ..aOM<$0.LocalModuleConfig>(5, _omitFieldNames ? '' : 'moduleConfig', + subBuilder: $0.LocalModuleConfig.create) + ..aOM<$1.Position>(6, _omitFieldNames ? '' : 'fixedPosition', + subBuilder: $1.Position.create) + ..aOS(7, _omitFieldNames ? '' : 'ringtone') + ..aOS(8, _omitFieldNames ? '' : 'cannedMessages') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceProfile clone() => DeviceProfile()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceProfile copyWith(void Function(DeviceProfile) updates) => + super.copyWith((message) => updates(message as DeviceProfile)) + as DeviceProfile; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceProfile create() => DeviceProfile._(); + @$core.override + DeviceProfile createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceProfile getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeviceProfile? _defaultInstance; + + /// + /// Long name for the node + @$pb.TagNumber(1) + $core.String get longName => $_getSZ(0); + @$pb.TagNumber(1) + set longName($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasLongName() => $_has(0); + @$pb.TagNumber(1) + void clearLongName() => $_clearField(1); + + /// + /// Short name of the node + @$pb.TagNumber(2) + $core.String get shortName => $_getSZ(1); + @$pb.TagNumber(2) + set shortName($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasShortName() => $_has(1); + @$pb.TagNumber(2) + void clearShortName() => $_clearField(2); + + /// + /// The url of the channels from our node + @$pb.TagNumber(3) + $core.String get channelUrl => $_getSZ(2); + @$pb.TagNumber(3) + set channelUrl($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasChannelUrl() => $_has(2); + @$pb.TagNumber(3) + void clearChannelUrl() => $_clearField(3); + + /// + /// The Config of the node + @$pb.TagNumber(4) + $0.LocalConfig get config => $_getN(3); + @$pb.TagNumber(4) + set config($0.LocalConfig value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasConfig() => $_has(3); + @$pb.TagNumber(4) + void clearConfig() => $_clearField(4); + @$pb.TagNumber(4) + $0.LocalConfig ensureConfig() => $_ensure(3); + + /// + /// The ModuleConfig of the node + @$pb.TagNumber(5) + $0.LocalModuleConfig get moduleConfig => $_getN(4); + @$pb.TagNumber(5) + set moduleConfig($0.LocalModuleConfig value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasModuleConfig() => $_has(4); + @$pb.TagNumber(5) + void clearModuleConfig() => $_clearField(5); + @$pb.TagNumber(5) + $0.LocalModuleConfig ensureModuleConfig() => $_ensure(4); + + /// + /// Fixed position data + @$pb.TagNumber(6) + $1.Position get fixedPosition => $_getN(5); + @$pb.TagNumber(6) + set fixedPosition($1.Position value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasFixedPosition() => $_has(5); + @$pb.TagNumber(6) + void clearFixedPosition() => $_clearField(6); + @$pb.TagNumber(6) + $1.Position ensureFixedPosition() => $_ensure(5); + + /// + /// Ringtone for ExternalNotification + @$pb.TagNumber(7) + $core.String get ringtone => $_getSZ(6); + @$pb.TagNumber(7) + set ringtone($core.String value) => $_setString(6, value); + @$pb.TagNumber(7) + $core.bool hasRingtone() => $_has(6); + @$pb.TagNumber(7) + void clearRingtone() => $_clearField(7); + + /// + /// Predefined messages for CannedMessage + @$pb.TagNumber(8) + $core.String get cannedMessages => $_getSZ(7); + @$pb.TagNumber(8) + set cannedMessages($core.String value) => $_setString(7, value); + @$pb.TagNumber(8) + $core.bool hasCannedMessages() => $_has(7); + @$pb.TagNumber(8) + void clearCannedMessages() => $_clearField(8); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/clientonly.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/clientonly.pbenum.dart new file mode 100644 index 000000000..42bbc13a1 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/clientonly.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/clientonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/clientonly.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/clientonly.pbjson.dart new file mode 100644 index 000000000..e5db14e8b --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/clientonly.pbjson.dart @@ -0,0 +1,120 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/clientonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use deviceProfileDescriptor instead') +const DeviceProfile$json = { + '1': 'DeviceProfile', + '2': [ + { + '1': 'long_name', + '3': 1, + '4': 1, + '5': 9, + '9': 0, + '10': 'longName', + '17': true + }, + { + '1': 'short_name', + '3': 2, + '4': 1, + '5': 9, + '9': 1, + '10': 'shortName', + '17': true + }, + { + '1': 'channel_url', + '3': 3, + '4': 1, + '5': 9, + '9': 2, + '10': 'channelUrl', + '17': true + }, + { + '1': 'config', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.LocalConfig', + '9': 3, + '10': 'config', + '17': true + }, + { + '1': 'module_config', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.LocalModuleConfig', + '9': 4, + '10': 'moduleConfig', + '17': true + }, + { + '1': 'fixed_position', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.Position', + '9': 5, + '10': 'fixedPosition', + '17': true + }, + { + '1': 'ringtone', + '3': 7, + '4': 1, + '5': 9, + '9': 6, + '10': 'ringtone', + '17': true + }, + { + '1': 'canned_messages', + '3': 8, + '4': 1, + '5': 9, + '9': 7, + '10': 'cannedMessages', + '17': true + }, + ], + '8': [ + {'1': '_long_name'}, + {'1': '_short_name'}, + {'1': '_channel_url'}, + {'1': '_config'}, + {'1': '_module_config'}, + {'1': '_fixed_position'}, + {'1': '_ringtone'}, + {'1': '_canned_messages'}, + ], +}; + +/// Descriptor for `DeviceProfile`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceProfileDescriptor = $convert.base64Decode( + 'Cg1EZXZpY2VQcm9maWxlEiAKCWxvbmdfbmFtZRgBIAEoCUgAUghsb25nTmFtZYgBARIiCgpzaG' + '9ydF9uYW1lGAIgASgJSAFSCXNob3J0TmFtZYgBARIkCgtjaGFubmVsX3VybBgDIAEoCUgCUgpj' + 'aGFubmVsVXJsiAEBEjQKBmNvbmZpZxgEIAEoCzIXLm1lc2h0YXN0aWMuTG9jYWxDb25maWdIA1' + 'IGY29uZmlniAEBEkcKDW1vZHVsZV9jb25maWcYBSABKAsyHS5tZXNodGFzdGljLkxvY2FsTW9k' + 'dWxlQ29uZmlnSARSDG1vZHVsZUNvbmZpZ4gBARJACg5maXhlZF9wb3NpdGlvbhgGIAEoCzIULm' + '1lc2h0YXN0aWMuUG9zaXRpb25IBVINZml4ZWRQb3NpdGlvbogBARIfCghyaW5ndG9uZRgHIAEo' + 'CUgGUghyaW5ndG9uZYgBARIsCg9jYW5uZWRfbWVzc2FnZXMYCCABKAlIB1IOY2FubmVkTWVzc2' + 'FnZXOIAQFCDAoKX2xvbmdfbmFtZUINCgtfc2hvcnRfbmFtZUIOCgxfY2hhbm5lbF91cmxCCQoH' + 'X2NvbmZpZ0IQCg5fbW9kdWxlX2NvbmZpZ0IRCg9fZml4ZWRfcG9zaXRpb25CCwoJX3Jpbmd0b2' + '5lQhIKEF9jYW5uZWRfbWVzc2FnZXM='); diff --git a/third_party/meshtastic_flutter/lib/generated/config.pb.dart b/third_party/meshtastic_flutter/lib/generated/config.pb.dart new file mode 100644 index 000000000..e5933e77f --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/config.pb.dart @@ -0,0 +1,2136 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/config.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'config.pbenum.dart'; +import 'device_ui.pb.dart' as $0; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'config.pbenum.dart'; + +/// +/// Configuration +class Config_DeviceConfig extends $pb.GeneratedMessage { + factory Config_DeviceConfig({ + Config_DeviceConfig_Role? role, + @$core.Deprecated('This field is deprecated.') $core.bool? serialEnabled, + $core.int? buttonGpio, + $core.int? buzzerGpio, + Config_DeviceConfig_RebroadcastMode? rebroadcastMode, + $core.int? nodeInfoBroadcastSecs, + $core.bool? doubleTapAsButtonPress, + @$core.Deprecated('This field is deprecated.') $core.bool? isManaged, + $core.bool? disableTripleClick, + $core.String? tzdef, + $core.bool? ledHeartbeatDisabled, + Config_DeviceConfig_BuzzerMode? buzzerMode, + }) { + final result = create(); + if (role != null) result.role = role; + if (serialEnabled != null) result.serialEnabled = serialEnabled; + if (buttonGpio != null) result.buttonGpio = buttonGpio; + if (buzzerGpio != null) result.buzzerGpio = buzzerGpio; + if (rebroadcastMode != null) result.rebroadcastMode = rebroadcastMode; + if (nodeInfoBroadcastSecs != null) + result.nodeInfoBroadcastSecs = nodeInfoBroadcastSecs; + if (doubleTapAsButtonPress != null) + result.doubleTapAsButtonPress = doubleTapAsButtonPress; + if (isManaged != null) result.isManaged = isManaged; + if (disableTripleClick != null) + result.disableTripleClick = disableTripleClick; + if (tzdef != null) result.tzdef = tzdef; + if (ledHeartbeatDisabled != null) + result.ledHeartbeatDisabled = ledHeartbeatDisabled; + if (buzzerMode != null) result.buzzerMode = buzzerMode; + return result; + } + + Config_DeviceConfig._(); + + factory Config_DeviceConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_DeviceConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.DeviceConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e( + 1, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: Config_DeviceConfig_Role.CLIENT, + valueOf: Config_DeviceConfig_Role.valueOf, + enumValues: Config_DeviceConfig_Role.values) + ..aOB(2, _omitFieldNames ? '' : 'serialEnabled') + ..a<$core.int>(4, _omitFieldNames ? '' : 'buttonGpio', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'buzzerGpio', $pb.PbFieldType.OU3) + ..e( + 6, _omitFieldNames ? '' : 'rebroadcastMode', $pb.PbFieldType.OE, + defaultOrMaker: Config_DeviceConfig_RebroadcastMode.ALL, + valueOf: Config_DeviceConfig_RebroadcastMode.valueOf, + enumValues: Config_DeviceConfig_RebroadcastMode.values) + ..a<$core.int>( + 7, _omitFieldNames ? '' : 'nodeInfoBroadcastSecs', $pb.PbFieldType.OU3) + ..aOB(8, _omitFieldNames ? '' : 'doubleTapAsButtonPress') + ..aOB(9, _omitFieldNames ? '' : 'isManaged') + ..aOB(10, _omitFieldNames ? '' : 'disableTripleClick') + ..aOS(11, _omitFieldNames ? '' : 'tzdef') + ..aOB(12, _omitFieldNames ? '' : 'ledHeartbeatDisabled') + ..e( + 13, _omitFieldNames ? '' : 'buzzerMode', $pb.PbFieldType.OE, + defaultOrMaker: Config_DeviceConfig_BuzzerMode.ALL_ENABLED, + valueOf: Config_DeviceConfig_BuzzerMode.valueOf, + enumValues: Config_DeviceConfig_BuzzerMode.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_DeviceConfig clone() => Config_DeviceConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_DeviceConfig copyWith(void Function(Config_DeviceConfig) updates) => + super.copyWith((message) => updates(message as Config_DeviceConfig)) + as Config_DeviceConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_DeviceConfig create() => Config_DeviceConfig._(); + @$core.override + Config_DeviceConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_DeviceConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_DeviceConfig? _defaultInstance; + + /// + /// Sets the role of node + @$pb.TagNumber(1) + Config_DeviceConfig_Role get role => $_getN(0); + @$pb.TagNumber(1) + set role(Config_DeviceConfig_Role value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasRole() => $_has(0); + @$pb.TagNumber(1) + void clearRole() => $_clearField(1); + + /// + /// Disabling this will disable the SerialConsole by not initilizing the StreamAPI + /// Moved to SecurityConfig + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + $core.bool get serialEnabled => $_getBF(1); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + set serialEnabled($core.bool value) => $_setBool(1, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + $core.bool hasSerialEnabled() => $_has(1); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + void clearSerialEnabled() => $_clearField(2); + + /// + /// For boards without a hard wired button, this is the pin number that will be used + /// Boards that have more than one button can swap the function with this one. defaults to BUTTON_PIN if defined. + @$pb.TagNumber(4) + $core.int get buttonGpio => $_getIZ(2); + @$pb.TagNumber(4) + set buttonGpio($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(4) + $core.bool hasButtonGpio() => $_has(2); + @$pb.TagNumber(4) + void clearButtonGpio() => $_clearField(4); + + /// + /// For boards without a PWM buzzer, this is the pin number that will be used + /// Defaults to PIN_BUZZER if defined. + @$pb.TagNumber(5) + $core.int get buzzerGpio => $_getIZ(3); + @$pb.TagNumber(5) + set buzzerGpio($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(5) + $core.bool hasBuzzerGpio() => $_has(3); + @$pb.TagNumber(5) + void clearBuzzerGpio() => $_clearField(5); + + /// + /// Sets the role of node + @$pb.TagNumber(6) + Config_DeviceConfig_RebroadcastMode get rebroadcastMode => $_getN(4); + @$pb.TagNumber(6) + set rebroadcastMode(Config_DeviceConfig_RebroadcastMode value) => + $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasRebroadcastMode() => $_has(4); + @$pb.TagNumber(6) + void clearRebroadcastMode() => $_clearField(6); + + /// + /// Send our nodeinfo this often + /// Defaults to 900 Seconds (15 minutes) + @$pb.TagNumber(7) + $core.int get nodeInfoBroadcastSecs => $_getIZ(5); + @$pb.TagNumber(7) + set nodeInfoBroadcastSecs($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(7) + $core.bool hasNodeInfoBroadcastSecs() => $_has(5); + @$pb.TagNumber(7) + void clearNodeInfoBroadcastSecs() => $_clearField(7); + + /// + /// Treat double tap interrupt on supported accelerometers as a button press if set to true + @$pb.TagNumber(8) + $core.bool get doubleTapAsButtonPress => $_getBF(6); + @$pb.TagNumber(8) + set doubleTapAsButtonPress($core.bool value) => $_setBool(6, value); + @$pb.TagNumber(8) + $core.bool hasDoubleTapAsButtonPress() => $_has(6); + @$pb.TagNumber(8) + void clearDoubleTapAsButtonPress() => $_clearField(8); + + /// + /// If true, device is considered to be "managed" by a mesh administrator + /// Clients should then limit available configuration and administrative options inside the user interface + /// Moved to SecurityConfig + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + $core.bool get isManaged => $_getBF(7); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + set isManaged($core.bool value) => $_setBool(7, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + $core.bool hasIsManaged() => $_has(7); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + void clearIsManaged() => $_clearField(9); + + /// + /// Disables the triple-press of user button to enable or disable GPS + @$pb.TagNumber(10) + $core.bool get disableTripleClick => $_getBF(8); + @$pb.TagNumber(10) + set disableTripleClick($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(10) + $core.bool hasDisableTripleClick() => $_has(8); + @$pb.TagNumber(10) + void clearDisableTripleClick() => $_clearField(10); + + /// + /// POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv. + @$pb.TagNumber(11) + $core.String get tzdef => $_getSZ(9); + @$pb.TagNumber(11) + set tzdef($core.String value) => $_setString(9, value); + @$pb.TagNumber(11) + $core.bool hasTzdef() => $_has(9); + @$pb.TagNumber(11) + void clearTzdef() => $_clearField(11); + + /// + /// If true, disable the default blinking LED (LED_PIN) behavior on the device + @$pb.TagNumber(12) + $core.bool get ledHeartbeatDisabled => $_getBF(10); + @$pb.TagNumber(12) + set ledHeartbeatDisabled($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(12) + $core.bool hasLedHeartbeatDisabled() => $_has(10); + @$pb.TagNumber(12) + void clearLedHeartbeatDisabled() => $_clearField(12); + + /// + /// Controls buzzer behavior for audio feedback + /// Defaults to ENABLED + @$pb.TagNumber(13) + Config_DeviceConfig_BuzzerMode get buzzerMode => $_getN(11); + @$pb.TagNumber(13) + set buzzerMode(Config_DeviceConfig_BuzzerMode value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasBuzzerMode() => $_has(11); + @$pb.TagNumber(13) + void clearBuzzerMode() => $_clearField(13); +} + +/// +/// Position Config +class Config_PositionConfig extends $pb.GeneratedMessage { + factory Config_PositionConfig({ + $core.int? positionBroadcastSecs, + $core.bool? positionBroadcastSmartEnabled, + $core.bool? fixedPosition, + @$core.Deprecated('This field is deprecated.') $core.bool? gpsEnabled, + $core.int? gpsUpdateInterval, + @$core.Deprecated('This field is deprecated.') $core.int? gpsAttemptTime, + $core.int? positionFlags, + $core.int? rxGpio, + $core.int? txGpio, + $core.int? broadcastSmartMinimumDistance, + $core.int? broadcastSmartMinimumIntervalSecs, + $core.int? gpsEnGpio, + Config_PositionConfig_GpsMode? gpsMode, + }) { + final result = create(); + if (positionBroadcastSecs != null) + result.positionBroadcastSecs = positionBroadcastSecs; + if (positionBroadcastSmartEnabled != null) + result.positionBroadcastSmartEnabled = positionBroadcastSmartEnabled; + if (fixedPosition != null) result.fixedPosition = fixedPosition; + if (gpsEnabled != null) result.gpsEnabled = gpsEnabled; + if (gpsUpdateInterval != null) result.gpsUpdateInterval = gpsUpdateInterval; + if (gpsAttemptTime != null) result.gpsAttemptTime = gpsAttemptTime; + if (positionFlags != null) result.positionFlags = positionFlags; + if (rxGpio != null) result.rxGpio = rxGpio; + if (txGpio != null) result.txGpio = txGpio; + if (broadcastSmartMinimumDistance != null) + result.broadcastSmartMinimumDistance = broadcastSmartMinimumDistance; + if (broadcastSmartMinimumIntervalSecs != null) + result.broadcastSmartMinimumIntervalSecs = + broadcastSmartMinimumIntervalSecs; + if (gpsEnGpio != null) result.gpsEnGpio = gpsEnGpio; + if (gpsMode != null) result.gpsMode = gpsMode; + return result; + } + + Config_PositionConfig._(); + + factory Config_PositionConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_PositionConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.PositionConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'positionBroadcastSecs', $pb.PbFieldType.OU3) + ..aOB(2, _omitFieldNames ? '' : 'positionBroadcastSmartEnabled') + ..aOB(3, _omitFieldNames ? '' : 'fixedPosition') + ..aOB(4, _omitFieldNames ? '' : 'gpsEnabled') + ..a<$core.int>( + 5, _omitFieldNames ? '' : 'gpsUpdateInterval', $pb.PbFieldType.OU3) + ..a<$core.int>( + 6, _omitFieldNames ? '' : 'gpsAttemptTime', $pb.PbFieldType.OU3) + ..a<$core.int>( + 7, _omitFieldNames ? '' : 'positionFlags', $pb.PbFieldType.OU3) + ..a<$core.int>(8, _omitFieldNames ? '' : 'rxGpio', $pb.PbFieldType.OU3) + ..a<$core.int>(9, _omitFieldNames ? '' : 'txGpio', $pb.PbFieldType.OU3) + ..a<$core.int>(10, _omitFieldNames ? '' : 'broadcastSmartMinimumDistance', + $pb.PbFieldType.OU3) + ..a<$core.int>( + 11, + _omitFieldNames ? '' : 'broadcastSmartMinimumIntervalSecs', + $pb.PbFieldType.OU3) + ..a<$core.int>(12, _omitFieldNames ? '' : 'gpsEnGpio', $pb.PbFieldType.OU3) + ..e( + 13, _omitFieldNames ? '' : 'gpsMode', $pb.PbFieldType.OE, + defaultOrMaker: Config_PositionConfig_GpsMode.DISABLED, + valueOf: Config_PositionConfig_GpsMode.valueOf, + enumValues: Config_PositionConfig_GpsMode.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_PositionConfig clone() => + Config_PositionConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_PositionConfig copyWith( + void Function(Config_PositionConfig) updates) => + super.copyWith((message) => updates(message as Config_PositionConfig)) + as Config_PositionConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_PositionConfig create() => Config_PositionConfig._(); + @$core.override + Config_PositionConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_PositionConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_PositionConfig? _defaultInstance; + + /// + /// We should send our position this often (but only if it has changed significantly) + /// Defaults to 15 minutes + @$pb.TagNumber(1) + $core.int get positionBroadcastSecs => $_getIZ(0); + @$pb.TagNumber(1) + set positionBroadcastSecs($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPositionBroadcastSecs() => $_has(0); + @$pb.TagNumber(1) + void clearPositionBroadcastSecs() => $_clearField(1); + + /// + /// Adaptive position braoadcast, which is now the default. + @$pb.TagNumber(2) + $core.bool get positionBroadcastSmartEnabled => $_getBF(1); + @$pb.TagNumber(2) + set positionBroadcastSmartEnabled($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasPositionBroadcastSmartEnabled() => $_has(1); + @$pb.TagNumber(2) + void clearPositionBroadcastSmartEnabled() => $_clearField(2); + + /// + /// If set, this node is at a fixed position. + /// We will generate GPS position updates at the regular interval, but use whatever the last lat/lon/alt we have for the node. + /// The lat/lon/alt can be set by an internal GPS or with the help of the app. + @$pb.TagNumber(3) + $core.bool get fixedPosition => $_getBF(2); + @$pb.TagNumber(3) + set fixedPosition($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasFixedPosition() => $_has(2); + @$pb.TagNumber(3) + void clearFixedPosition() => $_clearField(3); + + /// + /// Is GPS enabled for this node? + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + $core.bool get gpsEnabled => $_getBF(3); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + set gpsEnabled($core.bool value) => $_setBool(3, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + $core.bool hasGpsEnabled() => $_has(3); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + void clearGpsEnabled() => $_clearField(4); + + /// + /// How often should we try to get GPS position (in seconds) + /// or zero for the default of once every 30 seconds + /// or a very large value (maxint) to update only once at boot. + @$pb.TagNumber(5) + $core.int get gpsUpdateInterval => $_getIZ(4); + @$pb.TagNumber(5) + set gpsUpdateInterval($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasGpsUpdateInterval() => $_has(4); + @$pb.TagNumber(5) + void clearGpsUpdateInterval() => $_clearField(5); + + /// + /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(6) + $core.int get gpsAttemptTime => $_getIZ(5); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(6) + set gpsAttemptTime($core.int value) => $_setUnsignedInt32(5, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(6) + $core.bool hasGpsAttemptTime() => $_has(5); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(6) + void clearGpsAttemptTime() => $_clearField(6); + + /// + /// Bit field of boolean configuration options for POSITION messages + /// (bitwise OR of PositionFlags) + @$pb.TagNumber(7) + $core.int get positionFlags => $_getIZ(6); + @$pb.TagNumber(7) + set positionFlags($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasPositionFlags() => $_has(6); + @$pb.TagNumber(7) + void clearPositionFlags() => $_clearField(7); + + /// + /// (Re)define GPS_RX_PIN for your board. + @$pb.TagNumber(8) + $core.int get rxGpio => $_getIZ(7); + @$pb.TagNumber(8) + set rxGpio($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasRxGpio() => $_has(7); + @$pb.TagNumber(8) + void clearRxGpio() => $_clearField(8); + + /// + /// (Re)define GPS_TX_PIN for your board. + @$pb.TagNumber(9) + $core.int get txGpio => $_getIZ(8); + @$pb.TagNumber(9) + set txGpio($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasTxGpio() => $_has(8); + @$pb.TagNumber(9) + void clearTxGpio() => $_clearField(9); + + /// + /// The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled + @$pb.TagNumber(10) + $core.int get broadcastSmartMinimumDistance => $_getIZ(9); + @$pb.TagNumber(10) + set broadcastSmartMinimumDistance($core.int value) => + $_setUnsignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasBroadcastSmartMinimumDistance() => $_has(9); + @$pb.TagNumber(10) + void clearBroadcastSmartMinimumDistance() => $_clearField(10); + + /// + /// The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled + @$pb.TagNumber(11) + $core.int get broadcastSmartMinimumIntervalSecs => $_getIZ(10); + @$pb.TagNumber(11) + set broadcastSmartMinimumIntervalSecs($core.int value) => + $_setUnsignedInt32(10, value); + @$pb.TagNumber(11) + $core.bool hasBroadcastSmartMinimumIntervalSecs() => $_has(10); + @$pb.TagNumber(11) + void clearBroadcastSmartMinimumIntervalSecs() => $_clearField(11); + + /// + /// (Re)define PIN_GPS_EN for your board. + @$pb.TagNumber(12) + $core.int get gpsEnGpio => $_getIZ(11); + @$pb.TagNumber(12) + set gpsEnGpio($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasGpsEnGpio() => $_has(11); + @$pb.TagNumber(12) + void clearGpsEnGpio() => $_clearField(12); + + /// + /// Set where GPS is enabled, disabled, or not present + @$pb.TagNumber(13) + Config_PositionConfig_GpsMode get gpsMode => $_getN(12); + @$pb.TagNumber(13) + set gpsMode(Config_PositionConfig_GpsMode value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasGpsMode() => $_has(12); + @$pb.TagNumber(13) + void clearGpsMode() => $_clearField(13); +} + +/// +/// Power Config\ +/// See [Power Config](/docs/settings/config/power) for additional power config details. +class Config_PowerConfig extends $pb.GeneratedMessage { + factory Config_PowerConfig({ + $core.bool? isPowerSaving, + $core.int? onBatteryShutdownAfterSecs, + $core.double? adcMultiplierOverride, + $core.int? waitBluetoothSecs, + $core.int? sdsSecs, + $core.int? lsSecs, + $core.int? minWakeSecs, + $core.int? deviceBatteryInaAddress, + $fixnum.Int64? powermonEnables, + }) { + final result = create(); + if (isPowerSaving != null) result.isPowerSaving = isPowerSaving; + if (onBatteryShutdownAfterSecs != null) + result.onBatteryShutdownAfterSecs = onBatteryShutdownAfterSecs; + if (adcMultiplierOverride != null) + result.adcMultiplierOverride = adcMultiplierOverride; + if (waitBluetoothSecs != null) result.waitBluetoothSecs = waitBluetoothSecs; + if (sdsSecs != null) result.sdsSecs = sdsSecs; + if (lsSecs != null) result.lsSecs = lsSecs; + if (minWakeSecs != null) result.minWakeSecs = minWakeSecs; + if (deviceBatteryInaAddress != null) + result.deviceBatteryInaAddress = deviceBatteryInaAddress; + if (powermonEnables != null) result.powermonEnables = powermonEnables; + return result; + } + + Config_PowerConfig._(); + + factory Config_PowerConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_PowerConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.PowerConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'isPowerSaving') + ..a<$core.int>(2, _omitFieldNames ? '' : 'onBatteryShutdownAfterSecs', + $pb.PbFieldType.OU3) + ..a<$core.double>( + 3, _omitFieldNames ? '' : 'adcMultiplierOverride', $pb.PbFieldType.OF) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'waitBluetoothSecs', $pb.PbFieldType.OU3) + ..a<$core.int>(6, _omitFieldNames ? '' : 'sdsSecs', $pb.PbFieldType.OU3) + ..a<$core.int>(7, _omitFieldNames ? '' : 'lsSecs', $pb.PbFieldType.OU3) + ..a<$core.int>(8, _omitFieldNames ? '' : 'minWakeSecs', $pb.PbFieldType.OU3) + ..a<$core.int>(9, _omitFieldNames ? '' : 'deviceBatteryInaAddress', + $pb.PbFieldType.OU3) + ..a<$fixnum.Int64>( + 32, _omitFieldNames ? '' : 'powermonEnables', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_PowerConfig clone() => Config_PowerConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_PowerConfig copyWith(void Function(Config_PowerConfig) updates) => + super.copyWith((message) => updates(message as Config_PowerConfig)) + as Config_PowerConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_PowerConfig create() => Config_PowerConfig._(); + @$core.override + Config_PowerConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_PowerConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_PowerConfig? _defaultInstance; + + /// + /// Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. + /// Don't use this setting if you want to use your device with the phone apps or are using a device without a user button. + /// Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles + @$pb.TagNumber(1) + $core.bool get isPowerSaving => $_getBF(0); + @$pb.TagNumber(1) + set isPowerSaving($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasIsPowerSaving() => $_has(0); + @$pb.TagNumber(1) + void clearIsPowerSaving() => $_clearField(1); + + /// + /// Description: If non-zero, the device will fully power off this many seconds after external power is removed. + @$pb.TagNumber(2) + $core.int get onBatteryShutdownAfterSecs => $_getIZ(1); + @$pb.TagNumber(2) + set onBatteryShutdownAfterSecs($core.int value) => + $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasOnBatteryShutdownAfterSecs() => $_has(1); + @$pb.TagNumber(2) + void clearOnBatteryShutdownAfterSecs() => $_clearField(2); + + /// + /// Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k) + /// Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation. + /// https://meshtastic.org/docs/configuration/radio/power/#adc-multiplier-override + /// Should be set to floating point value between 2 and 6 + @$pb.TagNumber(3) + $core.double get adcMultiplierOverride => $_getN(2); + @$pb.TagNumber(3) + set adcMultiplierOverride($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasAdcMultiplierOverride() => $_has(2); + @$pb.TagNumber(3) + void clearAdcMultiplierOverride() => $_clearField(3); + + /// + /// Description: The number of seconds for to wait before turning off BLE in No Bluetooth states + /// Technical Details: ESP32 Only 0 for default of 1 minute + @$pb.TagNumber(4) + $core.int get waitBluetoothSecs => $_getIZ(3); + @$pb.TagNumber(4) + set waitBluetoothSecs($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasWaitBluetoothSecs() => $_has(3); + @$pb.TagNumber(4) + void clearWaitBluetoothSecs() => $_clearField(4); + + /// + /// Super Deep Sleep Seconds + /// While in Light Sleep if mesh_sds_timeout_secs is exceeded we will lower into super deep sleep + /// for this value (default 1 year) or a button press + /// 0 for default of one year + @$pb.TagNumber(6) + $core.int get sdsSecs => $_getIZ(4); + @$pb.TagNumber(6) + set sdsSecs($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(6) + $core.bool hasSdsSecs() => $_has(4); + @$pb.TagNumber(6) + void clearSdsSecs() => $_clearField(6); + + /// + /// Description: In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on + /// Technical Details: ESP32 Only 0 for default of 300 + @$pb.TagNumber(7) + $core.int get lsSecs => $_getIZ(5); + @$pb.TagNumber(7) + set lsSecs($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(7) + $core.bool hasLsSecs() => $_has(5); + @$pb.TagNumber(7) + void clearLsSecs() => $_clearField(7); + + /// + /// Description: While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value + /// Technical Details: ESP32 Only 0 for default of 10 seconds + @$pb.TagNumber(8) + $core.int get minWakeSecs => $_getIZ(6); + @$pb.TagNumber(8) + set minWakeSecs($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(8) + $core.bool hasMinWakeSecs() => $_has(6); + @$pb.TagNumber(8) + void clearMinWakeSecs() => $_clearField(8); + + /// + /// I2C address of INA_2XX to use for reading device battery voltage + @$pb.TagNumber(9) + $core.int get deviceBatteryInaAddress => $_getIZ(7); + @$pb.TagNumber(9) + set deviceBatteryInaAddress($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(9) + $core.bool hasDeviceBatteryInaAddress() => $_has(7); + @$pb.TagNumber(9) + void clearDeviceBatteryInaAddress() => $_clearField(9); + + /// + /// If non-zero, we want powermon log outputs. With the particular (bitfield) sources enabled. + /// Note: we picked an ID of 32 so that lower more efficient IDs can be used for more frequently used options. + @$pb.TagNumber(32) + $fixnum.Int64 get powermonEnables => $_getI64(8); + @$pb.TagNumber(32) + set powermonEnables($fixnum.Int64 value) => $_setInt64(8, value); + @$pb.TagNumber(32) + $core.bool hasPowermonEnables() => $_has(8); + @$pb.TagNumber(32) + void clearPowermonEnables() => $_clearField(32); +} + +class Config_NetworkConfig_IpV4Config extends $pb.GeneratedMessage { + factory Config_NetworkConfig_IpV4Config({ + $core.int? ip, + $core.int? gateway, + $core.int? subnet, + $core.int? dns, + }) { + final result = create(); + if (ip != null) result.ip = ip; + if (gateway != null) result.gateway = gateway; + if (subnet != null) result.subnet = subnet; + if (dns != null) result.dns = dns; + return result; + } + + Config_NetworkConfig_IpV4Config._(); + + factory Config_NetworkConfig_IpV4Config.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_NetworkConfig_IpV4Config.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.NetworkConfig.IpV4Config', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'ip', $pb.PbFieldType.OF3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'gateway', $pb.PbFieldType.OF3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'subnet', $pb.PbFieldType.OF3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'dns', $pb.PbFieldType.OF3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_NetworkConfig_IpV4Config clone() => + Config_NetworkConfig_IpV4Config()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_NetworkConfig_IpV4Config copyWith( + void Function(Config_NetworkConfig_IpV4Config) updates) => + super.copyWith( + (message) => updates(message as Config_NetworkConfig_IpV4Config)) + as Config_NetworkConfig_IpV4Config; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_NetworkConfig_IpV4Config create() => + Config_NetworkConfig_IpV4Config._(); + @$core.override + Config_NetworkConfig_IpV4Config createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_NetworkConfig_IpV4Config getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static Config_NetworkConfig_IpV4Config? _defaultInstance; + + /// + /// Static IP address + @$pb.TagNumber(1) + $core.int get ip => $_getIZ(0); + @$pb.TagNumber(1) + set ip($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasIp() => $_has(0); + @$pb.TagNumber(1) + void clearIp() => $_clearField(1); + + /// + /// Static gateway address + @$pb.TagNumber(2) + $core.int get gateway => $_getIZ(1); + @$pb.TagNumber(2) + set gateway($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasGateway() => $_has(1); + @$pb.TagNumber(2) + void clearGateway() => $_clearField(2); + + /// + /// Static subnet mask + @$pb.TagNumber(3) + $core.int get subnet => $_getIZ(2); + @$pb.TagNumber(3) + set subnet($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasSubnet() => $_has(2); + @$pb.TagNumber(3) + void clearSubnet() => $_clearField(3); + + /// + /// Static DNS server address + @$pb.TagNumber(4) + $core.int get dns => $_getIZ(3); + @$pb.TagNumber(4) + set dns($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasDns() => $_has(3); + @$pb.TagNumber(4) + void clearDns() => $_clearField(4); +} + +/// +/// Network Config +class Config_NetworkConfig extends $pb.GeneratedMessage { + factory Config_NetworkConfig({ + $core.bool? wifiEnabled, + $core.String? wifiSsid, + $core.String? wifiPsk, + $core.String? ntpServer, + $core.bool? ethEnabled, + Config_NetworkConfig_AddressMode? addressMode, + Config_NetworkConfig_IpV4Config? ipv4Config, + $core.String? rsyslogServer, + $core.int? enabledProtocols, + $core.bool? ipv6Enabled, + }) { + final result = create(); + if (wifiEnabled != null) result.wifiEnabled = wifiEnabled; + if (wifiSsid != null) result.wifiSsid = wifiSsid; + if (wifiPsk != null) result.wifiPsk = wifiPsk; + if (ntpServer != null) result.ntpServer = ntpServer; + if (ethEnabled != null) result.ethEnabled = ethEnabled; + if (addressMode != null) result.addressMode = addressMode; + if (ipv4Config != null) result.ipv4Config = ipv4Config; + if (rsyslogServer != null) result.rsyslogServer = rsyslogServer; + if (enabledProtocols != null) result.enabledProtocols = enabledProtocols; + if (ipv6Enabled != null) result.ipv6Enabled = ipv6Enabled; + return result; + } + + Config_NetworkConfig._(); + + factory Config_NetworkConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_NetworkConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.NetworkConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'wifiEnabled') + ..aOS(3, _omitFieldNames ? '' : 'wifiSsid') + ..aOS(4, _omitFieldNames ? '' : 'wifiPsk') + ..aOS(5, _omitFieldNames ? '' : 'ntpServer') + ..aOB(6, _omitFieldNames ? '' : 'ethEnabled') + ..e( + 7, _omitFieldNames ? '' : 'addressMode', $pb.PbFieldType.OE, + defaultOrMaker: Config_NetworkConfig_AddressMode.DHCP, + valueOf: Config_NetworkConfig_AddressMode.valueOf, + enumValues: Config_NetworkConfig_AddressMode.values) + ..aOM( + 8, _omitFieldNames ? '' : 'ipv4Config', + subBuilder: Config_NetworkConfig_IpV4Config.create) + ..aOS(9, _omitFieldNames ? '' : 'rsyslogServer') + ..a<$core.int>( + 10, _omitFieldNames ? '' : 'enabledProtocols', $pb.PbFieldType.OU3) + ..aOB(11, _omitFieldNames ? '' : 'ipv6Enabled') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_NetworkConfig clone() => + Config_NetworkConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_NetworkConfig copyWith(void Function(Config_NetworkConfig) updates) => + super.copyWith((message) => updates(message as Config_NetworkConfig)) + as Config_NetworkConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_NetworkConfig create() => Config_NetworkConfig._(); + @$core.override + Config_NetworkConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_NetworkConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_NetworkConfig? _defaultInstance; + + /// + /// Enable WiFi (disables Bluetooth) + @$pb.TagNumber(1) + $core.bool get wifiEnabled => $_getBF(0); + @$pb.TagNumber(1) + set wifiEnabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasWifiEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearWifiEnabled() => $_clearField(1); + + /// + /// If set, this node will try to join the specified wifi network and + /// acquire an address via DHCP + @$pb.TagNumber(3) + $core.String get wifiSsid => $_getSZ(1); + @$pb.TagNumber(3) + set wifiSsid($core.String value) => $_setString(1, value); + @$pb.TagNumber(3) + $core.bool hasWifiSsid() => $_has(1); + @$pb.TagNumber(3) + void clearWifiSsid() => $_clearField(3); + + /// + /// If set, will be use to authenticate to the named wifi + @$pb.TagNumber(4) + $core.String get wifiPsk => $_getSZ(2); + @$pb.TagNumber(4) + set wifiPsk($core.String value) => $_setString(2, value); + @$pb.TagNumber(4) + $core.bool hasWifiPsk() => $_has(2); + @$pb.TagNumber(4) + void clearWifiPsk() => $_clearField(4); + + /// + /// NTP server to use if WiFi is conneced, defaults to `meshtastic.pool.ntp.org` + @$pb.TagNumber(5) + $core.String get ntpServer => $_getSZ(3); + @$pb.TagNumber(5) + set ntpServer($core.String value) => $_setString(3, value); + @$pb.TagNumber(5) + $core.bool hasNtpServer() => $_has(3); + @$pb.TagNumber(5) + void clearNtpServer() => $_clearField(5); + + /// + /// Enable Ethernet + @$pb.TagNumber(6) + $core.bool get ethEnabled => $_getBF(4); + @$pb.TagNumber(6) + set ethEnabled($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(6) + $core.bool hasEthEnabled() => $_has(4); + @$pb.TagNumber(6) + void clearEthEnabled() => $_clearField(6); + + /// + /// acquire an address via DHCP or assign static + @$pb.TagNumber(7) + Config_NetworkConfig_AddressMode get addressMode => $_getN(5); + @$pb.TagNumber(7) + set addressMode(Config_NetworkConfig_AddressMode value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasAddressMode() => $_has(5); + @$pb.TagNumber(7) + void clearAddressMode() => $_clearField(7); + + /// + /// struct to keep static address + @$pb.TagNumber(8) + Config_NetworkConfig_IpV4Config get ipv4Config => $_getN(6); + @$pb.TagNumber(8) + set ipv4Config(Config_NetworkConfig_IpV4Config value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasIpv4Config() => $_has(6); + @$pb.TagNumber(8) + void clearIpv4Config() => $_clearField(8); + @$pb.TagNumber(8) + Config_NetworkConfig_IpV4Config ensureIpv4Config() => $_ensure(6); + + /// + /// rsyslog Server and Port + @$pb.TagNumber(9) + $core.String get rsyslogServer => $_getSZ(7); + @$pb.TagNumber(9) + set rsyslogServer($core.String value) => $_setString(7, value); + @$pb.TagNumber(9) + $core.bool hasRsyslogServer() => $_has(7); + @$pb.TagNumber(9) + void clearRsyslogServer() => $_clearField(9); + + /// + /// Flags for enabling/disabling network protocols + @$pb.TagNumber(10) + $core.int get enabledProtocols => $_getIZ(8); + @$pb.TagNumber(10) + set enabledProtocols($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(10) + $core.bool hasEnabledProtocols() => $_has(8); + @$pb.TagNumber(10) + void clearEnabledProtocols() => $_clearField(10); + + /// + /// Enable/Disable ipv6 support + @$pb.TagNumber(11) + $core.bool get ipv6Enabled => $_getBF(9); + @$pb.TagNumber(11) + set ipv6Enabled($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(11) + $core.bool hasIpv6Enabled() => $_has(9); + @$pb.TagNumber(11) + void clearIpv6Enabled() => $_clearField(11); +} + +/// +/// Display Config +class Config_DisplayConfig extends $pb.GeneratedMessage { + factory Config_DisplayConfig({ + $core.int? screenOnSecs, + @$core.Deprecated('This field is deprecated.') + Config_DisplayConfig_GpsCoordinateFormat? gpsFormat, + $core.int? autoScreenCarouselSecs, + @$core.Deprecated('This field is deprecated.') $core.bool? compassNorthTop, + $core.bool? flipScreen, + Config_DisplayConfig_DisplayUnits? units, + Config_DisplayConfig_OledType? oled, + Config_DisplayConfig_DisplayMode? displaymode, + $core.bool? headingBold, + $core.bool? wakeOnTapOrMotion, + Config_DisplayConfig_CompassOrientation? compassOrientation, + $core.bool? use12hClock, + }) { + final result = create(); + if (screenOnSecs != null) result.screenOnSecs = screenOnSecs; + if (gpsFormat != null) result.gpsFormat = gpsFormat; + if (autoScreenCarouselSecs != null) + result.autoScreenCarouselSecs = autoScreenCarouselSecs; + if (compassNorthTop != null) result.compassNorthTop = compassNorthTop; + if (flipScreen != null) result.flipScreen = flipScreen; + if (units != null) result.units = units; + if (oled != null) result.oled = oled; + if (displaymode != null) result.displaymode = displaymode; + if (headingBold != null) result.headingBold = headingBold; + if (wakeOnTapOrMotion != null) result.wakeOnTapOrMotion = wakeOnTapOrMotion; + if (compassOrientation != null) + result.compassOrientation = compassOrientation; + if (use12hClock != null) result.use12hClock = use12hClock; + return result; + } + + Config_DisplayConfig._(); + + factory Config_DisplayConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_DisplayConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.DisplayConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'screenOnSecs', $pb.PbFieldType.OU3) + ..e( + 2, _omitFieldNames ? '' : 'gpsFormat', $pb.PbFieldType.OE, + defaultOrMaker: Config_DisplayConfig_GpsCoordinateFormat.DEC, + valueOf: Config_DisplayConfig_GpsCoordinateFormat.valueOf, + enumValues: Config_DisplayConfig_GpsCoordinateFormat.values) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'autoScreenCarouselSecs', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'compassNorthTop') + ..aOB(5, _omitFieldNames ? '' : 'flipScreen') + ..e( + 6, _omitFieldNames ? '' : 'units', $pb.PbFieldType.OE, + defaultOrMaker: Config_DisplayConfig_DisplayUnits.METRIC, + valueOf: Config_DisplayConfig_DisplayUnits.valueOf, + enumValues: Config_DisplayConfig_DisplayUnits.values) + ..e( + 7, _omitFieldNames ? '' : 'oled', $pb.PbFieldType.OE, + defaultOrMaker: Config_DisplayConfig_OledType.OLED_AUTO, + valueOf: Config_DisplayConfig_OledType.valueOf, + enumValues: Config_DisplayConfig_OledType.values) + ..e( + 8, _omitFieldNames ? '' : 'displaymode', $pb.PbFieldType.OE, + defaultOrMaker: Config_DisplayConfig_DisplayMode.DEFAULT, + valueOf: Config_DisplayConfig_DisplayMode.valueOf, + enumValues: Config_DisplayConfig_DisplayMode.values) + ..aOB(9, _omitFieldNames ? '' : 'headingBold') + ..aOB(10, _omitFieldNames ? '' : 'wakeOnTapOrMotion') + ..e( + 11, _omitFieldNames ? '' : 'compassOrientation', $pb.PbFieldType.OE, + defaultOrMaker: Config_DisplayConfig_CompassOrientation.DEGREES_0, + valueOf: Config_DisplayConfig_CompassOrientation.valueOf, + enumValues: Config_DisplayConfig_CompassOrientation.values) + ..aOB(12, _omitFieldNames ? '' : 'use12hClock', protoName: 'use_12h_clock') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_DisplayConfig clone() => + Config_DisplayConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_DisplayConfig copyWith(void Function(Config_DisplayConfig) updates) => + super.copyWith((message) => updates(message as Config_DisplayConfig)) + as Config_DisplayConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_DisplayConfig create() => Config_DisplayConfig._(); + @$core.override + Config_DisplayConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_DisplayConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_DisplayConfig? _defaultInstance; + + /// + /// Number of seconds the screen stays on after pressing the user button or receiving a message + /// 0 for default of one minute MAXUINT for always on + @$pb.TagNumber(1) + $core.int get screenOnSecs => $_getIZ(0); + @$pb.TagNumber(1) + set screenOnSecs($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasScreenOnSecs() => $_has(0); + @$pb.TagNumber(1) + void clearScreenOnSecs() => $_clearField(1); + + /// + /// Deprecated in 2.7.4: Unused + /// How the GPS coordinates are formatted on the OLED screen. + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + Config_DisplayConfig_GpsCoordinateFormat get gpsFormat => $_getN(1); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + set gpsFormat(Config_DisplayConfig_GpsCoordinateFormat value) => + $_setField(2, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + $core.bool hasGpsFormat() => $_has(1); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(2) + void clearGpsFormat() => $_clearField(2); + + /// + /// Automatically toggles to the next page on the screen like a carousel, based the specified interval in seconds. + /// Potentially useful for devices without user buttons. + @$pb.TagNumber(3) + $core.int get autoScreenCarouselSecs => $_getIZ(2); + @$pb.TagNumber(3) + set autoScreenCarouselSecs($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasAutoScreenCarouselSecs() => $_has(2); + @$pb.TagNumber(3) + void clearAutoScreenCarouselSecs() => $_clearField(3); + + /// + /// If this is set, the displayed compass will always point north. if unset, the old behaviour + /// (top of display is heading direction) is used. + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + $core.bool get compassNorthTop => $_getBF(3); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + set compassNorthTop($core.bool value) => $_setBool(3, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + $core.bool hasCompassNorthTop() => $_has(3); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + void clearCompassNorthTop() => $_clearField(4); + + /// + /// Flip screen vertically, for cases that mount the screen upside down + @$pb.TagNumber(5) + $core.bool get flipScreen => $_getBF(4); + @$pb.TagNumber(5) + set flipScreen($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasFlipScreen() => $_has(4); + @$pb.TagNumber(5) + void clearFlipScreen() => $_clearField(5); + + /// + /// Perferred display units + @$pb.TagNumber(6) + Config_DisplayConfig_DisplayUnits get units => $_getN(5); + @$pb.TagNumber(6) + set units(Config_DisplayConfig_DisplayUnits value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasUnits() => $_has(5); + @$pb.TagNumber(6) + void clearUnits() => $_clearField(6); + + /// + /// Override auto-detect in screen + @$pb.TagNumber(7) + Config_DisplayConfig_OledType get oled => $_getN(6); + @$pb.TagNumber(7) + set oled(Config_DisplayConfig_OledType value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasOled() => $_has(6); + @$pb.TagNumber(7) + void clearOled() => $_clearField(7); + + /// + /// Display Mode + @$pb.TagNumber(8) + Config_DisplayConfig_DisplayMode get displaymode => $_getN(7); + @$pb.TagNumber(8) + set displaymode(Config_DisplayConfig_DisplayMode value) => + $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasDisplaymode() => $_has(7); + @$pb.TagNumber(8) + void clearDisplaymode() => $_clearField(8); + + /// + /// Print first line in pseudo-bold? FALSE is original style, TRUE is bold + @$pb.TagNumber(9) + $core.bool get headingBold => $_getBF(8); + @$pb.TagNumber(9) + set headingBold($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasHeadingBold() => $_has(8); + @$pb.TagNumber(9) + void clearHeadingBold() => $_clearField(9); + + /// + /// Should we wake the screen up on accelerometer detected motion or tap + @$pb.TagNumber(10) + $core.bool get wakeOnTapOrMotion => $_getBF(9); + @$pb.TagNumber(10) + set wakeOnTapOrMotion($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasWakeOnTapOrMotion() => $_has(9); + @$pb.TagNumber(10) + void clearWakeOnTapOrMotion() => $_clearField(10); + + /// + /// Indicates how to rotate or invert the compass output to accurate display on the display. + @$pb.TagNumber(11) + Config_DisplayConfig_CompassOrientation get compassOrientation => $_getN(10); + @$pb.TagNumber(11) + set compassOrientation(Config_DisplayConfig_CompassOrientation value) => + $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasCompassOrientation() => $_has(10); + @$pb.TagNumber(11) + void clearCompassOrientation() => $_clearField(11); + + /// + /// If false (default), the device will display the time in 24-hour format on screen. + /// If true, the device will display the time in 12-hour format on screen. + @$pb.TagNumber(12) + $core.bool get use12hClock => $_getBF(11); + @$pb.TagNumber(12) + set use12hClock($core.bool value) => $_setBool(11, value); + @$pb.TagNumber(12) + $core.bool hasUse12hClock() => $_has(11); + @$pb.TagNumber(12) + void clearUse12hClock() => $_clearField(12); +} + +/// +/// Lora Config +class Config_LoRaConfig extends $pb.GeneratedMessage { + factory Config_LoRaConfig({ + $core.bool? usePreset, + Config_LoRaConfig_ModemPreset? modemPreset, + $core.int? bandwidth, + $core.int? spreadFactor, + $core.int? codingRate, + $core.double? frequencyOffset, + Config_LoRaConfig_RegionCode? region, + $core.int? hopLimit, + $core.bool? txEnabled, + $core.int? txPower, + $core.int? channelNum, + $core.bool? overrideDutyCycle, + $core.bool? sx126xRxBoostedGain, + $core.double? overrideFrequency, + $core.bool? paFanDisabled, + $core.Iterable<$core.int>? ignoreIncoming, + $core.bool? ignoreMqtt, + $core.bool? configOkToMqtt, + }) { + final result = create(); + if (usePreset != null) result.usePreset = usePreset; + if (modemPreset != null) result.modemPreset = modemPreset; + if (bandwidth != null) result.bandwidth = bandwidth; + if (spreadFactor != null) result.spreadFactor = spreadFactor; + if (codingRate != null) result.codingRate = codingRate; + if (frequencyOffset != null) result.frequencyOffset = frequencyOffset; + if (region != null) result.region = region; + if (hopLimit != null) result.hopLimit = hopLimit; + if (txEnabled != null) result.txEnabled = txEnabled; + if (txPower != null) result.txPower = txPower; + if (channelNum != null) result.channelNum = channelNum; + if (overrideDutyCycle != null) result.overrideDutyCycle = overrideDutyCycle; + if (sx126xRxBoostedGain != null) + result.sx126xRxBoostedGain = sx126xRxBoostedGain; + if (overrideFrequency != null) result.overrideFrequency = overrideFrequency; + if (paFanDisabled != null) result.paFanDisabled = paFanDisabled; + if (ignoreIncoming != null) result.ignoreIncoming.addAll(ignoreIncoming); + if (ignoreMqtt != null) result.ignoreMqtt = ignoreMqtt; + if (configOkToMqtt != null) result.configOkToMqtt = configOkToMqtt; + return result; + } + + Config_LoRaConfig._(); + + factory Config_LoRaConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_LoRaConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.LoRaConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'usePreset') + ..e( + 2, _omitFieldNames ? '' : 'modemPreset', $pb.PbFieldType.OE, + defaultOrMaker: Config_LoRaConfig_ModemPreset.LONG_FAST, + valueOf: Config_LoRaConfig_ModemPreset.valueOf, + enumValues: Config_LoRaConfig_ModemPreset.values) + ..a<$core.int>(3, _omitFieldNames ? '' : 'bandwidth', $pb.PbFieldType.OU3) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'spreadFactor', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'codingRate', $pb.PbFieldType.OU3) + ..a<$core.double>( + 6, _omitFieldNames ? '' : 'frequencyOffset', $pb.PbFieldType.OF) + ..e( + 7, _omitFieldNames ? '' : 'region', $pb.PbFieldType.OE, + defaultOrMaker: Config_LoRaConfig_RegionCode.UNSET, + valueOf: Config_LoRaConfig_RegionCode.valueOf, + enumValues: Config_LoRaConfig_RegionCode.values) + ..a<$core.int>(8, _omitFieldNames ? '' : 'hopLimit', $pb.PbFieldType.OU3) + ..aOB(9, _omitFieldNames ? '' : 'txEnabled') + ..a<$core.int>(10, _omitFieldNames ? '' : 'txPower', $pb.PbFieldType.O3) + ..a<$core.int>(11, _omitFieldNames ? '' : 'channelNum', $pb.PbFieldType.OU3) + ..aOB(12, _omitFieldNames ? '' : 'overrideDutyCycle') + ..aOB(13, _omitFieldNames ? '' : 'sx126xRxBoostedGain') + ..a<$core.double>( + 14, _omitFieldNames ? '' : 'overrideFrequency', $pb.PbFieldType.OF) + ..aOB(15, _omitFieldNames ? '' : 'paFanDisabled') + ..p<$core.int>( + 103, _omitFieldNames ? '' : 'ignoreIncoming', $pb.PbFieldType.KU3) + ..aOB(104, _omitFieldNames ? '' : 'ignoreMqtt') + ..aOB(105, _omitFieldNames ? '' : 'configOkToMqtt') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_LoRaConfig clone() => Config_LoRaConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_LoRaConfig copyWith(void Function(Config_LoRaConfig) updates) => + super.copyWith((message) => updates(message as Config_LoRaConfig)) + as Config_LoRaConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_LoRaConfig create() => Config_LoRaConfig._(); + @$core.override + Config_LoRaConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_LoRaConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_LoRaConfig? _defaultInstance; + + /// + /// When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate` + /// will be taked from their respective manually defined fields + @$pb.TagNumber(1) + $core.bool get usePreset => $_getBF(0); + @$pb.TagNumber(1) + set usePreset($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasUsePreset() => $_has(0); + @$pb.TagNumber(1) + void clearUsePreset() => $_clearField(1); + + /// + /// Either modem_config or bandwidth/spreading/coding will be specified - NOT BOTH. + /// As a heuristic: If bandwidth is specified, do not use modem_config. + /// Because protobufs take ZERO space when the value is zero this works out nicely. + /// This value is replaced by bandwidth/spread_factor/coding_rate. + /// If you'd like to experiment with other options add them to MeshRadio.cpp in the device code. + @$pb.TagNumber(2) + Config_LoRaConfig_ModemPreset get modemPreset => $_getN(1); + @$pb.TagNumber(2) + set modemPreset(Config_LoRaConfig_ModemPreset value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasModemPreset() => $_has(1); + @$pb.TagNumber(2) + void clearModemPreset() => $_clearField(2); + + /// + /// Bandwidth in MHz + /// Certain bandwidth numbers are 'special' and will be converted to the + /// appropriate floating point value: 31 -> 31.25MHz + @$pb.TagNumber(3) + $core.int get bandwidth => $_getIZ(2); + @$pb.TagNumber(3) + set bandwidth($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasBandwidth() => $_has(2); + @$pb.TagNumber(3) + void clearBandwidth() => $_clearField(3); + + /// + /// A number from 7 to 12. + /// Indicates number of chirps per symbol as 1< $_getIZ(3); + @$pb.TagNumber(4) + set spreadFactor($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasSpreadFactor() => $_has(3); + @$pb.TagNumber(4) + void clearSpreadFactor() => $_clearField(4); + + /// + /// The denominator of the coding rate. + /// ie for 4/5, the value is 5. 4/8 the value is 8. + @$pb.TagNumber(5) + $core.int get codingRate => $_getIZ(4); + @$pb.TagNumber(5) + set codingRate($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasCodingRate() => $_has(4); + @$pb.TagNumber(5) + void clearCodingRate() => $_clearField(5); + + /// + /// This parameter is for advanced users with advanced test equipment, we do not recommend most users use it. + /// A frequency offset that is added to to the calculated band center frequency. + /// Used to correct for crystal calibration errors. + @$pb.TagNumber(6) + $core.double get frequencyOffset => $_getN(5); + @$pb.TagNumber(6) + set frequencyOffset($core.double value) => $_setFloat(5, value); + @$pb.TagNumber(6) + $core.bool hasFrequencyOffset() => $_has(5); + @$pb.TagNumber(6) + void clearFrequencyOffset() => $_clearField(6); + + /// + /// The region code for the radio (US, CN, EU433, etc...) + @$pb.TagNumber(7) + Config_LoRaConfig_RegionCode get region => $_getN(6); + @$pb.TagNumber(7) + set region(Config_LoRaConfig_RegionCode value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasRegion() => $_has(6); + @$pb.TagNumber(7) + void clearRegion() => $_clearField(7); + + /// + /// Maximum number of hops. This can't be greater than 7. + /// Default of 3 + /// Attempting to set a value > 7 results in the default + @$pb.TagNumber(8) + $core.int get hopLimit => $_getIZ(7); + @$pb.TagNumber(8) + set hopLimit($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasHopLimit() => $_has(7); + @$pb.TagNumber(8) + void clearHopLimit() => $_clearField(8); + + /// + /// Disable TX from the LoRa radio. Useful for hot-swapping antennas and other tests. + /// Defaults to false + @$pb.TagNumber(9) + $core.bool get txEnabled => $_getBF(8); + @$pb.TagNumber(9) + set txEnabled($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasTxEnabled() => $_has(8); + @$pb.TagNumber(9) + void clearTxEnabled() => $_clearField(9); + + /// + /// If zero, then use default max legal continuous power (ie. something that won't + /// burn out the radio hardware) + /// In most cases you should use zero here. + /// Units are in dBm. + @$pb.TagNumber(10) + $core.int get txPower => $_getIZ(9); + @$pb.TagNumber(10) + set txPower($core.int value) => $_setSignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasTxPower() => $_has(9); + @$pb.TagNumber(10) + void clearTxPower() => $_clearField(10); + + /// + /// This controls the actual hardware frequency the radio transmits on. + /// Most users should never need to be exposed to this field/concept. + /// A channel number between 1 and NUM_CHANNELS (whatever the max is in the current region). + /// If ZERO then the rule is "use the old channel name hash based + /// algorithm to derive the channel number") + /// If using the hash algorithm the channel number will be: hash(channel_name) % + /// NUM_CHANNELS (Where num channels depends on the regulatory region). + @$pb.TagNumber(11) + $core.int get channelNum => $_getIZ(10); + @$pb.TagNumber(11) + set channelNum($core.int value) => $_setUnsignedInt32(10, value); + @$pb.TagNumber(11) + $core.bool hasChannelNum() => $_has(10); + @$pb.TagNumber(11) + void clearChannelNum() => $_clearField(11); + + /// + /// If true, duty cycle limits will be exceeded and thus you're possibly not following + /// the local regulations if you're not a HAM. + /// Has no effect if the duty cycle of the used region is 100%. + @$pb.TagNumber(12) + $core.bool get overrideDutyCycle => $_getBF(11); + @$pb.TagNumber(12) + set overrideDutyCycle($core.bool value) => $_setBool(11, value); + @$pb.TagNumber(12) + $core.bool hasOverrideDutyCycle() => $_has(11); + @$pb.TagNumber(12) + void clearOverrideDutyCycle() => $_clearField(12); + + /// + /// If true, sets RX boosted gain mode on SX126X based radios + @$pb.TagNumber(13) + $core.bool get sx126xRxBoostedGain => $_getBF(12); + @$pb.TagNumber(13) + set sx126xRxBoostedGain($core.bool value) => $_setBool(12, value); + @$pb.TagNumber(13) + $core.bool hasSx126xRxBoostedGain() => $_has(12); + @$pb.TagNumber(13) + void clearSx126xRxBoostedGain() => $_clearField(13); + + /// + /// This parameter is for advanced users and licensed HAM radio operators. + /// Ignore Channel Calculation and use this frequency instead. The frequency_offset + /// will still be applied. This will allow you to use out-of-band frequencies. + /// Please respect your local laws and regulations. If you are a HAM, make sure you + /// enable HAM mode and turn off encryption. + @$pb.TagNumber(14) + $core.double get overrideFrequency => $_getN(13); + @$pb.TagNumber(14) + set overrideFrequency($core.double value) => $_setFloat(13, value); + @$pb.TagNumber(14) + $core.bool hasOverrideFrequency() => $_has(13); + @$pb.TagNumber(14) + void clearOverrideFrequency() => $_clearField(14); + + /// + /// If true, disable the build-in PA FAN using pin define in RF95_FAN_EN. + @$pb.TagNumber(15) + $core.bool get paFanDisabled => $_getBF(14); + @$pb.TagNumber(15) + set paFanDisabled($core.bool value) => $_setBool(14, value); + @$pb.TagNumber(15) + $core.bool hasPaFanDisabled() => $_has(14); + @$pb.TagNumber(15) + void clearPaFanDisabled() => $_clearField(15); + + /// + /// For testing it is useful sometimes to force a node to never listen to + /// particular other nodes (simulating radio out of range). All nodenums listed + /// in ignore_incoming will have packets they send dropped on receive (by router.cpp) + @$pb.TagNumber(103) + $pb.PbList<$core.int> get ignoreIncoming => $_getList(15); + + /// + /// If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it. + @$pb.TagNumber(104) + $core.bool get ignoreMqtt => $_getBF(16); + @$pb.TagNumber(104) + set ignoreMqtt($core.bool value) => $_setBool(16, value); + @$pb.TagNumber(104) + $core.bool hasIgnoreMqtt() => $_has(16); + @$pb.TagNumber(104) + void clearIgnoreMqtt() => $_clearField(104); + + /// + /// Sets the ok_to_mqtt bit on outgoing packets + @$pb.TagNumber(105) + $core.bool get configOkToMqtt => $_getBF(17); + @$pb.TagNumber(105) + set configOkToMqtt($core.bool value) => $_setBool(17, value); + @$pb.TagNumber(105) + $core.bool hasConfigOkToMqtt() => $_has(17); + @$pb.TagNumber(105) + void clearConfigOkToMqtt() => $_clearField(105); +} + +class Config_BluetoothConfig extends $pb.GeneratedMessage { + factory Config_BluetoothConfig({ + $core.bool? enabled, + Config_BluetoothConfig_PairingMode? mode, + $core.int? fixedPin, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (mode != null) result.mode = mode; + if (fixedPin != null) result.fixedPin = fixedPin; + return result; + } + + Config_BluetoothConfig._(); + + factory Config_BluetoothConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_BluetoothConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.BluetoothConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..e( + 2, _omitFieldNames ? '' : 'mode', $pb.PbFieldType.OE, + defaultOrMaker: Config_BluetoothConfig_PairingMode.RANDOM_PIN, + valueOf: Config_BluetoothConfig_PairingMode.valueOf, + enumValues: Config_BluetoothConfig_PairingMode.values) + ..a<$core.int>(3, _omitFieldNames ? '' : 'fixedPin', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_BluetoothConfig clone() => + Config_BluetoothConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_BluetoothConfig copyWith( + void Function(Config_BluetoothConfig) updates) => + super.copyWith((message) => updates(message as Config_BluetoothConfig)) + as Config_BluetoothConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_BluetoothConfig create() => Config_BluetoothConfig._(); + @$core.override + Config_BluetoothConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_BluetoothConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_BluetoothConfig? _defaultInstance; + + /// + /// Enable Bluetooth on the device + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// Determines the pairing strategy for the device + @$pb.TagNumber(2) + Config_BluetoothConfig_PairingMode get mode => $_getN(1); + @$pb.TagNumber(2) + set mode(Config_BluetoothConfig_PairingMode value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasMode() => $_has(1); + @$pb.TagNumber(2) + void clearMode() => $_clearField(2); + + /// + /// Specified PIN for PairingMode.FixedPin + @$pb.TagNumber(3) + $core.int get fixedPin => $_getIZ(2); + @$pb.TagNumber(3) + set fixedPin($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasFixedPin() => $_has(2); + @$pb.TagNumber(3) + void clearFixedPin() => $_clearField(3); +} + +class Config_SecurityConfig extends $pb.GeneratedMessage { + factory Config_SecurityConfig({ + $core.List<$core.int>? publicKey, + $core.List<$core.int>? privateKey, + $core.Iterable<$core.List<$core.int>>? adminKey, + $core.bool? isManaged, + $core.bool? serialEnabled, + $core.bool? debugLogApiEnabled, + $core.bool? adminChannelEnabled, + }) { + final result = create(); + if (publicKey != null) result.publicKey = publicKey; + if (privateKey != null) result.privateKey = privateKey; + if (adminKey != null) result.adminKey.addAll(adminKey); + if (isManaged != null) result.isManaged = isManaged; + if (serialEnabled != null) result.serialEnabled = serialEnabled; + if (debugLogApiEnabled != null) + result.debugLogApiEnabled = debugLogApiEnabled; + if (adminChannelEnabled != null) + result.adminChannelEnabled = adminChannelEnabled; + return result; + } + + Config_SecurityConfig._(); + + factory Config_SecurityConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_SecurityConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.SecurityConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.List<$core.int>>( + 1, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'privateKey', $pb.PbFieldType.OY) + ..p<$core.List<$core.int>>( + 3, _omitFieldNames ? '' : 'adminKey', $pb.PbFieldType.PY) + ..aOB(4, _omitFieldNames ? '' : 'isManaged') + ..aOB(5, _omitFieldNames ? '' : 'serialEnabled') + ..aOB(6, _omitFieldNames ? '' : 'debugLogApiEnabled') + ..aOB(8, _omitFieldNames ? '' : 'adminChannelEnabled') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_SecurityConfig clone() => + Config_SecurityConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_SecurityConfig copyWith( + void Function(Config_SecurityConfig) updates) => + super.copyWith((message) => updates(message as Config_SecurityConfig)) + as Config_SecurityConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_SecurityConfig create() => Config_SecurityConfig._(); + @$core.override + Config_SecurityConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_SecurityConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_SecurityConfig? _defaultInstance; + + /// + /// The public key of the user's device. + /// Sent out to other nodes on the mesh to allow them to compute a shared secret key. + @$pb.TagNumber(1) + $core.List<$core.int> get publicKey => $_getN(0); + @$pb.TagNumber(1) + set publicKey($core.List<$core.int> value) => $_setBytes(0, value); + @$pb.TagNumber(1) + $core.bool hasPublicKey() => $_has(0); + @$pb.TagNumber(1) + void clearPublicKey() => $_clearField(1); + + /// + /// The private key of the device. + /// Used to create a shared key with a remote device. + @$pb.TagNumber(2) + $core.List<$core.int> get privateKey => $_getN(1); + @$pb.TagNumber(2) + set privateKey($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasPrivateKey() => $_has(1); + @$pb.TagNumber(2) + void clearPrivateKey() => $_clearField(2); + + /// + /// The public key authorized to send admin messages to this node. + @$pb.TagNumber(3) + $pb.PbList<$core.List<$core.int>> get adminKey => $_getList(2); + + /// + /// If true, device is considered to be "managed" by a mesh administrator via admin messages + /// Device is managed by a mesh administrator. + @$pb.TagNumber(4) + $core.bool get isManaged => $_getBF(3); + @$pb.TagNumber(4) + set isManaged($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasIsManaged() => $_has(3); + @$pb.TagNumber(4) + void clearIsManaged() => $_clearField(4); + + /// + /// Serial Console over the Stream API." + @$pb.TagNumber(5) + $core.bool get serialEnabled => $_getBF(4); + @$pb.TagNumber(5) + set serialEnabled($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasSerialEnabled() => $_has(4); + @$pb.TagNumber(5) + void clearSerialEnabled() => $_clearField(5); + + /// + /// By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). + /// Output live debug logging over serial or bluetooth is set to true. + @$pb.TagNumber(6) + $core.bool get debugLogApiEnabled => $_getBF(5); + @$pb.TagNumber(6) + set debugLogApiEnabled($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasDebugLogApiEnabled() => $_has(5); + @$pb.TagNumber(6) + void clearDebugLogApiEnabled() => $_clearField(6); + + /// + /// Allow incoming device control over the insecure legacy admin channel. + @$pb.TagNumber(8) + $core.bool get adminChannelEnabled => $_getBF(6); + @$pb.TagNumber(8) + set adminChannelEnabled($core.bool value) => $_setBool(6, value); + @$pb.TagNumber(8) + $core.bool hasAdminChannelEnabled() => $_has(6); + @$pb.TagNumber(8) + void clearAdminChannelEnabled() => $_clearField(8); +} + +/// +/// Blank config request, strictly for getting the session key +class Config_SessionkeyConfig extends $pb.GeneratedMessage { + factory Config_SessionkeyConfig() => create(); + + Config_SessionkeyConfig._(); + + factory Config_SessionkeyConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config_SessionkeyConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config.SessionkeyConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_SessionkeyConfig clone() => + Config_SessionkeyConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config_SessionkeyConfig copyWith( + void Function(Config_SessionkeyConfig) updates) => + super.copyWith((message) => updates(message as Config_SessionkeyConfig)) + as Config_SessionkeyConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config_SessionkeyConfig create() => Config_SessionkeyConfig._(); + @$core.override + Config_SessionkeyConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config_SessionkeyConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Config_SessionkeyConfig? _defaultInstance; +} + +enum Config_PayloadVariant { + device, + position, + power, + network, + display, + lora, + bluetooth, + security, + sessionkey, + deviceUi, + notSet +} + +class Config extends $pb.GeneratedMessage { + factory Config({ + Config_DeviceConfig? device, + Config_PositionConfig? position, + Config_PowerConfig? power, + Config_NetworkConfig? network, + Config_DisplayConfig? display, + Config_LoRaConfig? lora, + Config_BluetoothConfig? bluetooth, + Config_SecurityConfig? security, + Config_SessionkeyConfig? sessionkey, + $0.DeviceUIConfig? deviceUi, + }) { + final result = create(); + if (device != null) result.device = device; + if (position != null) result.position = position; + if (power != null) result.power = power; + if (network != null) result.network = network; + if (display != null) result.display = display; + if (lora != null) result.lora = lora; + if (bluetooth != null) result.bluetooth = bluetooth; + if (security != null) result.security = security; + if (sessionkey != null) result.sessionkey = sessionkey; + if (deviceUi != null) result.deviceUi = deviceUi; + return result; + } + + Config._(); + + factory Config.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Config.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, Config_PayloadVariant> + _Config_PayloadVariantByTag = { + 1: Config_PayloadVariant.device, + 2: Config_PayloadVariant.position, + 3: Config_PayloadVariant.power, + 4: Config_PayloadVariant.network, + 5: Config_PayloadVariant.display, + 6: Config_PayloadVariant.lora, + 7: Config_PayloadVariant.bluetooth, + 8: Config_PayloadVariant.security, + 9: Config_PayloadVariant.sessionkey, + 10: Config_PayloadVariant.deviceUi, + 0: Config_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Config', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ..aOM(1, _omitFieldNames ? '' : 'device', + subBuilder: Config_DeviceConfig.create) + ..aOM(2, _omitFieldNames ? '' : 'position', + subBuilder: Config_PositionConfig.create) + ..aOM(3, _omitFieldNames ? '' : 'power', + subBuilder: Config_PowerConfig.create) + ..aOM(4, _omitFieldNames ? '' : 'network', + subBuilder: Config_NetworkConfig.create) + ..aOM(5, _omitFieldNames ? '' : 'display', + subBuilder: Config_DisplayConfig.create) + ..aOM(6, _omitFieldNames ? '' : 'lora', + subBuilder: Config_LoRaConfig.create) + ..aOM(7, _omitFieldNames ? '' : 'bluetooth', + subBuilder: Config_BluetoothConfig.create) + ..aOM(8, _omitFieldNames ? '' : 'security', + subBuilder: Config_SecurityConfig.create) + ..aOM(9, _omitFieldNames ? '' : 'sessionkey', + subBuilder: Config_SessionkeyConfig.create) + ..aOM<$0.DeviceUIConfig>(10, _omitFieldNames ? '' : 'deviceUi', + subBuilder: $0.DeviceUIConfig.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config clone() => Config()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Config copyWith(void Function(Config) updates) => + super.copyWith((message) => updates(message as Config)) as Config; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Config create() => Config._(); + @$core.override + Config createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Config getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Config? _defaultInstance; + + Config_PayloadVariant whichPayloadVariant() => + _Config_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + Config_DeviceConfig get device => $_getN(0); + @$pb.TagNumber(1) + set device(Config_DeviceConfig value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasDevice() => $_has(0); + @$pb.TagNumber(1) + void clearDevice() => $_clearField(1); + @$pb.TagNumber(1) + Config_DeviceConfig ensureDevice() => $_ensure(0); + + @$pb.TagNumber(2) + Config_PositionConfig get position => $_getN(1); + @$pb.TagNumber(2) + set position(Config_PositionConfig value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasPosition() => $_has(1); + @$pb.TagNumber(2) + void clearPosition() => $_clearField(2); + @$pb.TagNumber(2) + Config_PositionConfig ensurePosition() => $_ensure(1); + + @$pb.TagNumber(3) + Config_PowerConfig get power => $_getN(2); + @$pb.TagNumber(3) + set power(Config_PowerConfig value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasPower() => $_has(2); + @$pb.TagNumber(3) + void clearPower() => $_clearField(3); + @$pb.TagNumber(3) + Config_PowerConfig ensurePower() => $_ensure(2); + + @$pb.TagNumber(4) + Config_NetworkConfig get network => $_getN(3); + @$pb.TagNumber(4) + set network(Config_NetworkConfig value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasNetwork() => $_has(3); + @$pb.TagNumber(4) + void clearNetwork() => $_clearField(4); + @$pb.TagNumber(4) + Config_NetworkConfig ensureNetwork() => $_ensure(3); + + @$pb.TagNumber(5) + Config_DisplayConfig get display => $_getN(4); + @$pb.TagNumber(5) + set display(Config_DisplayConfig value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasDisplay() => $_has(4); + @$pb.TagNumber(5) + void clearDisplay() => $_clearField(5); + @$pb.TagNumber(5) + Config_DisplayConfig ensureDisplay() => $_ensure(4); + + @$pb.TagNumber(6) + Config_LoRaConfig get lora => $_getN(5); + @$pb.TagNumber(6) + set lora(Config_LoRaConfig value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasLora() => $_has(5); + @$pb.TagNumber(6) + void clearLora() => $_clearField(6); + @$pb.TagNumber(6) + Config_LoRaConfig ensureLora() => $_ensure(5); + + @$pb.TagNumber(7) + Config_BluetoothConfig get bluetooth => $_getN(6); + @$pb.TagNumber(7) + set bluetooth(Config_BluetoothConfig value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasBluetooth() => $_has(6); + @$pb.TagNumber(7) + void clearBluetooth() => $_clearField(7); + @$pb.TagNumber(7) + Config_BluetoothConfig ensureBluetooth() => $_ensure(6); + + @$pb.TagNumber(8) + Config_SecurityConfig get security => $_getN(7); + @$pb.TagNumber(8) + set security(Config_SecurityConfig value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasSecurity() => $_has(7); + @$pb.TagNumber(8) + void clearSecurity() => $_clearField(8); + @$pb.TagNumber(8) + Config_SecurityConfig ensureSecurity() => $_ensure(7); + + @$pb.TagNumber(9) + Config_SessionkeyConfig get sessionkey => $_getN(8); + @$pb.TagNumber(9) + set sessionkey(Config_SessionkeyConfig value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasSessionkey() => $_has(8); + @$pb.TagNumber(9) + void clearSessionkey() => $_clearField(9); + @$pb.TagNumber(9) + Config_SessionkeyConfig ensureSessionkey() => $_ensure(8); + + @$pb.TagNumber(10) + $0.DeviceUIConfig get deviceUi => $_getN(9); + @$pb.TagNumber(10) + set deviceUi($0.DeviceUIConfig value) => $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasDeviceUi() => $_has(9); + @$pb.TagNumber(10) + void clearDeviceUi() => $_clearField(10); + @$pb.TagNumber(10) + $0.DeviceUIConfig ensureDeviceUi() => $_ensure(9); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/config.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/config.pbenum.dart new file mode 100644 index 000000000..3e46a2da5 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/config.pbenum.dart @@ -0,0 +1,953 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/config.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// Defines the device's role on the Mesh network +class Config_DeviceConfig_Role extends $pb.ProtobufEnum { + /// + /// Description: App connected or stand alone messaging device. + /// Technical Details: Default Role + static const Config_DeviceConfig_Role CLIENT = + Config_DeviceConfig_Role._(0, _omitEnumNames ? '' : 'CLIENT'); + + /// + /// Description: Device that does not forward packets from other devices. + static const Config_DeviceConfig_Role CLIENT_MUTE = + Config_DeviceConfig_Role._(1, _omitEnumNames ? '' : 'CLIENT_MUTE'); + + /// + /// Description: Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list. + /// Technical Details: Mesh packets will prefer to be routed over this node. This node will not be used by client apps. + /// The wifi radio and the oled screen will be put to sleep. + /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. + static const Config_DeviceConfig_Role ROUTER = + Config_DeviceConfig_Role._(2, _omitEnumNames ? '' : 'ROUTER'); + @$core.Deprecated('This enum value is deprecated') + static const Config_DeviceConfig_Role ROUTER_CLIENT = + Config_DeviceConfig_Role._(3, _omitEnumNames ? '' : 'ROUTER_CLIENT'); + + /// + /// Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list. + /// Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry + /// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. + static const Config_DeviceConfig_Role REPEATER = + Config_DeviceConfig_Role._(4, _omitEnumNames ? '' : 'REPEATER'); + + /// + /// Description: Broadcasts GPS position packets as priority. + /// Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default. + /// When used in conjunction with power.is_power_saving = true, nodes will wake up, + /// send position, and then sleep for position.position_broadcast_secs seconds. + static const Config_DeviceConfig_Role TRACKER = + Config_DeviceConfig_Role._(5, _omitEnumNames ? '' : 'TRACKER'); + + /// + /// Description: Broadcasts telemetry packets as priority. + /// Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default. + /// When used in conjunction with power.is_power_saving = true, nodes will wake up, + /// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds. + static const Config_DeviceConfig_Role SENSOR = + Config_DeviceConfig_Role._(6, _omitEnumNames ? '' : 'SENSOR'); + + /// + /// Description: Optimized for ATAK system communication and reduces routine broadcasts. + /// Technical Details: Used for nodes dedicated for connection to an ATAK EUD. + /// Turns off many of the routine broadcasts to favor CoT packet stream + /// from the Meshtastic ATAK plugin -> IMeshService -> Node + static const Config_DeviceConfig_Role TAK = + Config_DeviceConfig_Role._(7, _omitEnumNames ? '' : 'TAK'); + + /// + /// Description: Device that only broadcasts as needed for stealth or power savings. + /// Technical Details: Used for nodes that "only speak when spoken to" + /// Turns all of the routine broadcasts but allows for ad-hoc communication + /// Still rebroadcasts, but with local only rebroadcast mode (known meshes only) + /// Can be used for clandestine operation or to dramatically reduce airtime / power consumption + static const Config_DeviceConfig_Role CLIENT_HIDDEN = + Config_DeviceConfig_Role._(8, _omitEnumNames ? '' : 'CLIENT_HIDDEN'); + + /// + /// Description: Broadcasts location as message to default channel regularly for to assist with device recovery. + /// Technical Details: Used to automatically send a text message to the mesh + /// with the current position of the device on a frequent interval: + /// "I'm lost! Position: lat / long" + static const Config_DeviceConfig_Role LOST_AND_FOUND = + Config_DeviceConfig_Role._(9, _omitEnumNames ? '' : 'LOST_AND_FOUND'); + + /// + /// Description: Enables automatic TAK PLI broadcasts and reduces routine broadcasts. + /// Technical Details: Turns off many of the routine broadcasts to favor ATAK CoT packet stream + /// and automatic TAK PLI (position location information) broadcasts. + /// Uses position module configuration to determine TAK PLI broadcast interval. + static const Config_DeviceConfig_Role TAK_TRACKER = + Config_DeviceConfig_Role._(10, _omitEnumNames ? '' : 'TAK_TRACKER'); + + /// + /// Description: Will always rebroadcast packets, but will do so after all other modes. + /// Technical Details: Used for router nodes that are intended to provide additional coverage + /// in areas not already covered by other routers, or to bridge around problematic terrain, + /// but should not be given priority over other routers in order to avoid unnecessaraily + /// consuming hops. + static const Config_DeviceConfig_Role ROUTER_LATE = + Config_DeviceConfig_Role._(11, _omitEnumNames ? '' : 'ROUTER_LATE'); + + static const $core.List values = + [ + CLIENT, + CLIENT_MUTE, + ROUTER, + ROUTER_CLIENT, + REPEATER, + TRACKER, + SENSOR, + TAK, + CLIENT_HIDDEN, + LOST_AND_FOUND, + TAK_TRACKER, + ROUTER_LATE, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 11); + static Config_DeviceConfig_Role? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DeviceConfig_Role._(super.value, super.name); +} + +/// +/// Defines the device's behavior for how messages are rebroadcast +class Config_DeviceConfig_RebroadcastMode extends $pb.ProtobufEnum { + /// + /// Default behavior. + /// Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params. + static const Config_DeviceConfig_RebroadcastMode ALL = + Config_DeviceConfig_RebroadcastMode._(0, _omitEnumNames ? '' : 'ALL'); + + /// + /// Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. + /// Only available in Repeater role. Setting this on any other roles will result in ALL behavior. + static const Config_DeviceConfig_RebroadcastMode ALL_SKIP_DECODING = + Config_DeviceConfig_RebroadcastMode._( + 1, _omitEnumNames ? '' : 'ALL_SKIP_DECODING'); + + /// + /// Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. + /// Only rebroadcasts message on the nodes local primary / secondary channels. + static const Config_DeviceConfig_RebroadcastMode LOCAL_ONLY = + Config_DeviceConfig_RebroadcastMode._( + 2, _omitEnumNames ? '' : 'LOCAL_ONLY'); + + /// + /// Ignores observed messages from foreign meshes like LOCAL_ONLY, + /// but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB) + static const Config_DeviceConfig_RebroadcastMode KNOWN_ONLY = + Config_DeviceConfig_RebroadcastMode._( + 3, _omitEnumNames ? '' : 'KNOWN_ONLY'); + + /// + /// Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. + static const Config_DeviceConfig_RebroadcastMode NONE = + Config_DeviceConfig_RebroadcastMode._(4, _omitEnumNames ? '' : 'NONE'); + + /// + /// Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. + /// Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. + static const Config_DeviceConfig_RebroadcastMode CORE_PORTNUMS_ONLY = + Config_DeviceConfig_RebroadcastMode._( + 5, _omitEnumNames ? '' : 'CORE_PORTNUMS_ONLY'); + + static const $core.List values = + [ + ALL, + ALL_SKIP_DECODING, + LOCAL_ONLY, + KNOWN_ONLY, + NONE, + CORE_PORTNUMS_ONLY, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 5); + static Config_DeviceConfig_RebroadcastMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DeviceConfig_RebroadcastMode._(super.value, super.name); +} + +/// +/// Defines buzzer behavior for audio feedback +class Config_DeviceConfig_BuzzerMode extends $pb.ProtobufEnum { + /// + /// Default behavior. + /// Buzzer is enabled for all audio feedback including button presses and alerts. + static const Config_DeviceConfig_BuzzerMode ALL_ENABLED = + Config_DeviceConfig_BuzzerMode._(0, _omitEnumNames ? '' : 'ALL_ENABLED'); + + /// + /// Disabled. + /// All buzzer audio feedback is disabled. + static const Config_DeviceConfig_BuzzerMode DISABLED = + Config_DeviceConfig_BuzzerMode._(1, _omitEnumNames ? '' : 'DISABLED'); + + /// + /// Notifications Only. + /// Buzzer is enabled only for notifications and alerts, but not for button presses. + /// External notification config determines the specifics of the notification behavior. + static const Config_DeviceConfig_BuzzerMode NOTIFICATIONS_ONLY = + Config_DeviceConfig_BuzzerMode._( + 2, _omitEnumNames ? '' : 'NOTIFICATIONS_ONLY'); + + /// + /// Non-notification system buzzer tones only. + /// Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. + static const Config_DeviceConfig_BuzzerMode SYSTEM_ONLY = + Config_DeviceConfig_BuzzerMode._(3, _omitEnumNames ? '' : 'SYSTEM_ONLY'); + + /// + /// Direct Message notifications only. + /// Buzzer is enabled only for direct messages and alerts, but not for button presses. + /// External notification config determines the specifics of the notification behavior. + static const Config_DeviceConfig_BuzzerMode DIRECT_MSG_ONLY = + Config_DeviceConfig_BuzzerMode._( + 4, _omitEnumNames ? '' : 'DIRECT_MSG_ONLY'); + + static const $core.List values = + [ + ALL_ENABLED, + DISABLED, + NOTIFICATIONS_ONLY, + SYSTEM_ONLY, + DIRECT_MSG_ONLY, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 4); + static Config_DeviceConfig_BuzzerMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DeviceConfig_BuzzerMode._(super.value, super.name); +} + +/// +/// Bit field of boolean configuration options, indicating which optional +/// fields to include when assembling POSITION messages. +/// Longitude, latitude, altitude, speed, heading, and DOP +/// are always included (also time if GPS-synced) +/// NOTE: the more fields are included, the larger the message will be - +/// leading to longer airtime and a higher risk of packet loss +class Config_PositionConfig_PositionFlags extends $pb.ProtobufEnum { + /// + /// Required for compilation + static const Config_PositionConfig_PositionFlags UNSET = + Config_PositionConfig_PositionFlags._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// Include an altitude value (if available) + static const Config_PositionConfig_PositionFlags ALTITUDE = + Config_PositionConfig_PositionFlags._( + 1, _omitEnumNames ? '' : 'ALTITUDE'); + + /// + /// Altitude value is MSL + static const Config_PositionConfig_PositionFlags ALTITUDE_MSL = + Config_PositionConfig_PositionFlags._( + 2, _omitEnumNames ? '' : 'ALTITUDE_MSL'); + + /// + /// Include geoidal separation + static const Config_PositionConfig_PositionFlags GEOIDAL_SEPARATION = + Config_PositionConfig_PositionFlags._( + 4, _omitEnumNames ? '' : 'GEOIDAL_SEPARATION'); + + /// + /// Include the DOP value ; PDOP used by default, see below + static const Config_PositionConfig_PositionFlags DOP = + Config_PositionConfig_PositionFlags._(8, _omitEnumNames ? '' : 'DOP'); + + /// + /// If POS_DOP set, send separate HDOP / VDOP values instead of PDOP + static const Config_PositionConfig_PositionFlags HVDOP = + Config_PositionConfig_PositionFlags._(16, _omitEnumNames ? '' : 'HVDOP'); + + /// + /// Include number of "satellites in view" + static const Config_PositionConfig_PositionFlags SATINVIEW = + Config_PositionConfig_PositionFlags._( + 32, _omitEnumNames ? '' : 'SATINVIEW'); + + /// + /// Include a sequence number incremented per packet + static const Config_PositionConfig_PositionFlags SEQ_NO = + Config_PositionConfig_PositionFlags._(64, _omitEnumNames ? '' : 'SEQ_NO'); + + /// + /// Include positional timestamp (from GPS solution) + static const Config_PositionConfig_PositionFlags TIMESTAMP = + Config_PositionConfig_PositionFlags._( + 128, _omitEnumNames ? '' : 'TIMESTAMP'); + + /// + /// Include positional heading + /// Intended for use with vehicle not walking speeds + /// walking speeds are likely to be error prone like the compass + static const Config_PositionConfig_PositionFlags HEADING = + Config_PositionConfig_PositionFlags._( + 256, _omitEnumNames ? '' : 'HEADING'); + + /// + /// Include positional speed + /// Intended for use with vehicle not walking speeds + /// walking speeds are likely to be error prone like the compass + static const Config_PositionConfig_PositionFlags SPEED = + Config_PositionConfig_PositionFlags._(512, _omitEnumNames ? '' : 'SPEED'); + + static const $core.List values = + [ + UNSET, + ALTITUDE, + ALTITUDE_MSL, + GEOIDAL_SEPARATION, + DOP, + HVDOP, + SATINVIEW, + SEQ_NO, + TIMESTAMP, + HEADING, + SPEED, + ]; + + static final $core.Map<$core.int, Config_PositionConfig_PositionFlags> + _byValue = $pb.ProtobufEnum.initByValue(values); + static Config_PositionConfig_PositionFlags? valueOf($core.int value) => + _byValue[value]; + + const Config_PositionConfig_PositionFlags._(super.value, super.name); +} + +class Config_PositionConfig_GpsMode extends $pb.ProtobufEnum { + /// + /// GPS is present but disabled + static const Config_PositionConfig_GpsMode DISABLED = + Config_PositionConfig_GpsMode._(0, _omitEnumNames ? '' : 'DISABLED'); + + /// + /// GPS is present and enabled + static const Config_PositionConfig_GpsMode ENABLED = + Config_PositionConfig_GpsMode._(1, _omitEnumNames ? '' : 'ENABLED'); + + /// + /// GPS is not present on the device + static const Config_PositionConfig_GpsMode NOT_PRESENT = + Config_PositionConfig_GpsMode._(2, _omitEnumNames ? '' : 'NOT_PRESENT'); + + static const $core.List values = + [ + DISABLED, + ENABLED, + NOT_PRESENT, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static Config_PositionConfig_GpsMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_PositionConfig_GpsMode._(super.value, super.name); +} + +class Config_NetworkConfig_AddressMode extends $pb.ProtobufEnum { + /// + /// obtain ip address via DHCP + static const Config_NetworkConfig_AddressMode DHCP = + Config_NetworkConfig_AddressMode._(0, _omitEnumNames ? '' : 'DHCP'); + + /// + /// use static ip address + static const Config_NetworkConfig_AddressMode STATIC = + Config_NetworkConfig_AddressMode._(1, _omitEnumNames ? '' : 'STATIC'); + + static const $core.List values = + [ + DHCP, + STATIC, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 1); + static Config_NetworkConfig_AddressMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_NetworkConfig_AddressMode._(super.value, super.name); +} + +/// +/// Available flags auxiliary network protocols +class Config_NetworkConfig_ProtocolFlags extends $pb.ProtobufEnum { + /// + /// Do not broadcast packets over any network protocol + static const Config_NetworkConfig_ProtocolFlags NO_BROADCAST = + Config_NetworkConfig_ProtocolFlags._( + 0, _omitEnumNames ? '' : 'NO_BROADCAST'); + + /// + /// Enable broadcasting packets via UDP over the local network + static const Config_NetworkConfig_ProtocolFlags UDP_BROADCAST = + Config_NetworkConfig_ProtocolFlags._( + 1, _omitEnumNames ? '' : 'UDP_BROADCAST'); + + static const $core.List values = + [ + NO_BROADCAST, + UDP_BROADCAST, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 1); + static Config_NetworkConfig_ProtocolFlags? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_NetworkConfig_ProtocolFlags._(super.value, super.name); +} + +/// +/// How the GPS coordinates are displayed on the OLED screen. +class Config_DisplayConfig_GpsCoordinateFormat extends $pb.ProtobufEnum { + /// + /// GPS coordinates are displayed in the normal decimal degrees format: + /// DD.DDDDDD DDD.DDDDDD + static const Config_DisplayConfig_GpsCoordinateFormat DEC = + Config_DisplayConfig_GpsCoordinateFormat._( + 0, _omitEnumNames ? '' : 'DEC'); + + /// + /// GPS coordinates are displayed in the degrees minutes seconds format: + /// DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant + static const Config_DisplayConfig_GpsCoordinateFormat DMS = + Config_DisplayConfig_GpsCoordinateFormat._( + 1, _omitEnumNames ? '' : 'DMS'); + + /// + /// Universal Transverse Mercator format: + /// ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing + static const Config_DisplayConfig_GpsCoordinateFormat UTM = + Config_DisplayConfig_GpsCoordinateFormat._( + 2, _omitEnumNames ? '' : 'UTM'); + + /// + /// Military Grid Reference System format: + /// ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square, + /// E is easting, N is northing + static const Config_DisplayConfig_GpsCoordinateFormat MGRS = + Config_DisplayConfig_GpsCoordinateFormat._( + 3, _omitEnumNames ? '' : 'MGRS'); + + /// + /// Open Location Code (aka Plus Codes). + static const Config_DisplayConfig_GpsCoordinateFormat OLC = + Config_DisplayConfig_GpsCoordinateFormat._( + 4, _omitEnumNames ? '' : 'OLC'); + + /// + /// Ordnance Survey Grid Reference (the National Grid System of the UK). + /// Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square, + /// E is the easting, N is the northing + static const Config_DisplayConfig_GpsCoordinateFormat OSGR = + Config_DisplayConfig_GpsCoordinateFormat._( + 5, _omitEnumNames ? '' : 'OSGR'); + + static const $core.List values = + [ + DEC, + DMS, + UTM, + MGRS, + OLC, + OSGR, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 5); + static Config_DisplayConfig_GpsCoordinateFormat? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DisplayConfig_GpsCoordinateFormat._(super.value, super.name); +} + +/// +/// Unit display preference +class Config_DisplayConfig_DisplayUnits extends $pb.ProtobufEnum { + /// + /// Metric (Default) + static const Config_DisplayConfig_DisplayUnits METRIC = + Config_DisplayConfig_DisplayUnits._(0, _omitEnumNames ? '' : 'METRIC'); + + /// + /// Imperial + static const Config_DisplayConfig_DisplayUnits IMPERIAL = + Config_DisplayConfig_DisplayUnits._(1, _omitEnumNames ? '' : 'IMPERIAL'); + + static const $core.List values = + [ + METRIC, + IMPERIAL, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 1); + static Config_DisplayConfig_DisplayUnits? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DisplayConfig_DisplayUnits._(super.value, super.name); +} + +/// +/// Override OLED outo detect with this if it fails. +class Config_DisplayConfig_OledType extends $pb.ProtobufEnum { + /// + /// Default / Autodetect + static const Config_DisplayConfig_OledType OLED_AUTO = + Config_DisplayConfig_OledType._(0, _omitEnumNames ? '' : 'OLED_AUTO'); + + /// + /// Default / Autodetect + static const Config_DisplayConfig_OledType OLED_SSD1306 = + Config_DisplayConfig_OledType._(1, _omitEnumNames ? '' : 'OLED_SSD1306'); + + /// + /// Default / Autodetect + static const Config_DisplayConfig_OledType OLED_SH1106 = + Config_DisplayConfig_OledType._(2, _omitEnumNames ? '' : 'OLED_SH1106'); + + /// + /// Can not be auto detected but set by proto. Used for 128x128 screens + static const Config_DisplayConfig_OledType OLED_SH1107 = + Config_DisplayConfig_OledType._(3, _omitEnumNames ? '' : 'OLED_SH1107'); + + /// + /// Can not be auto detected but set by proto. Used for 128x64 screens + static const Config_DisplayConfig_OledType OLED_SH1107_128_64 = + Config_DisplayConfig_OledType._( + 4, _omitEnumNames ? '' : 'OLED_SH1107_128_64'); + + static const $core.List values = + [ + OLED_AUTO, + OLED_SSD1306, + OLED_SH1106, + OLED_SH1107, + OLED_SH1107_128_64, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 4); + static Config_DisplayConfig_OledType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DisplayConfig_OledType._(super.value, super.name); +} + +class Config_DisplayConfig_DisplayMode extends $pb.ProtobufEnum { + /// + /// Default. The old style for the 128x64 OLED screen + static const Config_DisplayConfig_DisplayMode DEFAULT = + Config_DisplayConfig_DisplayMode._(0, _omitEnumNames ? '' : 'DEFAULT'); + + /// + /// Rearrange display elements to cater for bicolor OLED displays + static const Config_DisplayConfig_DisplayMode TWOCOLOR = + Config_DisplayConfig_DisplayMode._(1, _omitEnumNames ? '' : 'TWOCOLOR'); + + /// + /// Same as TwoColor, but with inverted top bar. Not so good for Epaper displays + static const Config_DisplayConfig_DisplayMode INVERTED = + Config_DisplayConfig_DisplayMode._(2, _omitEnumNames ? '' : 'INVERTED'); + + /// + /// TFT Full Color Displays (not implemented yet) + static const Config_DisplayConfig_DisplayMode COLOR = + Config_DisplayConfig_DisplayMode._(3, _omitEnumNames ? '' : 'COLOR'); + + static const $core.List values = + [ + DEFAULT, + TWOCOLOR, + INVERTED, + COLOR, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 3); + static Config_DisplayConfig_DisplayMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DisplayConfig_DisplayMode._(super.value, super.name); +} + +class Config_DisplayConfig_CompassOrientation extends $pb.ProtobufEnum { + /// + /// The compass and the display are in the same orientation. + static const Config_DisplayConfig_CompassOrientation DEGREES_0 = + Config_DisplayConfig_CompassOrientation._( + 0, _omitEnumNames ? '' : 'DEGREES_0'); + + /// + /// Rotate the compass by 90 degrees. + static const Config_DisplayConfig_CompassOrientation DEGREES_90 = + Config_DisplayConfig_CompassOrientation._( + 1, _omitEnumNames ? '' : 'DEGREES_90'); + + /// + /// Rotate the compass by 180 degrees. + static const Config_DisplayConfig_CompassOrientation DEGREES_180 = + Config_DisplayConfig_CompassOrientation._( + 2, _omitEnumNames ? '' : 'DEGREES_180'); + + /// + /// Rotate the compass by 270 degrees. + static const Config_DisplayConfig_CompassOrientation DEGREES_270 = + Config_DisplayConfig_CompassOrientation._( + 3, _omitEnumNames ? '' : 'DEGREES_270'); + + /// + /// Don't rotate the compass, but invert the result. + static const Config_DisplayConfig_CompassOrientation DEGREES_0_INVERTED = + Config_DisplayConfig_CompassOrientation._( + 4, _omitEnumNames ? '' : 'DEGREES_0_INVERTED'); + + /// + /// Rotate the compass by 90 degrees and invert. + static const Config_DisplayConfig_CompassOrientation DEGREES_90_INVERTED = + Config_DisplayConfig_CompassOrientation._( + 5, _omitEnumNames ? '' : 'DEGREES_90_INVERTED'); + + /// + /// Rotate the compass by 180 degrees and invert. + static const Config_DisplayConfig_CompassOrientation DEGREES_180_INVERTED = + Config_DisplayConfig_CompassOrientation._( + 6, _omitEnumNames ? '' : 'DEGREES_180_INVERTED'); + + /// + /// Rotate the compass by 270 degrees and invert. + static const Config_DisplayConfig_CompassOrientation DEGREES_270_INVERTED = + Config_DisplayConfig_CompassOrientation._( + 7, _omitEnumNames ? '' : 'DEGREES_270_INVERTED'); + + static const $core.List values = + [ + DEGREES_0, + DEGREES_90, + DEGREES_180, + DEGREES_270, + DEGREES_0_INVERTED, + DEGREES_90_INVERTED, + DEGREES_180_INVERTED, + DEGREES_270_INVERTED, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 7); + static Config_DisplayConfig_CompassOrientation? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_DisplayConfig_CompassOrientation._(super.value, super.name); +} + +class Config_LoRaConfig_RegionCode extends $pb.ProtobufEnum { + /// + /// Region is not set + static const Config_LoRaConfig_RegionCode UNSET = + Config_LoRaConfig_RegionCode._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// United States + static const Config_LoRaConfig_RegionCode US = + Config_LoRaConfig_RegionCode._(1, _omitEnumNames ? '' : 'US'); + + /// + /// European Union 433mhz + static const Config_LoRaConfig_RegionCode EU_433 = + Config_LoRaConfig_RegionCode._(2, _omitEnumNames ? '' : 'EU_433'); + + /// + /// European Union 868mhz + static const Config_LoRaConfig_RegionCode EU_868 = + Config_LoRaConfig_RegionCode._(3, _omitEnumNames ? '' : 'EU_868'); + + /// + /// China + static const Config_LoRaConfig_RegionCode CN = + Config_LoRaConfig_RegionCode._(4, _omitEnumNames ? '' : 'CN'); + + /// + /// Japan + static const Config_LoRaConfig_RegionCode JP = + Config_LoRaConfig_RegionCode._(5, _omitEnumNames ? '' : 'JP'); + + /// + /// Australia / New Zealand + static const Config_LoRaConfig_RegionCode ANZ = + Config_LoRaConfig_RegionCode._(6, _omitEnumNames ? '' : 'ANZ'); + + /// + /// Korea + static const Config_LoRaConfig_RegionCode KR = + Config_LoRaConfig_RegionCode._(7, _omitEnumNames ? '' : 'KR'); + + /// + /// Taiwan + static const Config_LoRaConfig_RegionCode TW = + Config_LoRaConfig_RegionCode._(8, _omitEnumNames ? '' : 'TW'); + + /// + /// Russia + static const Config_LoRaConfig_RegionCode RU = + Config_LoRaConfig_RegionCode._(9, _omitEnumNames ? '' : 'RU'); + + /// + /// India + static const Config_LoRaConfig_RegionCode IN = + Config_LoRaConfig_RegionCode._(10, _omitEnumNames ? '' : 'IN'); + + /// + /// New Zealand 865mhz + static const Config_LoRaConfig_RegionCode NZ_865 = + Config_LoRaConfig_RegionCode._(11, _omitEnumNames ? '' : 'NZ_865'); + + /// + /// Thailand + static const Config_LoRaConfig_RegionCode TH = + Config_LoRaConfig_RegionCode._(12, _omitEnumNames ? '' : 'TH'); + + /// + /// WLAN Band + static const Config_LoRaConfig_RegionCode LORA_24 = + Config_LoRaConfig_RegionCode._(13, _omitEnumNames ? '' : 'LORA_24'); + + /// + /// Ukraine 433mhz + static const Config_LoRaConfig_RegionCode UA_433 = + Config_LoRaConfig_RegionCode._(14, _omitEnumNames ? '' : 'UA_433'); + + /// + /// Ukraine 868mhz + static const Config_LoRaConfig_RegionCode UA_868 = + Config_LoRaConfig_RegionCode._(15, _omitEnumNames ? '' : 'UA_868'); + + /// + /// Malaysia 433mhz + static const Config_LoRaConfig_RegionCode MY_433 = + Config_LoRaConfig_RegionCode._(16, _omitEnumNames ? '' : 'MY_433'); + + /// + /// Malaysia 919mhz + static const Config_LoRaConfig_RegionCode MY_919 = + Config_LoRaConfig_RegionCode._(17, _omitEnumNames ? '' : 'MY_919'); + + /// + /// Singapore 923mhz + static const Config_LoRaConfig_RegionCode SG_923 = + Config_LoRaConfig_RegionCode._(18, _omitEnumNames ? '' : 'SG_923'); + + /// + /// Philippines 433mhz + static const Config_LoRaConfig_RegionCode PH_433 = + Config_LoRaConfig_RegionCode._(19, _omitEnumNames ? '' : 'PH_433'); + + /// + /// Philippines 868mhz + static const Config_LoRaConfig_RegionCode PH_868 = + Config_LoRaConfig_RegionCode._(20, _omitEnumNames ? '' : 'PH_868'); + + /// + /// Philippines 915mhz + static const Config_LoRaConfig_RegionCode PH_915 = + Config_LoRaConfig_RegionCode._(21, _omitEnumNames ? '' : 'PH_915'); + + /// + /// Australia / New Zealand 433MHz + static const Config_LoRaConfig_RegionCode ANZ_433 = + Config_LoRaConfig_RegionCode._(22, _omitEnumNames ? '' : 'ANZ_433'); + + /// + /// Kazakhstan 433MHz + static const Config_LoRaConfig_RegionCode KZ_433 = + Config_LoRaConfig_RegionCode._(23, _omitEnumNames ? '' : 'KZ_433'); + + /// + /// Kazakhstan 863MHz + static const Config_LoRaConfig_RegionCode KZ_863 = + Config_LoRaConfig_RegionCode._(24, _omitEnumNames ? '' : 'KZ_863'); + + /// + /// Nepal 865MHz + static const Config_LoRaConfig_RegionCode NP_865 = + Config_LoRaConfig_RegionCode._(25, _omitEnumNames ? '' : 'NP_865'); + + /// + /// Brazil 902MHz + static const Config_LoRaConfig_RegionCode BR_902 = + Config_LoRaConfig_RegionCode._(26, _omitEnumNames ? '' : 'BR_902'); + + static const $core.List values = + [ + UNSET, + US, + EU_433, + EU_868, + CN, + JP, + ANZ, + KR, + TW, + RU, + IN, + NZ_865, + TH, + LORA_24, + UA_433, + UA_868, + MY_433, + MY_919, + SG_923, + PH_433, + PH_868, + PH_915, + ANZ_433, + KZ_433, + KZ_863, + NP_865, + BR_902, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 26); + static Config_LoRaConfig_RegionCode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_LoRaConfig_RegionCode._(super.value, super.name); +} + +/// +/// Standard predefined channel settings +/// Note: these mappings must match ModemPreset Choice in the device code. +class Config_LoRaConfig_ModemPreset extends $pb.ProtobufEnum { + /// + /// Long Range - Fast + static const Config_LoRaConfig_ModemPreset LONG_FAST = + Config_LoRaConfig_ModemPreset._(0, _omitEnumNames ? '' : 'LONG_FAST'); + + /// + /// Long Range - Slow + static const Config_LoRaConfig_ModemPreset LONG_SLOW = + Config_LoRaConfig_ModemPreset._(1, _omitEnumNames ? '' : 'LONG_SLOW'); + + /// + /// Very Long Range - Slow + /// Deprecated in 2.5: Works only with txco and is unusably slow + @$core.Deprecated('This enum value is deprecated') + static const Config_LoRaConfig_ModemPreset VERY_LONG_SLOW = + Config_LoRaConfig_ModemPreset._( + 2, _omitEnumNames ? '' : 'VERY_LONG_SLOW'); + + /// + /// Medium Range - Slow + static const Config_LoRaConfig_ModemPreset MEDIUM_SLOW = + Config_LoRaConfig_ModemPreset._(3, _omitEnumNames ? '' : 'MEDIUM_SLOW'); + + /// + /// Medium Range - Fast + static const Config_LoRaConfig_ModemPreset MEDIUM_FAST = + Config_LoRaConfig_ModemPreset._(4, _omitEnumNames ? '' : 'MEDIUM_FAST'); + + /// + /// Short Range - Slow + static const Config_LoRaConfig_ModemPreset SHORT_SLOW = + Config_LoRaConfig_ModemPreset._(5, _omitEnumNames ? '' : 'SHORT_SLOW'); + + /// + /// Short Range - Fast + static const Config_LoRaConfig_ModemPreset SHORT_FAST = + Config_LoRaConfig_ModemPreset._(6, _omitEnumNames ? '' : 'SHORT_FAST'); + + /// + /// Long Range - Moderately Fast + static const Config_LoRaConfig_ModemPreset LONG_MODERATE = + Config_LoRaConfig_ModemPreset._(7, _omitEnumNames ? '' : 'LONG_MODERATE'); + + /// + /// Short Range - Turbo + /// This is the fastest preset and the only one with 500kHz bandwidth. + /// It is not legal to use in all regions due to this wider bandwidth. + static const Config_LoRaConfig_ModemPreset SHORT_TURBO = + Config_LoRaConfig_ModemPreset._(8, _omitEnumNames ? '' : 'SHORT_TURBO'); + + static const $core.List values = + [ + LONG_FAST, + LONG_SLOW, + VERY_LONG_SLOW, + MEDIUM_SLOW, + MEDIUM_FAST, + SHORT_SLOW, + SHORT_FAST, + LONG_MODERATE, + SHORT_TURBO, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 8); + static Config_LoRaConfig_ModemPreset? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_LoRaConfig_ModemPreset._(super.value, super.name); +} + +class Config_BluetoothConfig_PairingMode extends $pb.ProtobufEnum { + /// + /// Device generates a random PIN that will be shown on the screen of the device for pairing + static const Config_BluetoothConfig_PairingMode RANDOM_PIN = + Config_BluetoothConfig_PairingMode._( + 0, _omitEnumNames ? '' : 'RANDOM_PIN'); + + /// + /// Device requires a specified fixed PIN for pairing + static const Config_BluetoothConfig_PairingMode FIXED_PIN = + Config_BluetoothConfig_PairingMode._( + 1, _omitEnumNames ? '' : 'FIXED_PIN'); + + /// + /// Device requires no PIN for pairing + static const Config_BluetoothConfig_PairingMode NO_PIN = + Config_BluetoothConfig_PairingMode._(2, _omitEnumNames ? '' : 'NO_PIN'); + + static const $core.List values = + [ + RANDOM_PIN, + FIXED_PIN, + NO_PIN, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static Config_BluetoothConfig_PairingMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Config_BluetoothConfig_PairingMode._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/config.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/config.pbjson.dart new file mode 100644 index 000000000..c5dd98ac3 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/config.pbjson.dart @@ -0,0 +1,911 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/config.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use configDescriptor instead') +const Config$json = { + '1': 'Config', + '2': [ + { + '1': 'device', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.DeviceConfig', + '9': 0, + '10': 'device' + }, + { + '1': 'position', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.PositionConfig', + '9': 0, + '10': 'position' + }, + { + '1': 'power', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.PowerConfig', + '9': 0, + '10': 'power' + }, + { + '1': 'network', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.NetworkConfig', + '9': 0, + '10': 'network' + }, + { + '1': 'display', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.DisplayConfig', + '9': 0, + '10': 'display' + }, + { + '1': 'lora', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.LoRaConfig', + '9': 0, + '10': 'lora' + }, + { + '1': 'bluetooth', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.BluetoothConfig', + '9': 0, + '10': 'bluetooth' + }, + { + '1': 'security', + '3': 8, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.SecurityConfig', + '9': 0, + '10': 'security' + }, + { + '1': 'sessionkey', + '3': 9, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.SessionkeyConfig', + '9': 0, + '10': 'sessionkey' + }, + { + '1': 'device_ui', + '3': 10, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceUIConfig', + '9': 0, + '10': 'deviceUi' + }, + ], + '3': [ + Config_DeviceConfig$json, + Config_PositionConfig$json, + Config_PowerConfig$json, + Config_NetworkConfig$json, + Config_DisplayConfig$json, + Config_LoRaConfig$json, + Config_BluetoothConfig$json, + Config_SecurityConfig$json, + Config_SessionkeyConfig$json + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DeviceConfig$json = { + '1': 'DeviceConfig', + '2': [ + { + '1': 'role', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.Role', + '10': 'role' + }, + { + '1': 'serial_enabled', + '3': 2, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'serialEnabled', + }, + {'1': 'button_gpio', '3': 4, '4': 1, '5': 13, '10': 'buttonGpio'}, + {'1': 'buzzer_gpio', '3': 5, '4': 1, '5': 13, '10': 'buzzerGpio'}, + { + '1': 'rebroadcast_mode', + '3': 6, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.RebroadcastMode', + '10': 'rebroadcastMode' + }, + { + '1': 'node_info_broadcast_secs', + '3': 7, + '4': 1, + '5': 13, + '10': 'nodeInfoBroadcastSecs' + }, + { + '1': 'double_tap_as_button_press', + '3': 8, + '4': 1, + '5': 8, + '10': 'doubleTapAsButtonPress' + }, + { + '1': 'is_managed', + '3': 9, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'isManaged', + }, + { + '1': 'disable_triple_click', + '3': 10, + '4': 1, + '5': 8, + '10': 'disableTripleClick' + }, + {'1': 'tzdef', '3': 11, '4': 1, '5': 9, '10': 'tzdef'}, + { + '1': 'led_heartbeat_disabled', + '3': 12, + '4': 1, + '5': 8, + '10': 'ledHeartbeatDisabled' + }, + { + '1': 'buzzer_mode', + '3': 13, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.BuzzerMode', + '10': 'buzzerMode' + }, + ], + '4': [ + Config_DeviceConfig_Role$json, + Config_DeviceConfig_RebroadcastMode$json, + Config_DeviceConfig_BuzzerMode$json + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DeviceConfig_Role$json = { + '1': 'Role', + '2': [ + {'1': 'CLIENT', '2': 0}, + {'1': 'CLIENT_MUTE', '2': 1}, + {'1': 'ROUTER', '2': 2}, + { + '1': 'ROUTER_CLIENT', + '2': 3, + '3': {'1': true}, + }, + {'1': 'REPEATER', '2': 4}, + {'1': 'TRACKER', '2': 5}, + {'1': 'SENSOR', '2': 6}, + {'1': 'TAK', '2': 7}, + {'1': 'CLIENT_HIDDEN', '2': 8}, + {'1': 'LOST_AND_FOUND', '2': 9}, + {'1': 'TAK_TRACKER', '2': 10}, + {'1': 'ROUTER_LATE', '2': 11}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DeviceConfig_RebroadcastMode$json = { + '1': 'RebroadcastMode', + '2': [ + {'1': 'ALL', '2': 0}, + {'1': 'ALL_SKIP_DECODING', '2': 1}, + {'1': 'LOCAL_ONLY', '2': 2}, + {'1': 'KNOWN_ONLY', '2': 3}, + {'1': 'NONE', '2': 4}, + {'1': 'CORE_PORTNUMS_ONLY', '2': 5}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DeviceConfig_BuzzerMode$json = { + '1': 'BuzzerMode', + '2': [ + {'1': 'ALL_ENABLED', '2': 0}, + {'1': 'DISABLED', '2': 1}, + {'1': 'NOTIFICATIONS_ONLY', '2': 2}, + {'1': 'SYSTEM_ONLY', '2': 3}, + {'1': 'DIRECT_MSG_ONLY', '2': 4}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_PositionConfig$json = { + '1': 'PositionConfig', + '2': [ + { + '1': 'position_broadcast_secs', + '3': 1, + '4': 1, + '5': 13, + '10': 'positionBroadcastSecs' + }, + { + '1': 'position_broadcast_smart_enabled', + '3': 2, + '4': 1, + '5': 8, + '10': 'positionBroadcastSmartEnabled' + }, + {'1': 'fixed_position', '3': 3, '4': 1, '5': 8, '10': 'fixedPosition'}, + { + '1': 'gps_enabled', + '3': 4, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'gpsEnabled', + }, + { + '1': 'gps_update_interval', + '3': 5, + '4': 1, + '5': 13, + '10': 'gpsUpdateInterval' + }, + { + '1': 'gps_attempt_time', + '3': 6, + '4': 1, + '5': 13, + '8': {'3': true}, + '10': 'gpsAttemptTime', + }, + {'1': 'position_flags', '3': 7, '4': 1, '5': 13, '10': 'positionFlags'}, + {'1': 'rx_gpio', '3': 8, '4': 1, '5': 13, '10': 'rxGpio'}, + {'1': 'tx_gpio', '3': 9, '4': 1, '5': 13, '10': 'txGpio'}, + { + '1': 'broadcast_smart_minimum_distance', + '3': 10, + '4': 1, + '5': 13, + '10': 'broadcastSmartMinimumDistance' + }, + { + '1': 'broadcast_smart_minimum_interval_secs', + '3': 11, + '4': 1, + '5': 13, + '10': 'broadcastSmartMinimumIntervalSecs' + }, + {'1': 'gps_en_gpio', '3': 12, '4': 1, '5': 13, '10': 'gpsEnGpio'}, + { + '1': 'gps_mode', + '3': 13, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.PositionConfig.GpsMode', + '10': 'gpsMode' + }, + ], + '4': [ + Config_PositionConfig_PositionFlags$json, + Config_PositionConfig_GpsMode$json + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_PositionConfig_PositionFlags$json = { + '1': 'PositionFlags', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'ALTITUDE', '2': 1}, + {'1': 'ALTITUDE_MSL', '2': 2}, + {'1': 'GEOIDAL_SEPARATION', '2': 4}, + {'1': 'DOP', '2': 8}, + {'1': 'HVDOP', '2': 16}, + {'1': 'SATINVIEW', '2': 32}, + {'1': 'SEQ_NO', '2': 64}, + {'1': 'TIMESTAMP', '2': 128}, + {'1': 'HEADING', '2': 256}, + {'1': 'SPEED', '2': 512}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_PositionConfig_GpsMode$json = { + '1': 'GpsMode', + '2': [ + {'1': 'DISABLED', '2': 0}, + {'1': 'ENABLED', '2': 1}, + {'1': 'NOT_PRESENT', '2': 2}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_PowerConfig$json = { + '1': 'PowerConfig', + '2': [ + {'1': 'is_power_saving', '3': 1, '4': 1, '5': 8, '10': 'isPowerSaving'}, + { + '1': 'on_battery_shutdown_after_secs', + '3': 2, + '4': 1, + '5': 13, + '10': 'onBatteryShutdownAfterSecs' + }, + { + '1': 'adc_multiplier_override', + '3': 3, + '4': 1, + '5': 2, + '10': 'adcMultiplierOverride' + }, + { + '1': 'wait_bluetooth_secs', + '3': 4, + '4': 1, + '5': 13, + '10': 'waitBluetoothSecs' + }, + {'1': 'sds_secs', '3': 6, '4': 1, '5': 13, '10': 'sdsSecs'}, + {'1': 'ls_secs', '3': 7, '4': 1, '5': 13, '10': 'lsSecs'}, + {'1': 'min_wake_secs', '3': 8, '4': 1, '5': 13, '10': 'minWakeSecs'}, + { + '1': 'device_battery_ina_address', + '3': 9, + '4': 1, + '5': 13, + '10': 'deviceBatteryInaAddress' + }, + {'1': 'powermon_enables', '3': 32, '4': 1, '5': 4, '10': 'powermonEnables'}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_NetworkConfig$json = { + '1': 'NetworkConfig', + '2': [ + {'1': 'wifi_enabled', '3': 1, '4': 1, '5': 8, '10': 'wifiEnabled'}, + {'1': 'wifi_ssid', '3': 3, '4': 1, '5': 9, '10': 'wifiSsid'}, + {'1': 'wifi_psk', '3': 4, '4': 1, '5': 9, '10': 'wifiPsk'}, + {'1': 'ntp_server', '3': 5, '4': 1, '5': 9, '10': 'ntpServer'}, + {'1': 'eth_enabled', '3': 6, '4': 1, '5': 8, '10': 'ethEnabled'}, + { + '1': 'address_mode', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.NetworkConfig.AddressMode', + '10': 'addressMode' + }, + { + '1': 'ipv4_config', + '3': 8, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.NetworkConfig.IpV4Config', + '10': 'ipv4Config' + }, + {'1': 'rsyslog_server', '3': 9, '4': 1, '5': 9, '10': 'rsyslogServer'}, + { + '1': 'enabled_protocols', + '3': 10, + '4': 1, + '5': 13, + '10': 'enabledProtocols' + }, + {'1': 'ipv6_enabled', '3': 11, '4': 1, '5': 8, '10': 'ipv6Enabled'}, + ], + '3': [Config_NetworkConfig_IpV4Config$json], + '4': [ + Config_NetworkConfig_AddressMode$json, + Config_NetworkConfig_ProtocolFlags$json + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_NetworkConfig_IpV4Config$json = { + '1': 'IpV4Config', + '2': [ + {'1': 'ip', '3': 1, '4': 1, '5': 7, '10': 'ip'}, + {'1': 'gateway', '3': 2, '4': 1, '5': 7, '10': 'gateway'}, + {'1': 'subnet', '3': 3, '4': 1, '5': 7, '10': 'subnet'}, + {'1': 'dns', '3': 4, '4': 1, '5': 7, '10': 'dns'}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_NetworkConfig_AddressMode$json = { + '1': 'AddressMode', + '2': [ + {'1': 'DHCP', '2': 0}, + {'1': 'STATIC', '2': 1}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_NetworkConfig_ProtocolFlags$json = { + '1': 'ProtocolFlags', + '2': [ + {'1': 'NO_BROADCAST', '2': 0}, + {'1': 'UDP_BROADCAST', '2': 1}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DisplayConfig$json = { + '1': 'DisplayConfig', + '2': [ + {'1': 'screen_on_secs', '3': 1, '4': 1, '5': 13, '10': 'screenOnSecs'}, + { + '1': 'gps_format', + '3': 2, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DisplayConfig.GpsCoordinateFormat', + '8': {'3': true}, + '10': 'gpsFormat', + }, + { + '1': 'auto_screen_carousel_secs', + '3': 3, + '4': 1, + '5': 13, + '10': 'autoScreenCarouselSecs' + }, + { + '1': 'compass_north_top', + '3': 4, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'compassNorthTop', + }, + {'1': 'flip_screen', '3': 5, '4': 1, '5': 8, '10': 'flipScreen'}, + { + '1': 'units', + '3': 6, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DisplayConfig.DisplayUnits', + '10': 'units' + }, + { + '1': 'oled', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DisplayConfig.OledType', + '10': 'oled' + }, + { + '1': 'displaymode', + '3': 8, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DisplayConfig.DisplayMode', + '10': 'displaymode' + }, + {'1': 'heading_bold', '3': 9, '4': 1, '5': 8, '10': 'headingBold'}, + { + '1': 'wake_on_tap_or_motion', + '3': 10, + '4': 1, + '5': 8, + '10': 'wakeOnTapOrMotion' + }, + { + '1': 'compass_orientation', + '3': 11, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DisplayConfig.CompassOrientation', + '10': 'compassOrientation' + }, + {'1': 'use_12h_clock', '3': 12, '4': 1, '5': 8, '10': 'use12hClock'}, + ], + '4': [ + Config_DisplayConfig_GpsCoordinateFormat$json, + Config_DisplayConfig_DisplayUnits$json, + Config_DisplayConfig_OledType$json, + Config_DisplayConfig_DisplayMode$json, + Config_DisplayConfig_CompassOrientation$json + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DisplayConfig_GpsCoordinateFormat$json = { + '1': 'GpsCoordinateFormat', + '2': [ + {'1': 'DEC', '2': 0}, + {'1': 'DMS', '2': 1}, + {'1': 'UTM', '2': 2}, + {'1': 'MGRS', '2': 3}, + {'1': 'OLC', '2': 4}, + {'1': 'OSGR', '2': 5}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DisplayConfig_DisplayUnits$json = { + '1': 'DisplayUnits', + '2': [ + {'1': 'METRIC', '2': 0}, + {'1': 'IMPERIAL', '2': 1}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DisplayConfig_OledType$json = { + '1': 'OledType', + '2': [ + {'1': 'OLED_AUTO', '2': 0}, + {'1': 'OLED_SSD1306', '2': 1}, + {'1': 'OLED_SH1106', '2': 2}, + {'1': 'OLED_SH1107', '2': 3}, + {'1': 'OLED_SH1107_128_64', '2': 4}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DisplayConfig_DisplayMode$json = { + '1': 'DisplayMode', + '2': [ + {'1': 'DEFAULT', '2': 0}, + {'1': 'TWOCOLOR', '2': 1}, + {'1': 'INVERTED', '2': 2}, + {'1': 'COLOR', '2': 3}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_DisplayConfig_CompassOrientation$json = { + '1': 'CompassOrientation', + '2': [ + {'1': 'DEGREES_0', '2': 0}, + {'1': 'DEGREES_90', '2': 1}, + {'1': 'DEGREES_180', '2': 2}, + {'1': 'DEGREES_270', '2': 3}, + {'1': 'DEGREES_0_INVERTED', '2': 4}, + {'1': 'DEGREES_90_INVERTED', '2': 5}, + {'1': 'DEGREES_180_INVERTED', '2': 6}, + {'1': 'DEGREES_270_INVERTED', '2': 7}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_LoRaConfig$json = { + '1': 'LoRaConfig', + '2': [ + {'1': 'use_preset', '3': 1, '4': 1, '5': 8, '10': 'usePreset'}, + { + '1': 'modem_preset', + '3': 2, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.LoRaConfig.ModemPreset', + '10': 'modemPreset' + }, + {'1': 'bandwidth', '3': 3, '4': 1, '5': 13, '10': 'bandwidth'}, + {'1': 'spread_factor', '3': 4, '4': 1, '5': 13, '10': 'spreadFactor'}, + {'1': 'coding_rate', '3': 5, '4': 1, '5': 13, '10': 'codingRate'}, + {'1': 'frequency_offset', '3': 6, '4': 1, '5': 2, '10': 'frequencyOffset'}, + { + '1': 'region', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.LoRaConfig.RegionCode', + '10': 'region' + }, + {'1': 'hop_limit', '3': 8, '4': 1, '5': 13, '10': 'hopLimit'}, + {'1': 'tx_enabled', '3': 9, '4': 1, '5': 8, '10': 'txEnabled'}, + {'1': 'tx_power', '3': 10, '4': 1, '5': 5, '10': 'txPower'}, + {'1': 'channel_num', '3': 11, '4': 1, '5': 13, '10': 'channelNum'}, + { + '1': 'override_duty_cycle', + '3': 12, + '4': 1, + '5': 8, + '10': 'overrideDutyCycle' + }, + { + '1': 'sx126x_rx_boosted_gain', + '3': 13, + '4': 1, + '5': 8, + '10': 'sx126xRxBoostedGain' + }, + { + '1': 'override_frequency', + '3': 14, + '4': 1, + '5': 2, + '10': 'overrideFrequency' + }, + {'1': 'pa_fan_disabled', '3': 15, '4': 1, '5': 8, '10': 'paFanDisabled'}, + {'1': 'ignore_incoming', '3': 103, '4': 3, '5': 13, '10': 'ignoreIncoming'}, + {'1': 'ignore_mqtt', '3': 104, '4': 1, '5': 8, '10': 'ignoreMqtt'}, + { + '1': 'config_ok_to_mqtt', + '3': 105, + '4': 1, + '5': 8, + '10': 'configOkToMqtt' + }, + ], + '4': [Config_LoRaConfig_RegionCode$json, Config_LoRaConfig_ModemPreset$json], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_LoRaConfig_RegionCode$json = { + '1': 'RegionCode', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'US', '2': 1}, + {'1': 'EU_433', '2': 2}, + {'1': 'EU_868', '2': 3}, + {'1': 'CN', '2': 4}, + {'1': 'JP', '2': 5}, + {'1': 'ANZ', '2': 6}, + {'1': 'KR', '2': 7}, + {'1': 'TW', '2': 8}, + {'1': 'RU', '2': 9}, + {'1': 'IN', '2': 10}, + {'1': 'NZ_865', '2': 11}, + {'1': 'TH', '2': 12}, + {'1': 'LORA_24', '2': 13}, + {'1': 'UA_433', '2': 14}, + {'1': 'UA_868', '2': 15}, + {'1': 'MY_433', '2': 16}, + {'1': 'MY_919', '2': 17}, + {'1': 'SG_923', '2': 18}, + {'1': 'PH_433', '2': 19}, + {'1': 'PH_868', '2': 20}, + {'1': 'PH_915', '2': 21}, + {'1': 'ANZ_433', '2': 22}, + {'1': 'KZ_433', '2': 23}, + {'1': 'KZ_863', '2': 24}, + {'1': 'NP_865', '2': 25}, + {'1': 'BR_902', '2': 26}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_LoRaConfig_ModemPreset$json = { + '1': 'ModemPreset', + '2': [ + {'1': 'LONG_FAST', '2': 0}, + {'1': 'LONG_SLOW', '2': 1}, + { + '1': 'VERY_LONG_SLOW', + '2': 2, + '3': {'1': true}, + }, + {'1': 'MEDIUM_SLOW', '2': 3}, + {'1': 'MEDIUM_FAST', '2': 4}, + {'1': 'SHORT_SLOW', '2': 5}, + {'1': 'SHORT_FAST', '2': 6}, + {'1': 'LONG_MODERATE', '2': 7}, + {'1': 'SHORT_TURBO', '2': 8}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_BluetoothConfig$json = { + '1': 'BluetoothConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + { + '1': 'mode', + '3': 2, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.BluetoothConfig.PairingMode', + '10': 'mode' + }, + {'1': 'fixed_pin', '3': 3, '4': 1, '5': 13, '10': 'fixedPin'}, + ], + '4': [Config_BluetoothConfig_PairingMode$json], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_BluetoothConfig_PairingMode$json = { + '1': 'PairingMode', + '2': [ + {'1': 'RANDOM_PIN', '2': 0}, + {'1': 'FIXED_PIN', '2': 1}, + {'1': 'NO_PIN', '2': 2}, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_SecurityConfig$json = { + '1': 'SecurityConfig', + '2': [ + {'1': 'public_key', '3': 1, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'private_key', '3': 2, '4': 1, '5': 12, '10': 'privateKey'}, + {'1': 'admin_key', '3': 3, '4': 3, '5': 12, '10': 'adminKey'}, + {'1': 'is_managed', '3': 4, '4': 1, '5': 8, '10': 'isManaged'}, + {'1': 'serial_enabled', '3': 5, '4': 1, '5': 8, '10': 'serialEnabled'}, + { + '1': 'debug_log_api_enabled', + '3': 6, + '4': 1, + '5': 8, + '10': 'debugLogApiEnabled' + }, + { + '1': 'admin_channel_enabled', + '3': 8, + '4': 1, + '5': 8, + '10': 'adminChannelEnabled' + }, + ], +}; + +@$core.Deprecated('Use configDescriptor instead') +const Config_SessionkeyConfig$json = { + '1': 'SessionkeyConfig', +}; + +/// Descriptor for `Config`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List configDescriptor = $convert.base64Decode( + 'CgZDb25maWcSOQoGZGV2aWNlGAEgASgLMh8ubWVzaHRhc3RpYy5Db25maWcuRGV2aWNlQ29uZm' + 'lnSABSBmRldmljZRI/Cghwb3NpdGlvbhgCIAEoCzIhLm1lc2h0YXN0aWMuQ29uZmlnLlBvc2l0' + 'aW9uQ29uZmlnSABSCHBvc2l0aW9uEjYKBXBvd2VyGAMgASgLMh4ubWVzaHRhc3RpYy5Db25maW' + 'cuUG93ZXJDb25maWdIAFIFcG93ZXISPAoHbmV0d29yaxgEIAEoCzIgLm1lc2h0YXN0aWMuQ29u' + 'ZmlnLk5ldHdvcmtDb25maWdIAFIHbmV0d29yaxI8CgdkaXNwbGF5GAUgASgLMiAubWVzaHRhc3' + 'RpYy5Db25maWcuRGlzcGxheUNvbmZpZ0gAUgdkaXNwbGF5EjMKBGxvcmEYBiABKAsyHS5tZXNo' + 'dGFzdGljLkNvbmZpZy5Mb1JhQ29uZmlnSABSBGxvcmESQgoJYmx1ZXRvb3RoGAcgASgLMiIubW' + 'VzaHRhc3RpYy5Db25maWcuQmx1ZXRvb3RoQ29uZmlnSABSCWJsdWV0b290aBI/CghzZWN1cml0' + 'eRgIIAEoCzIhLm1lc2h0YXN0aWMuQ29uZmlnLlNlY3VyaXR5Q29uZmlnSABSCHNlY3VyaXR5Ek' + 'UKCnNlc3Npb25rZXkYCSABKAsyIy5tZXNodGFzdGljLkNvbmZpZy5TZXNzaW9ua2V5Q29uZmln' + 'SABSCnNlc3Npb25rZXkSOQoJZGV2aWNlX3VpGAogASgLMhoubWVzaHRhc3RpYy5EZXZpY2VVSU' + 'NvbmZpZ0gAUghkZXZpY2VVaRqWCAoMRGV2aWNlQ29uZmlnEjgKBHJvbGUYASABKA4yJC5tZXNo' + 'dGFzdGljLkNvbmZpZy5EZXZpY2VDb25maWcuUm9sZVIEcm9sZRIpCg5zZXJpYWxfZW5hYmxlZB' + 'gCIAEoCEICGAFSDXNlcmlhbEVuYWJsZWQSHwoLYnV0dG9uX2dwaW8YBCABKA1SCmJ1dHRvbkdw' + 'aW8SHwoLYnV6emVyX2dwaW8YBSABKA1SCmJ1enplckdwaW8SWgoQcmVicm9hZGNhc3RfbW9kZR' + 'gGIAEoDjIvLm1lc2h0YXN0aWMuQ29uZmlnLkRldmljZUNvbmZpZy5SZWJyb2FkY2FzdE1vZGVS' + 'D3JlYnJvYWRjYXN0TW9kZRI3Chhub2RlX2luZm9fYnJvYWRjYXN0X3NlY3MYByABKA1SFW5vZG' + 'VJbmZvQnJvYWRjYXN0U2VjcxI6Chpkb3VibGVfdGFwX2FzX2J1dHRvbl9wcmVzcxgIIAEoCFIW' + 'ZG91YmxlVGFwQXNCdXR0b25QcmVzcxIhCgppc19tYW5hZ2VkGAkgASgIQgIYAVIJaXNNYW5hZ2' + 'VkEjAKFGRpc2FibGVfdHJpcGxlX2NsaWNrGAogASgIUhJkaXNhYmxlVHJpcGxlQ2xpY2sSFAoF' + 'dHpkZWYYCyABKAlSBXR6ZGVmEjQKFmxlZF9oZWFydGJlYXRfZGlzYWJsZWQYDCABKAhSFGxlZE' + 'hlYXJ0YmVhdERpc2FibGVkEksKC2J1enplcl9tb2RlGA0gASgOMioubWVzaHRhc3RpYy5Db25m' + 'aWcuRGV2aWNlQ29uZmlnLkJ1enplck1vZGVSCmJ1enplck1vZGUivwEKBFJvbGUSCgoGQ0xJRU' + '5UEAASDwoLQ0xJRU5UX01VVEUQARIKCgZST1VURVIQAhIVCg1ST1VURVJfQ0xJRU5UEAMaAggB' + 'EgwKCFJFUEVBVEVSEAQSCwoHVFJBQ0tFUhAFEgoKBlNFTlNPUhAGEgcKA1RBSxAHEhEKDUNMSU' + 'VOVF9ISURERU4QCBISCg5MT1NUX0FORF9GT1VORBAJEg8KC1RBS19UUkFDS0VSEAoSDwoLUk9V' + 'VEVSX0xBVEUQCyJzCg9SZWJyb2FkY2FzdE1vZGUSBwoDQUxMEAASFQoRQUxMX1NLSVBfREVDT0' + 'RJTkcQARIOCgpMT0NBTF9PTkxZEAISDgoKS05PV05fT05MWRADEggKBE5PTkUQBBIWChJDT1JF' + 'X1BPUlROVU1TX09OTFkQBSJpCgpCdXp6ZXJNb2RlEg8KC0FMTF9FTkFCTEVEEAASDAoIRElTQU' + 'JMRUQQARIWChJOT1RJRklDQVRJT05TX09OTFkQAhIPCgtTWVNURU1fT05MWRADEhMKD0RJUkVD' + 'VF9NU0dfT05MWRAEGvoGCg5Qb3NpdGlvbkNvbmZpZxI2Chdwb3NpdGlvbl9icm9hZGNhc3Rfc2' + 'VjcxgBIAEoDVIVcG9zaXRpb25Ccm9hZGNhc3RTZWNzEkcKIHBvc2l0aW9uX2Jyb2FkY2FzdF9z' + 'bWFydF9lbmFibGVkGAIgASgIUh1wb3NpdGlvbkJyb2FkY2FzdFNtYXJ0RW5hYmxlZBIlCg5maX' + 'hlZF9wb3NpdGlvbhgDIAEoCFINZml4ZWRQb3NpdGlvbhIjCgtncHNfZW5hYmxlZBgEIAEoCEIC' + 'GAFSCmdwc0VuYWJsZWQSLgoTZ3BzX3VwZGF0ZV9pbnRlcnZhbBgFIAEoDVIRZ3BzVXBkYXRlSW' + '50ZXJ2YWwSLAoQZ3BzX2F0dGVtcHRfdGltZRgGIAEoDUICGAFSDmdwc0F0dGVtcHRUaW1lEiUK' + 'DnBvc2l0aW9uX2ZsYWdzGAcgASgNUg1wb3NpdGlvbkZsYWdzEhcKB3J4X2dwaW8YCCABKA1SBn' + 'J4R3BpbxIXCgd0eF9ncGlvGAkgASgNUgZ0eEdwaW8SRwogYnJvYWRjYXN0X3NtYXJ0X21pbmlt' + 'dW1fZGlzdGFuY2UYCiABKA1SHWJyb2FkY2FzdFNtYXJ0TWluaW11bURpc3RhbmNlElAKJWJyb2' + 'FkY2FzdF9zbWFydF9taW5pbXVtX2ludGVydmFsX3NlY3MYCyABKA1SIWJyb2FkY2FzdFNtYXJ0' + 'TWluaW11bUludGVydmFsU2VjcxIeCgtncHNfZW5fZ3BpbxgMIAEoDVIJZ3BzRW5HcGlvEkQKCG' + 'dwc19tb2RlGA0gASgOMikubWVzaHRhc3RpYy5Db25maWcuUG9zaXRpb25Db25maWcuR3BzTW9k' + 'ZVIHZ3BzTW9kZSKrAQoNUG9zaXRpb25GbGFncxIJCgVVTlNFVBAAEgwKCEFMVElUVURFEAESEA' + 'oMQUxUSVRVREVfTVNMEAISFgoSR0VPSURBTF9TRVBBUkFUSU9OEAQSBwoDRE9QEAgSCQoFSFZE' + 'T1AQEBINCglTQVRJTlZJRVcQIBIKCgZTRVFfTk8QQBIOCglUSU1FU1RBTVAQgAESDAoHSEVBRE' + 'lORxCAAhIKCgVTUEVFRBCABCI1CgdHcHNNb2RlEgwKCERJU0FCTEVEEAASCwoHRU5BQkxFRBAB' + 'Eg8KC05PVF9QUkVTRU5UEAIaoQMKC1Bvd2VyQ29uZmlnEiYKD2lzX3Bvd2VyX3NhdmluZxgBIA' + 'EoCFINaXNQb3dlclNhdmluZxJCCh5vbl9iYXR0ZXJ5X3NodXRkb3duX2FmdGVyX3NlY3MYAiAB' + 'KA1SGm9uQmF0dGVyeVNodXRkb3duQWZ0ZXJTZWNzEjYKF2FkY19tdWx0aXBsaWVyX292ZXJyaW' + 'RlGAMgASgCUhVhZGNNdWx0aXBsaWVyT3ZlcnJpZGUSLgoTd2FpdF9ibHVldG9vdGhfc2VjcxgE' + 'IAEoDVIRd2FpdEJsdWV0b290aFNlY3MSGQoIc2RzX3NlY3MYBiABKA1SB3Nkc1NlY3MSFwoHbH' + 'Nfc2VjcxgHIAEoDVIGbHNTZWNzEiIKDW1pbl93YWtlX3NlY3MYCCABKA1SC21pbldha2VTZWNz' + 'EjsKGmRldmljZV9iYXR0ZXJ5X2luYV9hZGRyZXNzGAkgASgNUhdkZXZpY2VCYXR0ZXJ5SW5hQW' + 'RkcmVzcxIpChBwb3dlcm1vbl9lbmFibGVzGCAgASgEUg9wb3dlcm1vbkVuYWJsZXMa/QQKDU5l' + 'dHdvcmtDb25maWcSIQoMd2lmaV9lbmFibGVkGAEgASgIUgt3aWZpRW5hYmxlZBIbCgl3aWZpX3' + 'NzaWQYAyABKAlSCHdpZmlTc2lkEhkKCHdpZmlfcHNrGAQgASgJUgd3aWZpUHNrEh0KCm50cF9z' + 'ZXJ2ZXIYBSABKAlSCW50cFNlcnZlchIfCgtldGhfZW5hYmxlZBgGIAEoCFIKZXRoRW5hYmxlZB' + 'JPCgxhZGRyZXNzX21vZGUYByABKA4yLC5tZXNodGFzdGljLkNvbmZpZy5OZXR3b3JrQ29uZmln' + 'LkFkZHJlc3NNb2RlUgthZGRyZXNzTW9kZRJMCgtpcHY0X2NvbmZpZxgIIAEoCzIrLm1lc2h0YX' + 'N0aWMuQ29uZmlnLk5ldHdvcmtDb25maWcuSXBWNENvbmZpZ1IKaXB2NENvbmZpZxIlCg5yc3lz' + 'bG9nX3NlcnZlchgJIAEoCVINcnN5c2xvZ1NlcnZlchIrChFlbmFibGVkX3Byb3RvY29scxgKIA' + 'EoDVIQZW5hYmxlZFByb3RvY29scxIhCgxpcHY2X2VuYWJsZWQYCyABKAhSC2lwdjZFbmFibGVk' + 'GmAKCklwVjRDb25maWcSDgoCaXAYASABKAdSAmlwEhgKB2dhdGV3YXkYAiABKAdSB2dhdGV3YX' + 'kSFgoGc3VibmV0GAMgASgHUgZzdWJuZXQSEAoDZG5zGAQgASgHUgNkbnMiIwoLQWRkcmVzc01v' + 'ZGUSCAoEREhDUBAAEgoKBlNUQVRJQxABIjQKDVByb3RvY29sRmxhZ3MSEAoMTk9fQlJPQURDQV' + 'NUEAASEQoNVURQX0JST0FEQ0FTVBABGq0JCg1EaXNwbGF5Q29uZmlnEiQKDnNjcmVlbl9vbl9z' + 'ZWNzGAEgASgNUgxzY3JlZW5PblNlY3MSVwoKZ3BzX2Zvcm1hdBgCIAEoDjI0Lm1lc2h0YXN0aW' + 'MuQ29uZmlnLkRpc3BsYXlDb25maWcuR3BzQ29vcmRpbmF0ZUZvcm1hdEICGAFSCWdwc0Zvcm1h' + 'dBI5ChlhdXRvX3NjcmVlbl9jYXJvdXNlbF9zZWNzGAMgASgNUhZhdXRvU2NyZWVuQ2Fyb3VzZW' + 'xTZWNzEi4KEWNvbXBhc3Nfbm9ydGhfdG9wGAQgASgIQgIYAVIPY29tcGFzc05vcnRoVG9wEh8K' + 'C2ZsaXBfc2NyZWVuGAUgASgIUgpmbGlwU2NyZWVuEkMKBXVuaXRzGAYgASgOMi0ubWVzaHRhc3' + 'RpYy5Db25maWcuRGlzcGxheUNvbmZpZy5EaXNwbGF5VW5pdHNSBXVuaXRzEj0KBG9sZWQYByAB' + 'KA4yKS5tZXNodGFzdGljLkNvbmZpZy5EaXNwbGF5Q29uZmlnLk9sZWRUeXBlUgRvbGVkEk4KC2' + 'Rpc3BsYXltb2RlGAggASgOMiwubWVzaHRhc3RpYy5Db25maWcuRGlzcGxheUNvbmZpZy5EaXNw' + 'bGF5TW9kZVILZGlzcGxheW1vZGUSIQoMaGVhZGluZ19ib2xkGAkgASgIUgtoZWFkaW5nQm9sZB' + 'IwChV3YWtlX29uX3RhcF9vcl9tb3Rpb24YCiABKAhSEXdha2VPblRhcE9yTW90aW9uEmQKE2Nv' + 'bXBhc3Nfb3JpZW50YXRpb24YCyABKA4yMy5tZXNodGFzdGljLkNvbmZpZy5EaXNwbGF5Q29uZm' + 'lnLkNvbXBhc3NPcmllbnRhdGlvblISY29tcGFzc09yaWVudGF0aW9uEiIKDXVzZV8xMmhfY2xv' + 'Y2sYDCABKAhSC3VzZTEyaENsb2NrIk0KE0dwc0Nvb3JkaW5hdGVGb3JtYXQSBwoDREVDEAASBw' + 'oDRE1TEAESBwoDVVRNEAISCAoETUdSUxADEgcKA09MQxAEEggKBE9TR1IQBSIoCgxEaXNwbGF5' + 'VW5pdHMSCgoGTUVUUklDEAASDAoISU1QRVJJQUwQASJlCghPbGVkVHlwZRINCglPTEVEX0FVVE' + '8QABIQCgxPTEVEX1NTRDEzMDYQARIPCgtPTEVEX1NIMTEwNhACEg8KC09MRURfU0gxMTA3EAMS' + 'FgoST0xFRF9TSDExMDdfMTI4XzY0EAQiQQoLRGlzcGxheU1vZGUSCwoHREVGQVVMVBAAEgwKCF' + 'RXT0NPTE9SEAESDAoISU5WRVJURUQQAhIJCgVDT0xPUhADIroBChJDb21wYXNzT3JpZW50YXRp' + 'b24SDQoJREVHUkVFU18wEAASDgoKREVHUkVFU185MBABEg8KC0RFR1JFRVNfMTgwEAISDwoLRE' + 'VHUkVFU18yNzAQAxIWChJERUdSRUVTXzBfSU5WRVJURUQQBBIXChNERUdSRUVTXzkwX0lOVkVS' + 'VEVEEAUSGAoUREVHUkVFU18xODBfSU5WRVJURUQQBhIYChRERUdSRUVTXzI3MF9JTlZFUlRFRB' + 'AHGtAJCgpMb1JhQ29uZmlnEh0KCnVzZV9wcmVzZXQYASABKAhSCXVzZVByZXNldBJMCgxtb2Rl' + 'bV9wcmVzZXQYAiABKA4yKS5tZXNodGFzdGljLkNvbmZpZy5Mb1JhQ29uZmlnLk1vZGVtUHJlc2' + 'V0Ugttb2RlbVByZXNldBIcCgliYW5kd2lkdGgYAyABKA1SCWJhbmR3aWR0aBIjCg1zcHJlYWRf' + 'ZmFjdG9yGAQgASgNUgxzcHJlYWRGYWN0b3ISHwoLY29kaW5nX3JhdGUYBSABKA1SCmNvZGluZ1' + 'JhdGUSKQoQZnJlcXVlbmN5X29mZnNldBgGIAEoAlIPZnJlcXVlbmN5T2Zmc2V0EkAKBnJlZ2lv' + 'bhgHIAEoDjIoLm1lc2h0YXN0aWMuQ29uZmlnLkxvUmFDb25maWcuUmVnaW9uQ29kZVIGcmVnaW' + '9uEhsKCWhvcF9saW1pdBgIIAEoDVIIaG9wTGltaXQSHQoKdHhfZW5hYmxlZBgJIAEoCFIJdHhF' + 'bmFibGVkEhkKCHR4X3Bvd2VyGAogASgFUgd0eFBvd2VyEh8KC2NoYW5uZWxfbnVtGAsgASgNUg' + 'pjaGFubmVsTnVtEi4KE292ZXJyaWRlX2R1dHlfY3ljbGUYDCABKAhSEW92ZXJyaWRlRHV0eUN5' + 'Y2xlEjMKFnN4MTI2eF9yeF9ib29zdGVkX2dhaW4YDSABKAhSE3N4MTI2eFJ4Qm9vc3RlZEdhaW' + '4SLQoSb3ZlcnJpZGVfZnJlcXVlbmN5GA4gASgCUhFvdmVycmlkZUZyZXF1ZW5jeRImCg9wYV9m' + 'YW5fZGlzYWJsZWQYDyABKAhSDXBhRmFuRGlzYWJsZWQSJwoPaWdub3JlX2luY29taW5nGGcgAy' + 'gNUg5pZ25vcmVJbmNvbWluZxIfCgtpZ25vcmVfbXF0dBhoIAEoCFIKaWdub3JlTXF0dBIpChFj' + 'b25maWdfb2tfdG9fbXF0dBhpIAEoCFIOY29uZmlnT2tUb01xdHQirgIKClJlZ2lvbkNvZGUSCQ' + 'oFVU5TRVQQABIGCgJVUxABEgoKBkVVXzQzMxACEgoKBkVVXzg2OBADEgYKAkNOEAQSBgoCSlAQ' + 'BRIHCgNBTloQBhIGCgJLUhAHEgYKAlRXEAgSBgoCUlUQCRIGCgJJThAKEgoKBk5aXzg2NRALEg' + 'YKAlRIEAwSCwoHTE9SQV8yNBANEgoKBlVBXzQzMxAOEgoKBlVBXzg2OBAPEgoKBk1ZXzQzMxAQ' + 'EgoKBk1ZXzkxORAREgoKBlNHXzkyMxASEgoKBlBIXzQzMxATEgoKBlBIXzg2OBAUEgoKBlBIXz' + 'kxNRAVEgsKB0FOWl80MzMQFhIKCgZLWl80MzMQFxIKCgZLWl84NjMQGBIKCgZOUF84NjUQGRIK' + 'CgZCUl85MDIQGiKpAQoLTW9kZW1QcmVzZXQSDQoJTE9OR19GQVNUEAASDQoJTE9OR19TTE9XEA' + 'ESFgoOVkVSWV9MT05HX1NMT1cQAhoCCAESDwoLTUVESVVNX1NMT1cQAxIPCgtNRURJVU1fRkFT' + 'VBAEEg4KClNIT1JUX1NMT1cQBRIOCgpTSE9SVF9GQVNUEAYSEQoNTE9OR19NT0RFUkFURRAHEg' + '8KC1NIT1JUX1RVUkJPEAgaxgEKD0JsdWV0b290aENvbmZpZxIYCgdlbmFibGVkGAEgASgIUgdl' + 'bmFibGVkEkIKBG1vZGUYAiABKA4yLi5tZXNodGFzdGljLkNvbmZpZy5CbHVldG9vdGhDb25maW' + 'cuUGFpcmluZ01vZGVSBG1vZGUSGwoJZml4ZWRfcGluGAMgASgNUghmaXhlZFBpbiI4CgtQYWly' + 'aW5nTW9kZRIOCgpSQU5ET01fUElOEAASDQoJRklYRURfUElOEAESCgoGTk9fUElOEAIamgIKDl' + 'NlY3VyaXR5Q29uZmlnEh0KCnB1YmxpY19rZXkYASABKAxSCXB1YmxpY0tleRIfCgtwcml2YXRl' + 'X2tleRgCIAEoDFIKcHJpdmF0ZUtleRIbCglhZG1pbl9rZXkYAyADKAxSCGFkbWluS2V5Eh0KCm' + 'lzX21hbmFnZWQYBCABKAhSCWlzTWFuYWdlZBIlCg5zZXJpYWxfZW5hYmxlZBgFIAEoCFINc2Vy' + 'aWFsRW5hYmxlZBIxChVkZWJ1Z19sb2dfYXBpX2VuYWJsZWQYBiABKAhSEmRlYnVnTG9nQXBpRW' + '5hYmxlZBIyChVhZG1pbl9jaGFubmVsX2VuYWJsZWQYCCABKAhSE2FkbWluQ2hhbm5lbEVuYWJs' + 'ZWQaEgoQU2Vzc2lvbmtleUNvbmZpZ0IRCg9wYXlsb2FkX3ZhcmlhbnQ='); diff --git a/third_party/meshtastic_flutter/lib/generated/connection_status.pb.dart b/third_party/meshtastic_flutter/lib/generated/connection_status.pb.dart new file mode 100644 index 000000000..c3daead81 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/connection_status.pb.dart @@ -0,0 +1,563 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/connection_status.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +class DeviceConnectionStatus extends $pb.GeneratedMessage { + factory DeviceConnectionStatus({ + WifiConnectionStatus? wifi, + EthernetConnectionStatus? ethernet, + BluetoothConnectionStatus? bluetooth, + SerialConnectionStatus? serial, + }) { + final result = create(); + if (wifi != null) result.wifi = wifi; + if (ethernet != null) result.ethernet = ethernet; + if (bluetooth != null) result.bluetooth = bluetooth; + if (serial != null) result.serial = serial; + return result; + } + + DeviceConnectionStatus._(); + + factory DeviceConnectionStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeviceConnectionStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeviceConnectionStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'wifi', + subBuilder: WifiConnectionStatus.create) + ..aOM(2, _omitFieldNames ? '' : 'ethernet', + subBuilder: EthernetConnectionStatus.create) + ..aOM(3, _omitFieldNames ? '' : 'bluetooth', + subBuilder: BluetoothConnectionStatus.create) + ..aOM(4, _omitFieldNames ? '' : 'serial', + subBuilder: SerialConnectionStatus.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceConnectionStatus clone() => + DeviceConnectionStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceConnectionStatus copyWith( + void Function(DeviceConnectionStatus) updates) => + super.copyWith((message) => updates(message as DeviceConnectionStatus)) + as DeviceConnectionStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceConnectionStatus create() => DeviceConnectionStatus._(); + @$core.override + DeviceConnectionStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceConnectionStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeviceConnectionStatus? _defaultInstance; + + /// + /// WiFi Status + @$pb.TagNumber(1) + WifiConnectionStatus get wifi => $_getN(0); + @$pb.TagNumber(1) + set wifi(WifiConnectionStatus value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasWifi() => $_has(0); + @$pb.TagNumber(1) + void clearWifi() => $_clearField(1); + @$pb.TagNumber(1) + WifiConnectionStatus ensureWifi() => $_ensure(0); + + /// + /// WiFi Status + @$pb.TagNumber(2) + EthernetConnectionStatus get ethernet => $_getN(1); + @$pb.TagNumber(2) + set ethernet(EthernetConnectionStatus value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasEthernet() => $_has(1); + @$pb.TagNumber(2) + void clearEthernet() => $_clearField(2); + @$pb.TagNumber(2) + EthernetConnectionStatus ensureEthernet() => $_ensure(1); + + /// + /// Bluetooth Status + @$pb.TagNumber(3) + BluetoothConnectionStatus get bluetooth => $_getN(2); + @$pb.TagNumber(3) + set bluetooth(BluetoothConnectionStatus value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasBluetooth() => $_has(2); + @$pb.TagNumber(3) + void clearBluetooth() => $_clearField(3); + @$pb.TagNumber(3) + BluetoothConnectionStatus ensureBluetooth() => $_ensure(2); + + /// + /// Serial Status + @$pb.TagNumber(4) + SerialConnectionStatus get serial => $_getN(3); + @$pb.TagNumber(4) + set serial(SerialConnectionStatus value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasSerial() => $_has(3); + @$pb.TagNumber(4) + void clearSerial() => $_clearField(4); + @$pb.TagNumber(4) + SerialConnectionStatus ensureSerial() => $_ensure(3); +} + +/// +/// WiFi connection status +class WifiConnectionStatus extends $pb.GeneratedMessage { + factory WifiConnectionStatus({ + NetworkConnectionStatus? status, + $core.String? ssid, + $core.int? rssi, + }) { + final result = create(); + if (status != null) result.status = status; + if (ssid != null) result.ssid = ssid; + if (rssi != null) result.rssi = rssi; + return result; + } + + WifiConnectionStatus._(); + + factory WifiConnectionStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory WifiConnectionStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'WifiConnectionStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'status', + subBuilder: NetworkConnectionStatus.create) + ..aOS(2, _omitFieldNames ? '' : 'ssid') + ..a<$core.int>(3, _omitFieldNames ? '' : 'rssi', $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WifiConnectionStatus clone() => + WifiConnectionStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + WifiConnectionStatus copyWith(void Function(WifiConnectionStatus) updates) => + super.copyWith((message) => updates(message as WifiConnectionStatus)) + as WifiConnectionStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiConnectionStatus create() => WifiConnectionStatus._(); + @$core.override + WifiConnectionStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiConnectionStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static WifiConnectionStatus? _defaultInstance; + + /// + /// Connection status + @$pb.TagNumber(1) + NetworkConnectionStatus get status => $_getN(0); + @$pb.TagNumber(1) + set status(NetworkConnectionStatus value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasStatus() => $_has(0); + @$pb.TagNumber(1) + void clearStatus() => $_clearField(1); + @$pb.TagNumber(1) + NetworkConnectionStatus ensureStatus() => $_ensure(0); + + /// + /// WiFi access point SSID + @$pb.TagNumber(2) + $core.String get ssid => $_getSZ(1); + @$pb.TagNumber(2) + set ssid($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasSsid() => $_has(1); + @$pb.TagNumber(2) + void clearSsid() => $_clearField(2); + + /// + /// RSSI of wireless connection + @$pb.TagNumber(3) + $core.int get rssi => $_getIZ(2); + @$pb.TagNumber(3) + set rssi($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasRssi() => $_has(2); + @$pb.TagNumber(3) + void clearRssi() => $_clearField(3); +} + +/// +/// Ethernet connection status +class EthernetConnectionStatus extends $pb.GeneratedMessage { + factory EthernetConnectionStatus({ + NetworkConnectionStatus? status, + }) { + final result = create(); + if (status != null) result.status = status; + return result; + } + + EthernetConnectionStatus._(); + + factory EthernetConnectionStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory EthernetConnectionStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'EthernetConnectionStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'status', + subBuilder: NetworkConnectionStatus.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EthernetConnectionStatus clone() => + EthernetConnectionStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EthernetConnectionStatus copyWith( + void Function(EthernetConnectionStatus) updates) => + super.copyWith((message) => updates(message as EthernetConnectionStatus)) + as EthernetConnectionStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EthernetConnectionStatus create() => EthernetConnectionStatus._(); + @$core.override + EthernetConnectionStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EthernetConnectionStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static EthernetConnectionStatus? _defaultInstance; + + /// + /// Connection status + @$pb.TagNumber(1) + NetworkConnectionStatus get status => $_getN(0); + @$pb.TagNumber(1) + set status(NetworkConnectionStatus value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasStatus() => $_has(0); + @$pb.TagNumber(1) + void clearStatus() => $_clearField(1); + @$pb.TagNumber(1) + NetworkConnectionStatus ensureStatus() => $_ensure(0); +} + +/// +/// Ethernet or WiFi connection status +class NetworkConnectionStatus extends $pb.GeneratedMessage { + factory NetworkConnectionStatus({ + $core.int? ipAddress, + $core.bool? isConnected, + $core.bool? isMqttConnected, + $core.bool? isSyslogConnected, + }) { + final result = create(); + if (ipAddress != null) result.ipAddress = ipAddress; + if (isConnected != null) result.isConnected = isConnected; + if (isMqttConnected != null) result.isMqttConnected = isMqttConnected; + if (isSyslogConnected != null) result.isSyslogConnected = isSyslogConnected; + return result; + } + + NetworkConnectionStatus._(); + + factory NetworkConnectionStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NetworkConnectionStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NetworkConnectionStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'ipAddress', $pb.PbFieldType.OF3) + ..aOB(2, _omitFieldNames ? '' : 'isConnected') + ..aOB(3, _omitFieldNames ? '' : 'isMqttConnected') + ..aOB(4, _omitFieldNames ? '' : 'isSyslogConnected') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NetworkConnectionStatus clone() => + NetworkConnectionStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NetworkConnectionStatus copyWith( + void Function(NetworkConnectionStatus) updates) => + super.copyWith((message) => updates(message as NetworkConnectionStatus)) + as NetworkConnectionStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NetworkConnectionStatus create() => NetworkConnectionStatus._(); + @$core.override + NetworkConnectionStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NetworkConnectionStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NetworkConnectionStatus? _defaultInstance; + + /// + /// IP address of device + @$pb.TagNumber(1) + $core.int get ipAddress => $_getIZ(0); + @$pb.TagNumber(1) + set ipAddress($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasIpAddress() => $_has(0); + @$pb.TagNumber(1) + void clearIpAddress() => $_clearField(1); + + /// + /// Whether the device has an active connection or not + @$pb.TagNumber(2) + $core.bool get isConnected => $_getBF(1); + @$pb.TagNumber(2) + set isConnected($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasIsConnected() => $_has(1); + @$pb.TagNumber(2) + void clearIsConnected() => $_clearField(2); + + /// + /// Whether the device has an active connection to an MQTT broker or not + @$pb.TagNumber(3) + $core.bool get isMqttConnected => $_getBF(2); + @$pb.TagNumber(3) + set isMqttConnected($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasIsMqttConnected() => $_has(2); + @$pb.TagNumber(3) + void clearIsMqttConnected() => $_clearField(3); + + /// + /// Whether the device is actively remote syslogging or not + @$pb.TagNumber(4) + $core.bool get isSyslogConnected => $_getBF(3); + @$pb.TagNumber(4) + set isSyslogConnected($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasIsSyslogConnected() => $_has(3); + @$pb.TagNumber(4) + void clearIsSyslogConnected() => $_clearField(4); +} + +/// +/// Bluetooth connection status +class BluetoothConnectionStatus extends $pb.GeneratedMessage { + factory BluetoothConnectionStatus({ + $core.int? pin, + $core.int? rssi, + $core.bool? isConnected, + }) { + final result = create(); + if (pin != null) result.pin = pin; + if (rssi != null) result.rssi = rssi; + if (isConnected != null) result.isConnected = isConnected; + return result; + } + + BluetoothConnectionStatus._(); + + factory BluetoothConnectionStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory BluetoothConnectionStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'BluetoothConnectionStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'pin', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'rssi', $pb.PbFieldType.O3) + ..aOB(3, _omitFieldNames ? '' : 'isConnected') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + BluetoothConnectionStatus clone() => + BluetoothConnectionStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + BluetoothConnectionStatus copyWith( + void Function(BluetoothConnectionStatus) updates) => + super.copyWith((message) => updates(message as BluetoothConnectionStatus)) + as BluetoothConnectionStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BluetoothConnectionStatus create() => BluetoothConnectionStatus._(); + @$core.override + BluetoothConnectionStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BluetoothConnectionStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static BluetoothConnectionStatus? _defaultInstance; + + /// + /// The pairing PIN for bluetooth + @$pb.TagNumber(1) + $core.int get pin => $_getIZ(0); + @$pb.TagNumber(1) + set pin($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPin() => $_has(0); + @$pb.TagNumber(1) + void clearPin() => $_clearField(1); + + /// + /// RSSI of bluetooth connection + @$pb.TagNumber(2) + $core.int get rssi => $_getIZ(1); + @$pb.TagNumber(2) + set rssi($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasRssi() => $_has(1); + @$pb.TagNumber(2) + void clearRssi() => $_clearField(2); + + /// + /// Whether the device has an active connection or not + @$pb.TagNumber(3) + $core.bool get isConnected => $_getBF(2); + @$pb.TagNumber(3) + set isConnected($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasIsConnected() => $_has(2); + @$pb.TagNumber(3) + void clearIsConnected() => $_clearField(3); +} + +/// +/// Serial connection status +class SerialConnectionStatus extends $pb.GeneratedMessage { + factory SerialConnectionStatus({ + $core.int? baud, + $core.bool? isConnected, + }) { + final result = create(); + if (baud != null) result.baud = baud; + if (isConnected != null) result.isConnected = isConnected; + return result; + } + + SerialConnectionStatus._(); + + factory SerialConnectionStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SerialConnectionStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SerialConnectionStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'baud', $pb.PbFieldType.OU3) + ..aOB(2, _omitFieldNames ? '' : 'isConnected') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SerialConnectionStatus clone() => + SerialConnectionStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SerialConnectionStatus copyWith( + void Function(SerialConnectionStatus) updates) => + super.copyWith((message) => updates(message as SerialConnectionStatus)) + as SerialConnectionStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SerialConnectionStatus create() => SerialConnectionStatus._(); + @$core.override + SerialConnectionStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SerialConnectionStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SerialConnectionStatus? _defaultInstance; + + /// + /// Serial baud rate + @$pb.TagNumber(1) + $core.int get baud => $_getIZ(0); + @$pb.TagNumber(1) + set baud($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasBaud() => $_has(0); + @$pb.TagNumber(1) + void clearBaud() => $_clearField(1); + + /// + /// Whether the device has an active connection or not + @$pb.TagNumber(2) + $core.bool get isConnected => $_getBF(1); + @$pb.TagNumber(2) + set isConnected($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasIsConnected() => $_has(1); + @$pb.TagNumber(2) + void clearIsConnected() => $_clearField(2); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/connection_status.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/connection_status.pbenum.dart new file mode 100644 index 000000000..2d63a1f04 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/connection_status.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/connection_status.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/connection_status.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/connection_status.pbjson.dart new file mode 100644 index 000000000..91b6236af --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/connection_status.pbjson.dart @@ -0,0 +1,177 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/connection_status.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use deviceConnectionStatusDescriptor instead') +const DeviceConnectionStatus$json = { + '1': 'DeviceConnectionStatus', + '2': [ + { + '1': 'wifi', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.WifiConnectionStatus', + '9': 0, + '10': 'wifi', + '17': true + }, + { + '1': 'ethernet', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.EthernetConnectionStatus', + '9': 1, + '10': 'ethernet', + '17': true + }, + { + '1': 'bluetooth', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.BluetoothConnectionStatus', + '9': 2, + '10': 'bluetooth', + '17': true + }, + { + '1': 'serial', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.SerialConnectionStatus', + '9': 3, + '10': 'serial', + '17': true + }, + ], + '8': [ + {'1': '_wifi'}, + {'1': '_ethernet'}, + {'1': '_bluetooth'}, + {'1': '_serial'}, + ], +}; + +/// Descriptor for `DeviceConnectionStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceConnectionStatusDescriptor = $convert.base64Decode( + 'ChZEZXZpY2VDb25uZWN0aW9uU3RhdHVzEjkKBHdpZmkYASABKAsyIC5tZXNodGFzdGljLldpZm' + 'lDb25uZWN0aW9uU3RhdHVzSABSBHdpZmmIAQESRQoIZXRoZXJuZXQYAiABKAsyJC5tZXNodGFz' + 'dGljLkV0aGVybmV0Q29ubmVjdGlvblN0YXR1c0gBUghldGhlcm5ldIgBARJICglibHVldG9vdG' + 'gYAyABKAsyJS5tZXNodGFzdGljLkJsdWV0b290aENvbm5lY3Rpb25TdGF0dXNIAlIJYmx1ZXRv' + 'b3RoiAEBEj8KBnNlcmlhbBgEIAEoCzIiLm1lc2h0YXN0aWMuU2VyaWFsQ29ubmVjdGlvblN0YX' + 'R1c0gDUgZzZXJpYWyIAQFCBwoFX3dpZmlCCwoJX2V0aGVybmV0QgwKCl9ibHVldG9vdGhCCQoH' + 'X3NlcmlhbA=='); + +@$core.Deprecated('Use wifiConnectionStatusDescriptor instead') +const WifiConnectionStatus$json = { + '1': 'WifiConnectionStatus', + '2': [ + { + '1': 'status', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.NetworkConnectionStatus', + '10': 'status' + }, + {'1': 'ssid', '3': 2, '4': 1, '5': 9, '10': 'ssid'}, + {'1': 'rssi', '3': 3, '4': 1, '5': 5, '10': 'rssi'}, + ], +}; + +/// Descriptor for `WifiConnectionStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiConnectionStatusDescriptor = $convert.base64Decode( + 'ChRXaWZpQ29ubmVjdGlvblN0YXR1cxI7CgZzdGF0dXMYASABKAsyIy5tZXNodGFzdGljLk5ldH' + 'dvcmtDb25uZWN0aW9uU3RhdHVzUgZzdGF0dXMSEgoEc3NpZBgCIAEoCVIEc3NpZBISCgRyc3Np' + 'GAMgASgFUgRyc3Np'); + +@$core.Deprecated('Use ethernetConnectionStatusDescriptor instead') +const EthernetConnectionStatus$json = { + '1': 'EthernetConnectionStatus', + '2': [ + { + '1': 'status', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.NetworkConnectionStatus', + '10': 'status' + }, + ], +}; + +/// Descriptor for `EthernetConnectionStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List ethernetConnectionStatusDescriptor = + $convert.base64Decode( + 'ChhFdGhlcm5ldENvbm5lY3Rpb25TdGF0dXMSOwoGc3RhdHVzGAEgASgLMiMubWVzaHRhc3RpYy' + '5OZXR3b3JrQ29ubmVjdGlvblN0YXR1c1IGc3RhdHVz'); + +@$core.Deprecated('Use networkConnectionStatusDescriptor instead') +const NetworkConnectionStatus$json = { + '1': 'NetworkConnectionStatus', + '2': [ + {'1': 'ip_address', '3': 1, '4': 1, '5': 7, '10': 'ipAddress'}, + {'1': 'is_connected', '3': 2, '4': 1, '5': 8, '10': 'isConnected'}, + {'1': 'is_mqtt_connected', '3': 3, '4': 1, '5': 8, '10': 'isMqttConnected'}, + { + '1': 'is_syslog_connected', + '3': 4, + '4': 1, + '5': 8, + '10': 'isSyslogConnected' + }, + ], +}; + +/// Descriptor for `NetworkConnectionStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List networkConnectionStatusDescriptor = $convert.base64Decode( + 'ChdOZXR3b3JrQ29ubmVjdGlvblN0YXR1cxIdCgppcF9hZGRyZXNzGAEgASgHUglpcEFkZHJlc3' + 'MSIQoMaXNfY29ubmVjdGVkGAIgASgIUgtpc0Nvbm5lY3RlZBIqChFpc19tcXR0X2Nvbm5lY3Rl' + 'ZBgDIAEoCFIPaXNNcXR0Q29ubmVjdGVkEi4KE2lzX3N5c2xvZ19jb25uZWN0ZWQYBCABKAhSEW' + 'lzU3lzbG9nQ29ubmVjdGVk'); + +@$core.Deprecated('Use bluetoothConnectionStatusDescriptor instead') +const BluetoothConnectionStatus$json = { + '1': 'BluetoothConnectionStatus', + '2': [ + {'1': 'pin', '3': 1, '4': 1, '5': 13, '10': 'pin'}, + {'1': 'rssi', '3': 2, '4': 1, '5': 5, '10': 'rssi'}, + {'1': 'is_connected', '3': 3, '4': 1, '5': 8, '10': 'isConnected'}, + ], +}; + +/// Descriptor for `BluetoothConnectionStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List bluetoothConnectionStatusDescriptor = + $convert.base64Decode( + 'ChlCbHVldG9vdGhDb25uZWN0aW9uU3RhdHVzEhAKA3BpbhgBIAEoDVIDcGluEhIKBHJzc2kYAi' + 'ABKAVSBHJzc2kSIQoMaXNfY29ubmVjdGVkGAMgASgIUgtpc0Nvbm5lY3RlZA=='); + +@$core.Deprecated('Use serialConnectionStatusDescriptor instead') +const SerialConnectionStatus$json = { + '1': 'SerialConnectionStatus', + '2': [ + {'1': 'baud', '3': 1, '4': 1, '5': 13, '10': 'baud'}, + {'1': 'is_connected', '3': 2, '4': 1, '5': 8, '10': 'isConnected'}, + ], +}; + +/// Descriptor for `SerialConnectionStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List serialConnectionStatusDescriptor = + $convert.base64Decode( + 'ChZTZXJpYWxDb25uZWN0aW9uU3RhdHVzEhIKBGJhdWQYASABKA1SBGJhdWQSIQoMaXNfY29ubm' + 'VjdGVkGAIgASgIUgtpc0Nvbm5lY3RlZA=='); diff --git a/third_party/meshtastic_flutter/lib/generated/device_ui.pb.dart b/third_party/meshtastic_flutter/lib/generated/device_ui.pb.dart new file mode 100644 index 000000000..23a5fe93e --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/device_ui.pb.dart @@ -0,0 +1,763 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/device_ui.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'device_ui.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'device_ui.pbenum.dart'; + +class DeviceUIConfig extends $pb.GeneratedMessage { + factory DeviceUIConfig({ + $core.int? version, + $core.int? screenBrightness, + $core.int? screenTimeout, + $core.bool? screenLock, + $core.bool? settingsLock, + $core.int? pinCode, + Theme? theme, + $core.bool? alertEnabled, + $core.bool? bannerEnabled, + $core.int? ringToneId, + Language? language, + NodeFilter? nodeFilter, + NodeHighlight? nodeHighlight, + $core.List<$core.int>? calibrationData, + Map_? mapData, + CompassMode? compassMode, + $core.int? screenRgbColor, + $core.bool? isClockfaceAnalog, + }) { + final result = create(); + if (version != null) result.version = version; + if (screenBrightness != null) result.screenBrightness = screenBrightness; + if (screenTimeout != null) result.screenTimeout = screenTimeout; + if (screenLock != null) result.screenLock = screenLock; + if (settingsLock != null) result.settingsLock = settingsLock; + if (pinCode != null) result.pinCode = pinCode; + if (theme != null) result.theme = theme; + if (alertEnabled != null) result.alertEnabled = alertEnabled; + if (bannerEnabled != null) result.bannerEnabled = bannerEnabled; + if (ringToneId != null) result.ringToneId = ringToneId; + if (language != null) result.language = language; + if (nodeFilter != null) result.nodeFilter = nodeFilter; + if (nodeHighlight != null) result.nodeHighlight = nodeHighlight; + if (calibrationData != null) result.calibrationData = calibrationData; + if (mapData != null) result.mapData = mapData; + if (compassMode != null) result.compassMode = compassMode; + if (screenRgbColor != null) result.screenRgbColor = screenRgbColor; + if (isClockfaceAnalog != null) result.isClockfaceAnalog = isClockfaceAnalog; + return result; + } + + DeviceUIConfig._(); + + factory DeviceUIConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeviceUIConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeviceUIConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'screenBrightness', $pb.PbFieldType.OU3) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'screenTimeout', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'screenLock') + ..aOB(5, _omitFieldNames ? '' : 'settingsLock') + ..a<$core.int>(6, _omitFieldNames ? '' : 'pinCode', $pb.PbFieldType.OU3) + ..e(7, _omitFieldNames ? '' : 'theme', $pb.PbFieldType.OE, + defaultOrMaker: Theme.DARK, + valueOf: Theme.valueOf, + enumValues: Theme.values) + ..aOB(8, _omitFieldNames ? '' : 'alertEnabled') + ..aOB(9, _omitFieldNames ? '' : 'bannerEnabled') + ..a<$core.int>(10, _omitFieldNames ? '' : 'ringToneId', $pb.PbFieldType.OU3) + ..e(11, _omitFieldNames ? '' : 'language', $pb.PbFieldType.OE, + defaultOrMaker: Language.ENGLISH, + valueOf: Language.valueOf, + enumValues: Language.values) + ..aOM(12, _omitFieldNames ? '' : 'nodeFilter', + subBuilder: NodeFilter.create) + ..aOM(13, _omitFieldNames ? '' : 'nodeHighlight', + subBuilder: NodeHighlight.create) + ..a<$core.List<$core.int>>( + 14, _omitFieldNames ? '' : 'calibrationData', $pb.PbFieldType.OY) + ..aOM(15, _omitFieldNames ? '' : 'mapData', subBuilder: Map_.create) + ..e( + 16, _omitFieldNames ? '' : 'compassMode', $pb.PbFieldType.OE, + defaultOrMaker: CompassMode.DYNAMIC, + valueOf: CompassMode.valueOf, + enumValues: CompassMode.values) + ..a<$core.int>( + 17, _omitFieldNames ? '' : 'screenRgbColor', $pb.PbFieldType.OU3) + ..aOB(18, _omitFieldNames ? '' : 'isClockfaceAnalog') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceUIConfig clone() => DeviceUIConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceUIConfig copyWith(void Function(DeviceUIConfig) updates) => + super.copyWith((message) => updates(message as DeviceUIConfig)) + as DeviceUIConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceUIConfig create() => DeviceUIConfig._(); + @$core.override + DeviceUIConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceUIConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeviceUIConfig? _defaultInstance; + + /// + /// A version integer used to invalidate saved files when we make incompatible changes. + @$pb.TagNumber(1) + $core.int get version => $_getIZ(0); + @$pb.TagNumber(1) + set version($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasVersion() => $_has(0); + @$pb.TagNumber(1) + void clearVersion() => $_clearField(1); + + /// + /// TFT display brightness 1..255 + @$pb.TagNumber(2) + $core.int get screenBrightness => $_getIZ(1); + @$pb.TagNumber(2) + set screenBrightness($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasScreenBrightness() => $_has(1); + @$pb.TagNumber(2) + void clearScreenBrightness() => $_clearField(2); + + /// + /// Screen timeout 0..900 + @$pb.TagNumber(3) + $core.int get screenTimeout => $_getIZ(2); + @$pb.TagNumber(3) + set screenTimeout($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasScreenTimeout() => $_has(2); + @$pb.TagNumber(3) + void clearScreenTimeout() => $_clearField(3); + + /// + /// Screen/Settings lock enabled + @$pb.TagNumber(4) + $core.bool get screenLock => $_getBF(3); + @$pb.TagNumber(4) + set screenLock($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasScreenLock() => $_has(3); + @$pb.TagNumber(4) + void clearScreenLock() => $_clearField(4); + + @$pb.TagNumber(5) + $core.bool get settingsLock => $_getBF(4); + @$pb.TagNumber(5) + set settingsLock($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasSettingsLock() => $_has(4); + @$pb.TagNumber(5) + void clearSettingsLock() => $_clearField(5); + + @$pb.TagNumber(6) + $core.int get pinCode => $_getIZ(5); + @$pb.TagNumber(6) + set pinCode($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasPinCode() => $_has(5); + @$pb.TagNumber(6) + void clearPinCode() => $_clearField(6); + + /// + /// Color theme + @$pb.TagNumber(7) + Theme get theme => $_getN(6); + @$pb.TagNumber(7) + set theme(Theme value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasTheme() => $_has(6); + @$pb.TagNumber(7) + void clearTheme() => $_clearField(7); + + /// + /// Audible message, banner and ring tone + @$pb.TagNumber(8) + $core.bool get alertEnabled => $_getBF(7); + @$pb.TagNumber(8) + set alertEnabled($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasAlertEnabled() => $_has(7); + @$pb.TagNumber(8) + void clearAlertEnabled() => $_clearField(8); + + @$pb.TagNumber(9) + $core.bool get bannerEnabled => $_getBF(8); + @$pb.TagNumber(9) + set bannerEnabled($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasBannerEnabled() => $_has(8); + @$pb.TagNumber(9) + void clearBannerEnabled() => $_clearField(9); + + @$pb.TagNumber(10) + $core.int get ringToneId => $_getIZ(9); + @$pb.TagNumber(10) + set ringToneId($core.int value) => $_setUnsignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasRingToneId() => $_has(9); + @$pb.TagNumber(10) + void clearRingToneId() => $_clearField(10); + + /// + /// Localization + @$pb.TagNumber(11) + Language get language => $_getN(10); + @$pb.TagNumber(11) + set language(Language value) => $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasLanguage() => $_has(10); + @$pb.TagNumber(11) + void clearLanguage() => $_clearField(11); + + /// + /// Node list filter + @$pb.TagNumber(12) + NodeFilter get nodeFilter => $_getN(11); + @$pb.TagNumber(12) + set nodeFilter(NodeFilter value) => $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasNodeFilter() => $_has(11); + @$pb.TagNumber(12) + void clearNodeFilter() => $_clearField(12); + @$pb.TagNumber(12) + NodeFilter ensureNodeFilter() => $_ensure(11); + + /// + /// Node list highlightening + @$pb.TagNumber(13) + NodeHighlight get nodeHighlight => $_getN(12); + @$pb.TagNumber(13) + set nodeHighlight(NodeHighlight value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasNodeHighlight() => $_has(12); + @$pb.TagNumber(13) + void clearNodeHighlight() => $_clearField(13); + @$pb.TagNumber(13) + NodeHighlight ensureNodeHighlight() => $_ensure(12); + + /// + /// 8 integers for screen calibration data + @$pb.TagNumber(14) + $core.List<$core.int> get calibrationData => $_getN(13); + @$pb.TagNumber(14) + set calibrationData($core.List<$core.int> value) => $_setBytes(13, value); + @$pb.TagNumber(14) + $core.bool hasCalibrationData() => $_has(13); + @$pb.TagNumber(14) + void clearCalibrationData() => $_clearField(14); + + /// + /// Map related data + @$pb.TagNumber(15) + Map_ get mapData => $_getN(14); + @$pb.TagNumber(15) + set mapData(Map_ value) => $_setField(15, value); + @$pb.TagNumber(15) + $core.bool hasMapData() => $_has(14); + @$pb.TagNumber(15) + void clearMapData() => $_clearField(15); + @$pb.TagNumber(15) + Map_ ensureMapData() => $_ensure(14); + + /// + /// Compass mode + @$pb.TagNumber(16) + CompassMode get compassMode => $_getN(15); + @$pb.TagNumber(16) + set compassMode(CompassMode value) => $_setField(16, value); + @$pb.TagNumber(16) + $core.bool hasCompassMode() => $_has(15); + @$pb.TagNumber(16) + void clearCompassMode() => $_clearField(16); + + /// + /// RGB color for BaseUI + /// 0xRRGGBB format, e.g. 0xFF0000 for red + @$pb.TagNumber(17) + $core.int get screenRgbColor => $_getIZ(16); + @$pb.TagNumber(17) + set screenRgbColor($core.int value) => $_setUnsignedInt32(16, value); + @$pb.TagNumber(17) + $core.bool hasScreenRgbColor() => $_has(16); + @$pb.TagNumber(17) + void clearScreenRgbColor() => $_clearField(17); + + /// + /// Clockface analog style + /// true for analog clockface, false for digital clockface + @$pb.TagNumber(18) + $core.bool get isClockfaceAnalog => $_getBF(17); + @$pb.TagNumber(18) + set isClockfaceAnalog($core.bool value) => $_setBool(17, value); + @$pb.TagNumber(18) + $core.bool hasIsClockfaceAnalog() => $_has(17); + @$pb.TagNumber(18) + void clearIsClockfaceAnalog() => $_clearField(18); +} + +class NodeFilter extends $pb.GeneratedMessage { + factory NodeFilter({ + $core.bool? unknownSwitch, + $core.bool? offlineSwitch, + $core.bool? publicKeySwitch, + $core.int? hopsAway, + $core.bool? positionSwitch, + $core.String? nodeName, + $core.int? channel, + }) { + final result = create(); + if (unknownSwitch != null) result.unknownSwitch = unknownSwitch; + if (offlineSwitch != null) result.offlineSwitch = offlineSwitch; + if (publicKeySwitch != null) result.publicKeySwitch = publicKeySwitch; + if (hopsAway != null) result.hopsAway = hopsAway; + if (positionSwitch != null) result.positionSwitch = positionSwitch; + if (nodeName != null) result.nodeName = nodeName; + if (channel != null) result.channel = channel; + return result; + } + + NodeFilter._(); + + factory NodeFilter.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeFilter.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeFilter', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'unknownSwitch') + ..aOB(2, _omitFieldNames ? '' : 'offlineSwitch') + ..aOB(3, _omitFieldNames ? '' : 'publicKeySwitch') + ..a<$core.int>(4, _omitFieldNames ? '' : 'hopsAway', $pb.PbFieldType.O3) + ..aOB(5, _omitFieldNames ? '' : 'positionSwitch') + ..aOS(6, _omitFieldNames ? '' : 'nodeName') + ..a<$core.int>(7, _omitFieldNames ? '' : 'channel', $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeFilter clone() => NodeFilter()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeFilter copyWith(void Function(NodeFilter) updates) => + super.copyWith((message) => updates(message as NodeFilter)) as NodeFilter; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeFilter create() => NodeFilter._(); + @$core.override + NodeFilter createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeFilter getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeFilter? _defaultInstance; + + /// + /// Filter unknown nodes + @$pb.TagNumber(1) + $core.bool get unknownSwitch => $_getBF(0); + @$pb.TagNumber(1) + set unknownSwitch($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasUnknownSwitch() => $_has(0); + @$pb.TagNumber(1) + void clearUnknownSwitch() => $_clearField(1); + + /// + /// Filter offline nodes + @$pb.TagNumber(2) + $core.bool get offlineSwitch => $_getBF(1); + @$pb.TagNumber(2) + set offlineSwitch($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasOfflineSwitch() => $_has(1); + @$pb.TagNumber(2) + void clearOfflineSwitch() => $_clearField(2); + + /// + /// Filter nodes w/o public key + @$pb.TagNumber(3) + $core.bool get publicKeySwitch => $_getBF(2); + @$pb.TagNumber(3) + set publicKeySwitch($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasPublicKeySwitch() => $_has(2); + @$pb.TagNumber(3) + void clearPublicKeySwitch() => $_clearField(3); + + /// + /// Filter based on hops away + @$pb.TagNumber(4) + $core.int get hopsAway => $_getIZ(3); + @$pb.TagNumber(4) + set hopsAway($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasHopsAway() => $_has(3); + @$pb.TagNumber(4) + void clearHopsAway() => $_clearField(4); + + /// + /// Filter nodes w/o position + @$pb.TagNumber(5) + $core.bool get positionSwitch => $_getBF(4); + @$pb.TagNumber(5) + set positionSwitch($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasPositionSwitch() => $_has(4); + @$pb.TagNumber(5) + void clearPositionSwitch() => $_clearField(5); + + /// + /// Filter nodes by matching name string + @$pb.TagNumber(6) + $core.String get nodeName => $_getSZ(5); + @$pb.TagNumber(6) + set nodeName($core.String value) => $_setString(5, value); + @$pb.TagNumber(6) + $core.bool hasNodeName() => $_has(5); + @$pb.TagNumber(6) + void clearNodeName() => $_clearField(6); + + /// + /// Filter based on channel + @$pb.TagNumber(7) + $core.int get channel => $_getIZ(6); + @$pb.TagNumber(7) + set channel($core.int value) => $_setSignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasChannel() => $_has(6); + @$pb.TagNumber(7) + void clearChannel() => $_clearField(7); +} + +class NodeHighlight extends $pb.GeneratedMessage { + factory NodeHighlight({ + $core.bool? chatSwitch, + $core.bool? positionSwitch, + $core.bool? telemetrySwitch, + $core.bool? iaqSwitch, + $core.String? nodeName, + }) { + final result = create(); + if (chatSwitch != null) result.chatSwitch = chatSwitch; + if (positionSwitch != null) result.positionSwitch = positionSwitch; + if (telemetrySwitch != null) result.telemetrySwitch = telemetrySwitch; + if (iaqSwitch != null) result.iaqSwitch = iaqSwitch; + if (nodeName != null) result.nodeName = nodeName; + return result; + } + + NodeHighlight._(); + + factory NodeHighlight.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeHighlight.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeHighlight', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'chatSwitch') + ..aOB(2, _omitFieldNames ? '' : 'positionSwitch') + ..aOB(3, _omitFieldNames ? '' : 'telemetrySwitch') + ..aOB(4, _omitFieldNames ? '' : 'iaqSwitch') + ..aOS(5, _omitFieldNames ? '' : 'nodeName') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeHighlight clone() => NodeHighlight()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeHighlight copyWith(void Function(NodeHighlight) updates) => + super.copyWith((message) => updates(message as NodeHighlight)) + as NodeHighlight; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeHighlight create() => NodeHighlight._(); + @$core.override + NodeHighlight createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeHighlight getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeHighlight? _defaultInstance; + + /// + /// Hightlight nodes w/ active chat + @$pb.TagNumber(1) + $core.bool get chatSwitch => $_getBF(0); + @$pb.TagNumber(1) + set chatSwitch($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasChatSwitch() => $_has(0); + @$pb.TagNumber(1) + void clearChatSwitch() => $_clearField(1); + + /// + /// Highlight nodes w/ position + @$pb.TagNumber(2) + $core.bool get positionSwitch => $_getBF(1); + @$pb.TagNumber(2) + set positionSwitch($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasPositionSwitch() => $_has(1); + @$pb.TagNumber(2) + void clearPositionSwitch() => $_clearField(2); + + /// + /// Highlight nodes w/ telemetry data + @$pb.TagNumber(3) + $core.bool get telemetrySwitch => $_getBF(2); + @$pb.TagNumber(3) + set telemetrySwitch($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasTelemetrySwitch() => $_has(2); + @$pb.TagNumber(3) + void clearTelemetrySwitch() => $_clearField(3); + + /// + /// Highlight nodes w/ iaq data + @$pb.TagNumber(4) + $core.bool get iaqSwitch => $_getBF(3); + @$pb.TagNumber(4) + set iaqSwitch($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasIaqSwitch() => $_has(3); + @$pb.TagNumber(4) + void clearIaqSwitch() => $_clearField(4); + + /// + /// Highlight nodes by matching name string + @$pb.TagNumber(5) + $core.String get nodeName => $_getSZ(4); + @$pb.TagNumber(5) + set nodeName($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasNodeName() => $_has(4); + @$pb.TagNumber(5) + void clearNodeName() => $_clearField(5); +} + +class GeoPoint extends $pb.GeneratedMessage { + factory GeoPoint({ + $core.int? zoom, + $core.int? latitude, + $core.int? longitude, + }) { + final result = create(); + if (zoom != null) result.zoom = zoom; + if (latitude != null) result.latitude = latitude; + if (longitude != null) result.longitude = longitude; + return result; + } + + GeoPoint._(); + + factory GeoPoint.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory GeoPoint.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GeoPoint', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'zoom', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'latitude', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'longitude', $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GeoPoint clone() => GeoPoint()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + GeoPoint copyWith(void Function(GeoPoint) updates) => + super.copyWith((message) => updates(message as GeoPoint)) as GeoPoint; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GeoPoint create() => GeoPoint._(); + @$core.override + GeoPoint createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GeoPoint getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GeoPoint? _defaultInstance; + + /// + /// Zoom level + @$pb.TagNumber(1) + $core.int get zoom => $_getIZ(0); + @$pb.TagNumber(1) + set zoom($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasZoom() => $_has(0); + @$pb.TagNumber(1) + void clearZoom() => $_clearField(1); + + /// + /// Coordinate: latitude + @$pb.TagNumber(2) + $core.int get latitude => $_getIZ(1); + @$pb.TagNumber(2) + set latitude($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasLatitude() => $_has(1); + @$pb.TagNumber(2) + void clearLatitude() => $_clearField(2); + + /// + /// Coordinate: longitude + @$pb.TagNumber(3) + $core.int get longitude => $_getIZ(2); + @$pb.TagNumber(3) + set longitude($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasLongitude() => $_has(2); + @$pb.TagNumber(3) + void clearLongitude() => $_clearField(3); +} + +class Map_ extends $pb.GeneratedMessage { + factory Map_({ + GeoPoint? home, + $core.String? style, + $core.bool? followGps, + }) { + final result = create(); + if (home != null) result.home = home; + if (style != null) result.style = style; + if (followGps != null) result.followGps = followGps; + return result; + } + + Map_._(); + + factory Map_.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Map_.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Map', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'home', + subBuilder: GeoPoint.create) + ..aOS(2, _omitFieldNames ? '' : 'style') + ..aOB(3, _omitFieldNames ? '' : 'followGps') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Map_ clone() => Map_()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Map_ copyWith(void Function(Map_) updates) => + super.copyWith((message) => updates(message as Map_)) as Map_; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Map_ create() => Map_._(); + @$core.override + Map_ createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Map_ getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Map_? _defaultInstance; + + /// + /// Home coordinates + @$pb.TagNumber(1) + GeoPoint get home => $_getN(0); + @$pb.TagNumber(1) + set home(GeoPoint value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasHome() => $_has(0); + @$pb.TagNumber(1) + void clearHome() => $_clearField(1); + @$pb.TagNumber(1) + GeoPoint ensureHome() => $_ensure(0); + + /// + /// Map tile style + @$pb.TagNumber(2) + $core.String get style => $_getSZ(1); + @$pb.TagNumber(2) + set style($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasStyle() => $_has(1); + @$pb.TagNumber(2) + void clearStyle() => $_clearField(2); + + /// + /// Map scroll follows GPS + @$pb.TagNumber(3) + $core.bool get followGps => $_getBF(2); + @$pb.TagNumber(3) + set followGps($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasFollowGps() => $_has(2); + @$pb.TagNumber(3) + void clearFollowGps() => $_clearField(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/device_ui.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/device_ui.pbenum.dart new file mode 100644 index 000000000..5f69b03bf --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/device_ui.pbenum.dart @@ -0,0 +1,203 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/device_ui.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class CompassMode extends $pb.ProtobufEnum { + /// + /// Compass with dynamic ring and heading + static const CompassMode DYNAMIC = + CompassMode._(0, _omitEnumNames ? '' : 'DYNAMIC'); + + /// + /// Compass with fixed ring and heading + static const CompassMode FIXED_RING = + CompassMode._(1, _omitEnumNames ? '' : 'FIXED_RING'); + + /// + /// Compass with heading and freeze option + static const CompassMode FREEZE_HEADING = + CompassMode._(2, _omitEnumNames ? '' : 'FREEZE_HEADING'); + + static const $core.List values = [ + DYNAMIC, + FIXED_RING, + FREEZE_HEADING, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static CompassMode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const CompassMode._(super.value, super.name); +} + +class Theme extends $pb.ProtobufEnum { + /// + /// Dark + static const Theme DARK = Theme._(0, _omitEnumNames ? '' : 'DARK'); + + /// + /// Light + static const Theme LIGHT = Theme._(1, _omitEnumNames ? '' : 'LIGHT'); + + /// + /// Red + static const Theme RED = Theme._(2, _omitEnumNames ? '' : 'RED'); + + static const $core.List values = [ + DARK, + LIGHT, + RED, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static Theme? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Theme._(super.value, super.name); +} + +/// +/// Localization +class Language extends $pb.ProtobufEnum { + /// + /// English + static const Language ENGLISH = + Language._(0, _omitEnumNames ? '' : 'ENGLISH'); + + /// + /// French + static const Language FRENCH = Language._(1, _omitEnumNames ? '' : 'FRENCH'); + + /// + /// German + static const Language GERMAN = Language._(2, _omitEnumNames ? '' : 'GERMAN'); + + /// + /// Italian + static const Language ITALIAN = + Language._(3, _omitEnumNames ? '' : 'ITALIAN'); + + /// + /// Portuguese + static const Language PORTUGUESE = + Language._(4, _omitEnumNames ? '' : 'PORTUGUESE'); + + /// + /// Spanish + static const Language SPANISH = + Language._(5, _omitEnumNames ? '' : 'SPANISH'); + + /// + /// Swedish + static const Language SWEDISH = + Language._(6, _omitEnumNames ? '' : 'SWEDISH'); + + /// + /// Finnish + static const Language FINNISH = + Language._(7, _omitEnumNames ? '' : 'FINNISH'); + + /// + /// Polish + static const Language POLISH = Language._(8, _omitEnumNames ? '' : 'POLISH'); + + /// + /// Turkish + static const Language TURKISH = + Language._(9, _omitEnumNames ? '' : 'TURKISH'); + + /// + /// Serbian + static const Language SERBIAN = + Language._(10, _omitEnumNames ? '' : 'SERBIAN'); + + /// + /// Russian + static const Language RUSSIAN = + Language._(11, _omitEnumNames ? '' : 'RUSSIAN'); + + /// + /// Dutch + static const Language DUTCH = Language._(12, _omitEnumNames ? '' : 'DUTCH'); + + /// + /// Greek + static const Language GREEK = Language._(13, _omitEnumNames ? '' : 'GREEK'); + + /// + /// Norwegian + static const Language NORWEGIAN = + Language._(14, _omitEnumNames ? '' : 'NORWEGIAN'); + + /// + /// Slovenian + static const Language SLOVENIAN = + Language._(15, _omitEnumNames ? '' : 'SLOVENIAN'); + + /// + /// Ukrainian + static const Language UKRAINIAN = + Language._(16, _omitEnumNames ? '' : 'UKRAINIAN'); + + /// + /// Bulgarian + static const Language BULGARIAN = + Language._(17, _omitEnumNames ? '' : 'BULGARIAN'); + + /// + /// Simplified Chinese (experimental) + static const Language SIMPLIFIED_CHINESE = + Language._(30, _omitEnumNames ? '' : 'SIMPLIFIED_CHINESE'); + + /// + /// Traditional Chinese (experimental) + static const Language TRADITIONAL_CHINESE = + Language._(31, _omitEnumNames ? '' : 'TRADITIONAL_CHINESE'); + + static const $core.List values = [ + ENGLISH, + FRENCH, + GERMAN, + ITALIAN, + PORTUGUESE, + SPANISH, + SWEDISH, + FINNISH, + POLISH, + TURKISH, + SERBIAN, + RUSSIAN, + DUTCH, + GREEK, + NORWEGIAN, + SLOVENIAN, + UKRAINIAN, + BULGARIAN, + SIMPLIFIED_CHINESE, + TRADITIONAL_CHINESE, + ]; + + static final $core.Map<$core.int, Language> _byValue = + $pb.ProtobufEnum.initByValue(values); + static Language? valueOf($core.int value) => _byValue[value]; + + const Language._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/device_ui.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/device_ui.pbjson.dart new file mode 100644 index 000000000..b9283aa17 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/device_ui.pbjson.dart @@ -0,0 +1,261 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/device_ui.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use compassModeDescriptor instead') +const CompassMode$json = { + '1': 'CompassMode', + '2': [ + {'1': 'DYNAMIC', '2': 0}, + {'1': 'FIXED_RING', '2': 1}, + {'1': 'FREEZE_HEADING', '2': 2}, + ], +}; + +/// Descriptor for `CompassMode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List compassModeDescriptor = $convert.base64Decode( + 'CgtDb21wYXNzTW9kZRILCgdEWU5BTUlDEAASDgoKRklYRURfUklORxABEhIKDkZSRUVaRV9IRU' + 'FESU5HEAI='); + +@$core.Deprecated('Use themeDescriptor instead') +const Theme$json = { + '1': 'Theme', + '2': [ + {'1': 'DARK', '2': 0}, + {'1': 'LIGHT', '2': 1}, + {'1': 'RED', '2': 2}, + ], +}; + +/// Descriptor for `Theme`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List themeDescriptor = $convert + .base64Decode('CgVUaGVtZRIICgREQVJLEAASCQoFTElHSFQQARIHCgNSRUQQAg=='); + +@$core.Deprecated('Use languageDescriptor instead') +const Language$json = { + '1': 'Language', + '2': [ + {'1': 'ENGLISH', '2': 0}, + {'1': 'FRENCH', '2': 1}, + {'1': 'GERMAN', '2': 2}, + {'1': 'ITALIAN', '2': 3}, + {'1': 'PORTUGUESE', '2': 4}, + {'1': 'SPANISH', '2': 5}, + {'1': 'SWEDISH', '2': 6}, + {'1': 'FINNISH', '2': 7}, + {'1': 'POLISH', '2': 8}, + {'1': 'TURKISH', '2': 9}, + {'1': 'SERBIAN', '2': 10}, + {'1': 'RUSSIAN', '2': 11}, + {'1': 'DUTCH', '2': 12}, + {'1': 'GREEK', '2': 13}, + {'1': 'NORWEGIAN', '2': 14}, + {'1': 'SLOVENIAN', '2': 15}, + {'1': 'UKRAINIAN', '2': 16}, + {'1': 'BULGARIAN', '2': 17}, + {'1': 'SIMPLIFIED_CHINESE', '2': 30}, + {'1': 'TRADITIONAL_CHINESE', '2': 31}, + ], +}; + +/// Descriptor for `Language`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List languageDescriptor = $convert.base64Decode( + 'CghMYW5ndWFnZRILCgdFTkdMSVNIEAASCgoGRlJFTkNIEAESCgoGR0VSTUFOEAISCwoHSVRBTE' + 'lBThADEg4KClBPUlRVR1VFU0UQBBILCgdTUEFOSVNIEAUSCwoHU1dFRElTSBAGEgsKB0ZJTk5J' + 'U0gQBxIKCgZQT0xJU0gQCBILCgdUVVJLSVNIEAkSCwoHU0VSQklBThAKEgsKB1JVU1NJQU4QCx' + 'IJCgVEVVRDSBAMEgkKBUdSRUVLEA0SDQoJTk9SV0VHSUFOEA4SDQoJU0xPVkVOSUFOEA8SDQoJ' + 'VUtSQUlOSUFOEBASDQoJQlVMR0FSSUFOEBESFgoSU0lNUExJRklFRF9DSElORVNFEB4SFwoTVF' + 'JBRElUSU9OQUxfQ0hJTkVTRRAf'); + +@$core.Deprecated('Use deviceUIConfigDescriptor instead') +const DeviceUIConfig$json = { + '1': 'DeviceUIConfig', + '2': [ + {'1': 'version', '3': 1, '4': 1, '5': 13, '10': 'version'}, + { + '1': 'screen_brightness', + '3': 2, + '4': 1, + '5': 13, + '10': 'screenBrightness' + }, + {'1': 'screen_timeout', '3': 3, '4': 1, '5': 13, '10': 'screenTimeout'}, + {'1': 'screen_lock', '3': 4, '4': 1, '5': 8, '10': 'screenLock'}, + {'1': 'settings_lock', '3': 5, '4': 1, '5': 8, '10': 'settingsLock'}, + {'1': 'pin_code', '3': 6, '4': 1, '5': 13, '10': 'pinCode'}, + { + '1': 'theme', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Theme', + '10': 'theme' + }, + {'1': 'alert_enabled', '3': 8, '4': 1, '5': 8, '10': 'alertEnabled'}, + {'1': 'banner_enabled', '3': 9, '4': 1, '5': 8, '10': 'bannerEnabled'}, + {'1': 'ring_tone_id', '3': 10, '4': 1, '5': 13, '10': 'ringToneId'}, + { + '1': 'language', + '3': 11, + '4': 1, + '5': 14, + '6': '.meshtastic.Language', + '10': 'language' + }, + { + '1': 'node_filter', + '3': 12, + '4': 1, + '5': 11, + '6': '.meshtastic.NodeFilter', + '10': 'nodeFilter' + }, + { + '1': 'node_highlight', + '3': 13, + '4': 1, + '5': 11, + '6': '.meshtastic.NodeHighlight', + '10': 'nodeHighlight' + }, + { + '1': 'calibration_data', + '3': 14, + '4': 1, + '5': 12, + '10': 'calibrationData' + }, + { + '1': 'map_data', + '3': 15, + '4': 1, + '5': 11, + '6': '.meshtastic.Map', + '10': 'mapData' + }, + { + '1': 'compass_mode', + '3': 16, + '4': 1, + '5': 14, + '6': '.meshtastic.CompassMode', + '10': 'compassMode' + }, + {'1': 'screen_rgb_color', '3': 17, '4': 1, '5': 13, '10': 'screenRgbColor'}, + { + '1': 'is_clockface_analog', + '3': 18, + '4': 1, + '5': 8, + '10': 'isClockfaceAnalog' + }, + ], +}; + +/// Descriptor for `DeviceUIConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceUIConfigDescriptor = $convert.base64Decode( + 'Cg5EZXZpY2VVSUNvbmZpZxIYCgd2ZXJzaW9uGAEgASgNUgd2ZXJzaW9uEisKEXNjcmVlbl9icm' + 'lnaHRuZXNzGAIgASgNUhBzY3JlZW5CcmlnaHRuZXNzEiUKDnNjcmVlbl90aW1lb3V0GAMgASgN' + 'Ug1zY3JlZW5UaW1lb3V0Eh8KC3NjcmVlbl9sb2NrGAQgASgIUgpzY3JlZW5Mb2NrEiMKDXNldH' + 'RpbmdzX2xvY2sYBSABKAhSDHNldHRpbmdzTG9jaxIZCghwaW5fY29kZRgGIAEoDVIHcGluQ29k' + 'ZRInCgV0aGVtZRgHIAEoDjIRLm1lc2h0YXN0aWMuVGhlbWVSBXRoZW1lEiMKDWFsZXJ0X2VuYW' + 'JsZWQYCCABKAhSDGFsZXJ0RW5hYmxlZBIlCg5iYW5uZXJfZW5hYmxlZBgJIAEoCFINYmFubmVy' + 'RW5hYmxlZBIgCgxyaW5nX3RvbmVfaWQYCiABKA1SCnJpbmdUb25lSWQSMAoIbGFuZ3VhZ2UYCy' + 'ABKA4yFC5tZXNodGFzdGljLkxhbmd1YWdlUghsYW5ndWFnZRI3Cgtub2RlX2ZpbHRlchgMIAEo' + 'CzIWLm1lc2h0YXN0aWMuTm9kZUZpbHRlclIKbm9kZUZpbHRlchJACg5ub2RlX2hpZ2hsaWdodB' + 'gNIAEoCzIZLm1lc2h0YXN0aWMuTm9kZUhpZ2hsaWdodFINbm9kZUhpZ2hsaWdodBIpChBjYWxp' + 'YnJhdGlvbl9kYXRhGA4gASgMUg9jYWxpYnJhdGlvbkRhdGESKgoIbWFwX2RhdGEYDyABKAsyDy' + '5tZXNodGFzdGljLk1hcFIHbWFwRGF0YRI6Cgxjb21wYXNzX21vZGUYECABKA4yFy5tZXNodGFz' + 'dGljLkNvbXBhc3NNb2RlUgtjb21wYXNzTW9kZRIoChBzY3JlZW5fcmdiX2NvbG9yGBEgASgNUg' + '5zY3JlZW5SZ2JDb2xvchIuChNpc19jbG9ja2ZhY2VfYW5hbG9nGBIgASgIUhFpc0Nsb2NrZmFj' + 'ZUFuYWxvZw=='); + +@$core.Deprecated('Use nodeFilterDescriptor instead') +const NodeFilter$json = { + '1': 'NodeFilter', + '2': [ + {'1': 'unknown_switch', '3': 1, '4': 1, '5': 8, '10': 'unknownSwitch'}, + {'1': 'offline_switch', '3': 2, '4': 1, '5': 8, '10': 'offlineSwitch'}, + {'1': 'public_key_switch', '3': 3, '4': 1, '5': 8, '10': 'publicKeySwitch'}, + {'1': 'hops_away', '3': 4, '4': 1, '5': 5, '10': 'hopsAway'}, + {'1': 'position_switch', '3': 5, '4': 1, '5': 8, '10': 'positionSwitch'}, + {'1': 'node_name', '3': 6, '4': 1, '5': 9, '10': 'nodeName'}, + {'1': 'channel', '3': 7, '4': 1, '5': 5, '10': 'channel'}, + ], +}; + +/// Descriptor for `NodeFilter`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeFilterDescriptor = $convert.base64Decode( + 'CgpOb2RlRmlsdGVyEiUKDnVua25vd25fc3dpdGNoGAEgASgIUg11bmtub3duU3dpdGNoEiUKDm' + '9mZmxpbmVfc3dpdGNoGAIgASgIUg1vZmZsaW5lU3dpdGNoEioKEXB1YmxpY19rZXlfc3dpdGNo' + 'GAMgASgIUg9wdWJsaWNLZXlTd2l0Y2gSGwoJaG9wc19hd2F5GAQgASgFUghob3BzQXdheRInCg' + '9wb3NpdGlvbl9zd2l0Y2gYBSABKAhSDnBvc2l0aW9uU3dpdGNoEhsKCW5vZGVfbmFtZRgGIAEo' + 'CVIIbm9kZU5hbWUSGAoHY2hhbm5lbBgHIAEoBVIHY2hhbm5lbA=='); + +@$core.Deprecated('Use nodeHighlightDescriptor instead') +const NodeHighlight$json = { + '1': 'NodeHighlight', + '2': [ + {'1': 'chat_switch', '3': 1, '4': 1, '5': 8, '10': 'chatSwitch'}, + {'1': 'position_switch', '3': 2, '4': 1, '5': 8, '10': 'positionSwitch'}, + {'1': 'telemetry_switch', '3': 3, '4': 1, '5': 8, '10': 'telemetrySwitch'}, + {'1': 'iaq_switch', '3': 4, '4': 1, '5': 8, '10': 'iaqSwitch'}, + {'1': 'node_name', '3': 5, '4': 1, '5': 9, '10': 'nodeName'}, + ], +}; + +/// Descriptor for `NodeHighlight`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeHighlightDescriptor = $convert.base64Decode( + 'Cg1Ob2RlSGlnaGxpZ2h0Eh8KC2NoYXRfc3dpdGNoGAEgASgIUgpjaGF0U3dpdGNoEicKD3Bvc2' + 'l0aW9uX3N3aXRjaBgCIAEoCFIOcG9zaXRpb25Td2l0Y2gSKQoQdGVsZW1ldHJ5X3N3aXRjaBgD' + 'IAEoCFIPdGVsZW1ldHJ5U3dpdGNoEh0KCmlhcV9zd2l0Y2gYBCABKAhSCWlhcVN3aXRjaBIbCg' + 'lub2RlX25hbWUYBSABKAlSCG5vZGVOYW1l'); + +@$core.Deprecated('Use geoPointDescriptor instead') +const GeoPoint$json = { + '1': 'GeoPoint', + '2': [ + {'1': 'zoom', '3': 1, '4': 1, '5': 5, '10': 'zoom'}, + {'1': 'latitude', '3': 2, '4': 1, '5': 5, '10': 'latitude'}, + {'1': 'longitude', '3': 3, '4': 1, '5': 5, '10': 'longitude'}, + ], +}; + +/// Descriptor for `GeoPoint`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List geoPointDescriptor = $convert.base64Decode( + 'CghHZW9Qb2ludBISCgR6b29tGAEgASgFUgR6b29tEhoKCGxhdGl0dWRlGAIgASgFUghsYXRpdH' + 'VkZRIcCglsb25naXR1ZGUYAyABKAVSCWxvbmdpdHVkZQ=='); + +@$core.Deprecated('Use map_Descriptor instead') +const Map_$json = { + '1': 'Map', + '2': [ + { + '1': 'home', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.GeoPoint', + '10': 'home' + }, + {'1': 'style', '3': 2, '4': 1, '5': 9, '10': 'style'}, + {'1': 'follow_gps', '3': 3, '4': 1, '5': 8, '10': 'followGps'}, + ], +}; + +/// Descriptor for `Map`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List map_Descriptor = $convert.base64Decode( + 'CgNNYXASKAoEaG9tZRgBIAEoCzIULm1lc2h0YXN0aWMuR2VvUG9pbnRSBGhvbWUSFAoFc3R5bG' + 'UYAiABKAlSBXN0eWxlEh0KCmZvbGxvd19ncHMYAyABKAhSCWZvbGxvd0dwcw=='); diff --git a/third_party/meshtastic_flutter/lib/generated/deviceonly.pb.dart b/third_party/meshtastic_flutter/lib/generated/deviceonly.pb.dart new file mode 100644 index 000000000..6a9bf3c51 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/deviceonly.pb.dart @@ -0,0 +1,1048 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/deviceonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'channel.pb.dart' as $2; +import 'config.pbenum.dart' as $4; +import 'localonly.pb.dart' as $3; +import 'mesh.pb.dart' as $1; +import 'telemetry.pb.dart' as $0; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// Position with static location information only for NodeDBLite +class PositionLite extends $pb.GeneratedMessage { + factory PositionLite({ + $core.int? latitudeI, + $core.int? longitudeI, + $core.int? altitude, + $core.int? time, + $1.Position_LocSource? locationSource, + }) { + final result = create(); + if (latitudeI != null) result.latitudeI = latitudeI; + if (longitudeI != null) result.longitudeI = longitudeI; + if (altitude != null) result.altitude = altitude; + if (time != null) result.time = time; + if (locationSource != null) result.locationSource = locationSource; + return result; + } + + PositionLite._(); + + factory PositionLite.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory PositionLite.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'PositionLite', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'latitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'longitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'altitude', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'time', $pb.PbFieldType.OF3) + ..e<$1.Position_LocSource>( + 5, _omitFieldNames ? '' : 'locationSource', $pb.PbFieldType.OE, + defaultOrMaker: $1.Position_LocSource.LOC_UNSET, + valueOf: $1.Position_LocSource.valueOf, + enumValues: $1.Position_LocSource.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PositionLite clone() => PositionLite()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PositionLite copyWith(void Function(PositionLite) updates) => + super.copyWith((message) => updates(message as PositionLite)) + as PositionLite; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PositionLite create() => PositionLite._(); + @$core.override + PositionLite createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PositionLite getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static PositionLite? _defaultInstance; + + /// + /// The new preferred location encoding, multiply by 1e-7 to get degrees + /// in floating point + @$pb.TagNumber(1) + $core.int get latitudeI => $_getIZ(0); + @$pb.TagNumber(1) + set latitudeI($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasLatitudeI() => $_has(0); + @$pb.TagNumber(1) + void clearLatitudeI() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + $core.int get longitudeI => $_getIZ(1); + @$pb.TagNumber(2) + set longitudeI($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasLongitudeI() => $_has(1); + @$pb.TagNumber(2) + void clearLongitudeI() => $_clearField(2); + + /// + /// In meters above MSL (but see issue #359) + @$pb.TagNumber(3) + $core.int get altitude => $_getIZ(2); + @$pb.TagNumber(3) + set altitude($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasAltitude() => $_has(2); + @$pb.TagNumber(3) + void clearAltitude() => $_clearField(3); + + /// + /// This is usually not sent over the mesh (to save space), but it is sent + /// from the phone so that the local device can set its RTC If it is sent over + /// the mesh (because there are devices on the mesh without GPS), it will only + /// be sent by devices which has a hardware GPS clock. + /// seconds since 1970 + @$pb.TagNumber(4) + $core.int get time => $_getIZ(3); + @$pb.TagNumber(4) + set time($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasTime() => $_has(3); + @$pb.TagNumber(4) + void clearTime() => $_clearField(4); + + /// + /// TODO: REPLACE + @$pb.TagNumber(5) + $1.Position_LocSource get locationSource => $_getN(4); + @$pb.TagNumber(5) + set locationSource($1.Position_LocSource value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasLocationSource() => $_has(4); + @$pb.TagNumber(5) + void clearLocationSource() => $_clearField(5); +} + +class UserLite extends $pb.GeneratedMessage { + factory UserLite({ + @$core.Deprecated('This field is deprecated.') + $core.List<$core.int>? macaddr, + $core.String? longName, + $core.String? shortName, + $1.HardwareModel? hwModel, + $core.bool? isLicensed, + $4.Config_DeviceConfig_Role? role, + $core.List<$core.int>? publicKey, + $core.bool? isUnmessagable, + }) { + final result = create(); + if (macaddr != null) result.macaddr = macaddr; + if (longName != null) result.longName = longName; + if (shortName != null) result.shortName = shortName; + if (hwModel != null) result.hwModel = hwModel; + if (isLicensed != null) result.isLicensed = isLicensed; + if (role != null) result.role = role; + if (publicKey != null) result.publicKey = publicKey; + if (isUnmessagable != null) result.isUnmessagable = isUnmessagable; + return result; + } + + UserLite._(); + + factory UserLite.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory UserLite.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'UserLite', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.List<$core.int>>( + 1, _omitFieldNames ? '' : 'macaddr', $pb.PbFieldType.OY) + ..aOS(2, _omitFieldNames ? '' : 'longName') + ..aOS(3, _omitFieldNames ? '' : 'shortName') + ..e<$1.HardwareModel>( + 4, _omitFieldNames ? '' : 'hwModel', $pb.PbFieldType.OE, + defaultOrMaker: $1.HardwareModel.UNSET, + valueOf: $1.HardwareModel.valueOf, + enumValues: $1.HardwareModel.values) + ..aOB(5, _omitFieldNames ? '' : 'isLicensed') + ..e<$4.Config_DeviceConfig_Role>( + 6, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: $4.Config_DeviceConfig_Role.CLIENT, + valueOf: $4.Config_DeviceConfig_Role.valueOf, + enumValues: $4.Config_DeviceConfig_Role.values) + ..a<$core.List<$core.int>>( + 7, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..aOB(9, _omitFieldNames ? '' : 'isUnmessagable') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + UserLite clone() => UserLite()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + UserLite copyWith(void Function(UserLite) updates) => + super.copyWith((message) => updates(message as UserLite)) as UserLite; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UserLite create() => UserLite._(); + @$core.override + UserLite createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UserLite getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UserLite? _defaultInstance; + + /// + /// This is the addr of the radio. + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + $core.List<$core.int> get macaddr => $_getN(0); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + set macaddr($core.List<$core.int> value) => $_setBytes(0, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + $core.bool hasMacaddr() => $_has(0); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(1) + void clearMacaddr() => $_clearField(1); + + /// + /// A full name for this user, i.e. "Kevin Hester" + @$pb.TagNumber(2) + $core.String get longName => $_getSZ(1); + @$pb.TagNumber(2) + set longName($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasLongName() => $_has(1); + @$pb.TagNumber(2) + void clearLongName() => $_clearField(2); + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + @$pb.TagNumber(3) + $core.String get shortName => $_getSZ(2); + @$pb.TagNumber(3) + set shortName($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasShortName() => $_has(2); + @$pb.TagNumber(3) + void clearShortName() => $_clearField(3); + + /// + /// TBEAM, HELTEC, etc... + /// Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. + /// Apps will still need the string here for older builds + /// (so OTA update can find the right image), but if the enum is available it will be used instead. + @$pb.TagNumber(4) + $1.HardwareModel get hwModel => $_getN(3); + @$pb.TagNumber(4) + set hwModel($1.HardwareModel value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasHwModel() => $_has(3); + @$pb.TagNumber(4) + void clearHwModel() => $_clearField(4); + + /// + /// In some regions Ham radio operators have different bandwidth limitations than others. + /// If this user is a licensed operator, set this flag. + /// Also, "long_name" should be their licence number. + @$pb.TagNumber(5) + $core.bool get isLicensed => $_getBF(4); + @$pb.TagNumber(5) + set isLicensed($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasIsLicensed() => $_has(4); + @$pb.TagNumber(5) + void clearIsLicensed() => $_clearField(5); + + /// + /// Indicates that the user's role in the mesh + @$pb.TagNumber(6) + $4.Config_DeviceConfig_Role get role => $_getN(5); + @$pb.TagNumber(6) + set role($4.Config_DeviceConfig_Role value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasRole() => $_has(5); + @$pb.TagNumber(6) + void clearRole() => $_clearField(6); + + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + @$pb.TagNumber(7) + $core.List<$core.int> get publicKey => $_getN(6); + @$pb.TagNumber(7) + set publicKey($core.List<$core.int> value) => $_setBytes(6, value); + @$pb.TagNumber(7) + $core.bool hasPublicKey() => $_has(6); + @$pb.TagNumber(7) + void clearPublicKey() => $_clearField(7); + + /// + /// Whether or not the node can be messaged + @$pb.TagNumber(9) + $core.bool get isUnmessagable => $_getBF(7); + @$pb.TagNumber(9) + set isUnmessagable($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(9) + $core.bool hasIsUnmessagable() => $_has(7); + @$pb.TagNumber(9) + void clearIsUnmessagable() => $_clearField(9); +} + +class NodeInfoLite extends $pb.GeneratedMessage { + factory NodeInfoLite({ + $core.int? num, + UserLite? user, + PositionLite? position, + $core.double? snr, + $core.int? lastHeard, + $0.DeviceMetrics? deviceMetrics, + $core.int? channel, + $core.bool? viaMqtt, + $core.int? hopsAway, + $core.bool? isFavorite, + $core.bool? isIgnored, + $core.int? nextHop, + $core.int? bitfield, + }) { + final result = create(); + if (num != null) result.num = num; + if (user != null) result.user = user; + if (position != null) result.position = position; + if (snr != null) result.snr = snr; + if (lastHeard != null) result.lastHeard = lastHeard; + if (deviceMetrics != null) result.deviceMetrics = deviceMetrics; + if (channel != null) result.channel = channel; + if (viaMqtt != null) result.viaMqtt = viaMqtt; + if (hopsAway != null) result.hopsAway = hopsAway; + if (isFavorite != null) result.isFavorite = isFavorite; + if (isIgnored != null) result.isIgnored = isIgnored; + if (nextHop != null) result.nextHop = nextHop; + if (bitfield != null) result.bitfield = bitfield; + return result; + } + + NodeInfoLite._(); + + factory NodeInfoLite.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeInfoLite.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeInfoLite', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'num', $pb.PbFieldType.OU3) + ..aOM(2, _omitFieldNames ? '' : 'user', + subBuilder: UserLite.create) + ..aOM(3, _omitFieldNames ? '' : 'position', + subBuilder: PositionLite.create) + ..a<$core.double>(4, _omitFieldNames ? '' : 'snr', $pb.PbFieldType.OF) + ..a<$core.int>(5, _omitFieldNames ? '' : 'lastHeard', $pb.PbFieldType.OF3) + ..aOM<$0.DeviceMetrics>(6, _omitFieldNames ? '' : 'deviceMetrics', + subBuilder: $0.DeviceMetrics.create) + ..a<$core.int>(7, _omitFieldNames ? '' : 'channel', $pb.PbFieldType.OU3) + ..aOB(8, _omitFieldNames ? '' : 'viaMqtt') + ..a<$core.int>(9, _omitFieldNames ? '' : 'hopsAway', $pb.PbFieldType.OU3) + ..aOB(10, _omitFieldNames ? '' : 'isFavorite') + ..aOB(11, _omitFieldNames ? '' : 'isIgnored') + ..a<$core.int>(12, _omitFieldNames ? '' : 'nextHop', $pb.PbFieldType.OU3) + ..a<$core.int>(13, _omitFieldNames ? '' : 'bitfield', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeInfoLite clone() => NodeInfoLite()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeInfoLite copyWith(void Function(NodeInfoLite) updates) => + super.copyWith((message) => updates(message as NodeInfoLite)) + as NodeInfoLite; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeInfoLite create() => NodeInfoLite._(); + @$core.override + NodeInfoLite createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeInfoLite getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeInfoLite? _defaultInstance; + + /// + /// The node number + @$pb.TagNumber(1) + $core.int get num => $_getIZ(0); + @$pb.TagNumber(1) + set num($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNum() => $_has(0); + @$pb.TagNumber(1) + void clearNum() => $_clearField(1); + + /// + /// The user info for this node + @$pb.TagNumber(2) + UserLite get user => $_getN(1); + @$pb.TagNumber(2) + set user(UserLite value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasUser() => $_has(1); + @$pb.TagNumber(2) + void clearUser() => $_clearField(2); + @$pb.TagNumber(2) + UserLite ensureUser() => $_ensure(1); + + /// + /// This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. + /// Position.time now indicates the last time we received a POSITION from that node. + @$pb.TagNumber(3) + PositionLite get position => $_getN(2); + @$pb.TagNumber(3) + set position(PositionLite value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasPosition() => $_has(2); + @$pb.TagNumber(3) + void clearPosition() => $_clearField(3); + @$pb.TagNumber(3) + PositionLite ensurePosition() => $_ensure(2); + + /// + /// Returns the Signal-to-noise ratio (SNR) of the last received message, + /// as measured by the receiver. Return SNR of the last received message in dB + @$pb.TagNumber(4) + $core.double get snr => $_getN(3); + @$pb.TagNumber(4) + set snr($core.double value) => $_setFloat(3, value); + @$pb.TagNumber(4) + $core.bool hasSnr() => $_has(3); + @$pb.TagNumber(4) + void clearSnr() => $_clearField(4); + + /// + /// Set to indicate the last time we received a packet from this node + @$pb.TagNumber(5) + $core.int get lastHeard => $_getIZ(4); + @$pb.TagNumber(5) + set lastHeard($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasLastHeard() => $_has(4); + @$pb.TagNumber(5) + void clearLastHeard() => $_clearField(5); + + /// + /// The latest device metrics for the node. + @$pb.TagNumber(6) + $0.DeviceMetrics get deviceMetrics => $_getN(5); + @$pb.TagNumber(6) + set deviceMetrics($0.DeviceMetrics value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasDeviceMetrics() => $_has(5); + @$pb.TagNumber(6) + void clearDeviceMetrics() => $_clearField(6); + @$pb.TagNumber(6) + $0.DeviceMetrics ensureDeviceMetrics() => $_ensure(5); + + /// + /// local channel index we heard that node on. Only populated if its not the default channel. + @$pb.TagNumber(7) + $core.int get channel => $_getIZ(6); + @$pb.TagNumber(7) + set channel($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasChannel() => $_has(6); + @$pb.TagNumber(7) + void clearChannel() => $_clearField(7); + + /// + /// True if we witnessed the node over MQTT instead of LoRA transport + @$pb.TagNumber(8) + $core.bool get viaMqtt => $_getBF(7); + @$pb.TagNumber(8) + set viaMqtt($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasViaMqtt() => $_has(7); + @$pb.TagNumber(8) + void clearViaMqtt() => $_clearField(8); + + /// + /// Number of hops away from us this node is (0 if direct neighbor) + @$pb.TagNumber(9) + $core.int get hopsAway => $_getIZ(8); + @$pb.TagNumber(9) + set hopsAway($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasHopsAway() => $_has(8); + @$pb.TagNumber(9) + void clearHopsAway() => $_clearField(9); + + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + @$pb.TagNumber(10) + $core.bool get isFavorite => $_getBF(9); + @$pb.TagNumber(10) + set isFavorite($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasIsFavorite() => $_has(9); + @$pb.TagNumber(10) + void clearIsFavorite() => $_clearField(10); + + /// + /// True if node is in our ignored list + /// Persists between NodeDB internal clean ups + @$pb.TagNumber(11) + $core.bool get isIgnored => $_getBF(10); + @$pb.TagNumber(11) + set isIgnored($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(11) + $core.bool hasIsIgnored() => $_has(10); + @$pb.TagNumber(11) + void clearIsIgnored() => $_clearField(11); + + /// + /// Last byte of the node number of the node that should be used as the next hop to reach this node. + @$pb.TagNumber(12) + $core.int get nextHop => $_getIZ(11); + @$pb.TagNumber(12) + set nextHop($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasNextHop() => $_has(11); + @$pb.TagNumber(12) + void clearNextHop() => $_clearField(12); + + /// + /// Bitfield for storing booleans. + /// LSB 0 is_key_manually_verified + @$pb.TagNumber(13) + $core.int get bitfield => $_getIZ(12); + @$pb.TagNumber(13) + set bitfield($core.int value) => $_setUnsignedInt32(12, value); + @$pb.TagNumber(13) + $core.bool hasBitfield() => $_has(12); + @$pb.TagNumber(13) + void clearBitfield() => $_clearField(13); +} + +/// +/// This message is never sent over the wire, but it is used for serializing DB +/// state to flash in the device code +/// FIXME, since we write this each time we enter deep sleep (and have infinite +/// flash) it would be better to use some sort of append only data structure for +/// the receive queue and use the preferences store for the other stuff +class DeviceState extends $pb.GeneratedMessage { + factory DeviceState({ + $1.MyNodeInfo? myNode, + $1.User? owner, + $core.Iterable<$1.MeshPacket>? receiveQueue, + $1.MeshPacket? rxTextMessage, + $core.int? version, + @$core.Deprecated('This field is deprecated.') $core.bool? noSave, + @$core.Deprecated('This field is deprecated.') $core.bool? didGpsReset, + $1.MeshPacket? rxWaypoint, + $core.Iterable<$1.NodeRemoteHardwarePin>? nodeRemoteHardwarePins, + }) { + final result = create(); + if (myNode != null) result.myNode = myNode; + if (owner != null) result.owner = owner; + if (receiveQueue != null) result.receiveQueue.addAll(receiveQueue); + if (rxTextMessage != null) result.rxTextMessage = rxTextMessage; + if (version != null) result.version = version; + if (noSave != null) result.noSave = noSave; + if (didGpsReset != null) result.didGpsReset = didGpsReset; + if (rxWaypoint != null) result.rxWaypoint = rxWaypoint; + if (nodeRemoteHardwarePins != null) + result.nodeRemoteHardwarePins.addAll(nodeRemoteHardwarePins); + return result; + } + + DeviceState._(); + + factory DeviceState.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeviceState.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeviceState', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM<$1.MyNodeInfo>(2, _omitFieldNames ? '' : 'myNode', + subBuilder: $1.MyNodeInfo.create) + ..aOM<$1.User>(3, _omitFieldNames ? '' : 'owner', + subBuilder: $1.User.create) + ..pc<$1.MeshPacket>( + 5, _omitFieldNames ? '' : 'receiveQueue', $pb.PbFieldType.PM, + subBuilder: $1.MeshPacket.create) + ..aOM<$1.MeshPacket>(7, _omitFieldNames ? '' : 'rxTextMessage', + subBuilder: $1.MeshPacket.create) + ..a<$core.int>(8, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..aOB(9, _omitFieldNames ? '' : 'noSave') + ..aOB(11, _omitFieldNames ? '' : 'didGpsReset') + ..aOM<$1.MeshPacket>(12, _omitFieldNames ? '' : 'rxWaypoint', + subBuilder: $1.MeshPacket.create) + ..pc<$1.NodeRemoteHardwarePin>( + 13, _omitFieldNames ? '' : 'nodeRemoteHardwarePins', $pb.PbFieldType.PM, + subBuilder: $1.NodeRemoteHardwarePin.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceState clone() => DeviceState()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceState copyWith(void Function(DeviceState) updates) => + super.copyWith((message) => updates(message as DeviceState)) + as DeviceState; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceState create() => DeviceState._(); + @$core.override + DeviceState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceState getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeviceState? _defaultInstance; + + /// + /// Read only settings/info about this node + @$pb.TagNumber(2) + $1.MyNodeInfo get myNode => $_getN(0); + @$pb.TagNumber(2) + set myNode($1.MyNodeInfo value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasMyNode() => $_has(0); + @$pb.TagNumber(2) + void clearMyNode() => $_clearField(2); + @$pb.TagNumber(2) + $1.MyNodeInfo ensureMyNode() => $_ensure(0); + + /// + /// My owner info + @$pb.TagNumber(3) + $1.User get owner => $_getN(1); + @$pb.TagNumber(3) + set owner($1.User value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasOwner() => $_has(1); + @$pb.TagNumber(3) + void clearOwner() => $_clearField(3); + @$pb.TagNumber(3) + $1.User ensureOwner() => $_ensure(1); + + /// + /// Received packets saved for delivery to the phone + @$pb.TagNumber(5) + $pb.PbList<$1.MeshPacket> get receiveQueue => $_getList(2); + + /// + /// We keep the last received text message (only) stored in the device flash, + /// so we can show it on the screen. + /// Might be null + @$pb.TagNumber(7) + $1.MeshPacket get rxTextMessage => $_getN(3); + @$pb.TagNumber(7) + set rxTextMessage($1.MeshPacket value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasRxTextMessage() => $_has(3); + @$pb.TagNumber(7) + void clearRxTextMessage() => $_clearField(7); + @$pb.TagNumber(7) + $1.MeshPacket ensureRxTextMessage() => $_ensure(3); + + /// + /// A version integer used to invalidate old save files when we make + /// incompatible changes This integer is set at build time and is private to + /// NodeDB.cpp in the device code. + @$pb.TagNumber(8) + $core.int get version => $_getIZ(4); + @$pb.TagNumber(8) + set version($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(8) + $core.bool hasVersion() => $_has(4); + @$pb.TagNumber(8) + void clearVersion() => $_clearField(8); + + /// + /// Used only during development. + /// Indicates developer is testing and changes should never be saved to flash. + /// Deprecated in 2.3.1 + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + $core.bool get noSave => $_getBF(5); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + set noSave($core.bool value) => $_setBool(5, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + $core.bool hasNoSave() => $_has(5); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + void clearNoSave() => $_clearField(9); + + /// + /// Previously used to manage GPS factory resets. + /// Deprecated in 2.5.23 + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(11) + $core.bool get didGpsReset => $_getBF(6); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(11) + set didGpsReset($core.bool value) => $_setBool(6, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(11) + $core.bool hasDidGpsReset() => $_has(6); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(11) + void clearDidGpsReset() => $_clearField(11); + + /// + /// We keep the last received waypoint stored in the device flash, + /// so we can show it on the screen. + /// Might be null + @$pb.TagNumber(12) + $1.MeshPacket get rxWaypoint => $_getN(7); + @$pb.TagNumber(12) + set rxWaypoint($1.MeshPacket value) => $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasRxWaypoint() => $_has(7); + @$pb.TagNumber(12) + void clearRxWaypoint() => $_clearField(12); + @$pb.TagNumber(12) + $1.MeshPacket ensureRxWaypoint() => $_ensure(7); + + /// + /// The mesh's nodes with their available gpio pins for RemoteHardware module + @$pb.TagNumber(13) + $pb.PbList<$1.NodeRemoteHardwarePin> get nodeRemoteHardwarePins => + $_getList(8); +} + +class NodeDatabase extends $pb.GeneratedMessage { + factory NodeDatabase({ + $core.int? version, + $core.Iterable? nodes, + }) { + final result = create(); + if (version != null) result.version = version; + if (nodes != null) result.nodes.addAll(nodes); + return result; + } + + NodeDatabase._(); + + factory NodeDatabase.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeDatabase.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeDatabase', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..pc(2, _omitFieldNames ? '' : 'nodes', $pb.PbFieldType.PM, + subBuilder: NodeInfoLite.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeDatabase clone() => NodeDatabase()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeDatabase copyWith(void Function(NodeDatabase) updates) => + super.copyWith((message) => updates(message as NodeDatabase)) + as NodeDatabase; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeDatabase create() => NodeDatabase._(); + @$core.override + NodeDatabase createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeDatabase getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeDatabase? _defaultInstance; + + /// + /// A version integer used to invalidate old save files when we make + /// incompatible changes This integer is set at build time and is private to + /// NodeDB.cpp in the device code. + @$pb.TagNumber(1) + $core.int get version => $_getIZ(0); + @$pb.TagNumber(1) + set version($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasVersion() => $_has(0); + @$pb.TagNumber(1) + void clearVersion() => $_clearField(1); + + /// + /// New lite version of NodeDB to decrease memory footprint + @$pb.TagNumber(2) + $pb.PbList get nodes => $_getList(1); +} + +/// +/// The on-disk saved channels +class ChannelFile extends $pb.GeneratedMessage { + factory ChannelFile({ + $core.Iterable<$2.Channel>? channels, + $core.int? version, + }) { + final result = create(); + if (channels != null) result.channels.addAll(channels); + if (version != null) result.version = version; + return result; + } + + ChannelFile._(); + + factory ChannelFile.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ChannelFile.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ChannelFile', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..pc<$2.Channel>(1, _omitFieldNames ? '' : 'channels', $pb.PbFieldType.PM, + subBuilder: $2.Channel.create) + ..a<$core.int>(2, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChannelFile clone() => ChannelFile()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChannelFile copyWith(void Function(ChannelFile) updates) => + super.copyWith((message) => updates(message as ChannelFile)) + as ChannelFile; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChannelFile create() => ChannelFile._(); + @$core.override + ChannelFile createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChannelFile getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ChannelFile? _defaultInstance; + + /// + /// The channels our node knows about + @$pb.TagNumber(1) + $pb.PbList<$2.Channel> get channels => $_getList(0); + + /// + /// A version integer used to invalidate old save files when we make + /// incompatible changes This integer is set at build time and is private to + /// NodeDB.cpp in the device code. + @$pb.TagNumber(2) + $core.int get version => $_getIZ(1); + @$pb.TagNumber(2) + set version($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasVersion() => $_has(1); + @$pb.TagNumber(2) + void clearVersion() => $_clearField(2); +} + +/// +/// The on-disk backup of the node's preferences +class BackupPreferences extends $pb.GeneratedMessage { + factory BackupPreferences({ + $core.int? version, + $core.int? timestamp, + $3.LocalConfig? config, + $3.LocalModuleConfig? moduleConfig, + ChannelFile? channels, + $1.User? owner, + }) { + final result = create(); + if (version != null) result.version = version; + if (timestamp != null) result.timestamp = timestamp; + if (config != null) result.config = config; + if (moduleConfig != null) result.moduleConfig = moduleConfig; + if (channels != null) result.channels = channels; + if (owner != null) result.owner = owner; + return result; + } + + BackupPreferences._(); + + factory BackupPreferences.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory BackupPreferences.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'BackupPreferences', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OF3) + ..aOM<$3.LocalConfig>(3, _omitFieldNames ? '' : 'config', + subBuilder: $3.LocalConfig.create) + ..aOM<$3.LocalModuleConfig>(4, _omitFieldNames ? '' : 'moduleConfig', + subBuilder: $3.LocalModuleConfig.create) + ..aOM(5, _omitFieldNames ? '' : 'channels', + subBuilder: ChannelFile.create) + ..aOM<$1.User>(6, _omitFieldNames ? '' : 'owner', + subBuilder: $1.User.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + BackupPreferences clone() => BackupPreferences()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + BackupPreferences copyWith(void Function(BackupPreferences) updates) => + super.copyWith((message) => updates(message as BackupPreferences)) + as BackupPreferences; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BackupPreferences create() => BackupPreferences._(); + @$core.override + BackupPreferences createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BackupPreferences getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static BackupPreferences? _defaultInstance; + + /// + /// The version of the backup + @$pb.TagNumber(1) + $core.int get version => $_getIZ(0); + @$pb.TagNumber(1) + set version($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasVersion() => $_has(0); + @$pb.TagNumber(1) + void clearVersion() => $_clearField(1); + + /// + /// The timestamp of the backup (if node has time) + @$pb.TagNumber(2) + $core.int get timestamp => $_getIZ(1); + @$pb.TagNumber(2) + set timestamp($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasTimestamp() => $_has(1); + @$pb.TagNumber(2) + void clearTimestamp() => $_clearField(2); + + /// + /// The node's configuration + @$pb.TagNumber(3) + $3.LocalConfig get config => $_getN(2); + @$pb.TagNumber(3) + set config($3.LocalConfig value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasConfig() => $_has(2); + @$pb.TagNumber(3) + void clearConfig() => $_clearField(3); + @$pb.TagNumber(3) + $3.LocalConfig ensureConfig() => $_ensure(2); + + /// + /// The node's module configuration + @$pb.TagNumber(4) + $3.LocalModuleConfig get moduleConfig => $_getN(3); + @$pb.TagNumber(4) + set moduleConfig($3.LocalModuleConfig value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasModuleConfig() => $_has(3); + @$pb.TagNumber(4) + void clearModuleConfig() => $_clearField(4); + @$pb.TagNumber(4) + $3.LocalModuleConfig ensureModuleConfig() => $_ensure(3); + + /// + /// The node's channels + @$pb.TagNumber(5) + ChannelFile get channels => $_getN(4); + @$pb.TagNumber(5) + set channels(ChannelFile value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasChannels() => $_has(4); + @$pb.TagNumber(5) + void clearChannels() => $_clearField(5); + @$pb.TagNumber(5) + ChannelFile ensureChannels() => $_ensure(4); + + /// + /// The node's user (owner) information + @$pb.TagNumber(6) + $1.User get owner => $_getN(5); + @$pb.TagNumber(6) + set owner($1.User value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasOwner() => $_has(5); + @$pb.TagNumber(6) + void clearOwner() => $_clearField(6); + @$pb.TagNumber(6) + $1.User ensureOwner() => $_ensure(5); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/deviceonly.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/deviceonly.pbenum.dart new file mode 100644 index 000000000..887e6e8f0 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/deviceonly.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/deviceonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/deviceonly.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/deviceonly.pbjson.dart new file mode 100644 index 000000000..579380b0a --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/deviceonly.pbjson.dart @@ -0,0 +1,340 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/deviceonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use positionLiteDescriptor instead') +const PositionLite$json = { + '1': 'PositionLite', + '2': [ + {'1': 'latitude_i', '3': 1, '4': 1, '5': 15, '10': 'latitudeI'}, + {'1': 'longitude_i', '3': 2, '4': 1, '5': 15, '10': 'longitudeI'}, + {'1': 'altitude', '3': 3, '4': 1, '5': 5, '10': 'altitude'}, + {'1': 'time', '3': 4, '4': 1, '5': 7, '10': 'time'}, + { + '1': 'location_source', + '3': 5, + '4': 1, + '5': 14, + '6': '.meshtastic.Position.LocSource', + '10': 'locationSource' + }, + ], +}; + +/// Descriptor for `PositionLite`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List positionLiteDescriptor = $convert.base64Decode( + 'CgxQb3NpdGlvbkxpdGUSHQoKbGF0aXR1ZGVfaRgBIAEoD1IJbGF0aXR1ZGVJEh8KC2xvbmdpdH' + 'VkZV9pGAIgASgPUgpsb25naXR1ZGVJEhoKCGFsdGl0dWRlGAMgASgFUghhbHRpdHVkZRISCgR0' + 'aW1lGAQgASgHUgR0aW1lEkcKD2xvY2F0aW9uX3NvdXJjZRgFIAEoDjIeLm1lc2h0YXN0aWMuUG' + '9zaXRpb24uTG9jU291cmNlUg5sb2NhdGlvblNvdXJjZQ=='); + +@$core.Deprecated('Use userLiteDescriptor instead') +const UserLite$json = { + '1': 'UserLite', + '2': [ + { + '1': 'macaddr', + '3': 1, + '4': 1, + '5': 12, + '8': {'3': true}, + '10': 'macaddr', + }, + {'1': 'long_name', '3': 2, '4': 1, '5': 9, '10': 'longName'}, + {'1': 'short_name', '3': 3, '4': 1, '5': 9, '10': 'shortName'}, + { + '1': 'hw_model', + '3': 4, + '4': 1, + '5': 14, + '6': '.meshtastic.HardwareModel', + '10': 'hwModel' + }, + {'1': 'is_licensed', '3': 5, '4': 1, '5': 8, '10': 'isLicensed'}, + { + '1': 'role', + '3': 6, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.Role', + '10': 'role' + }, + {'1': 'public_key', '3': 7, '4': 1, '5': 12, '10': 'publicKey'}, + { + '1': 'is_unmessagable', + '3': 9, + '4': 1, + '5': 8, + '9': 0, + '10': 'isUnmessagable', + '17': true + }, + ], + '8': [ + {'1': '_is_unmessagable'}, + ], +}; + +/// Descriptor for `UserLite`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List userLiteDescriptor = $convert.base64Decode( + 'CghVc2VyTGl0ZRIcCgdtYWNhZGRyGAEgASgMQgIYAVIHbWFjYWRkchIbCglsb25nX25hbWUYAi' + 'ABKAlSCGxvbmdOYW1lEh0KCnNob3J0X25hbWUYAyABKAlSCXNob3J0TmFtZRI0Cghod19tb2Rl' + 'bBgEIAEoDjIZLm1lc2h0YXN0aWMuSGFyZHdhcmVNb2RlbFIHaHdNb2RlbBIfCgtpc19saWNlbn' + 'NlZBgFIAEoCFIKaXNMaWNlbnNlZBI4CgRyb2xlGAYgASgOMiQubWVzaHRhc3RpYy5Db25maWcu' + 'RGV2aWNlQ29uZmlnLlJvbGVSBHJvbGUSHQoKcHVibGljX2tleRgHIAEoDFIJcHVibGljS2V5Ei' + 'wKD2lzX3VubWVzc2FnYWJsZRgJIAEoCEgAUg5pc1VubWVzc2FnYWJsZYgBAUISChBfaXNfdW5t' + 'ZXNzYWdhYmxl'); + +@$core.Deprecated('Use nodeInfoLiteDescriptor instead') +const NodeInfoLite$json = { + '1': 'NodeInfoLite', + '2': [ + {'1': 'num', '3': 1, '4': 1, '5': 13, '10': 'num'}, + { + '1': 'user', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.UserLite', + '10': 'user' + }, + { + '1': 'position', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.PositionLite', + '10': 'position' + }, + {'1': 'snr', '3': 4, '4': 1, '5': 2, '10': 'snr'}, + {'1': 'last_heard', '3': 5, '4': 1, '5': 7, '10': 'lastHeard'}, + { + '1': 'device_metrics', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceMetrics', + '10': 'deviceMetrics' + }, + {'1': 'channel', '3': 7, '4': 1, '5': 13, '10': 'channel'}, + {'1': 'via_mqtt', '3': 8, '4': 1, '5': 8, '10': 'viaMqtt'}, + { + '1': 'hops_away', + '3': 9, + '4': 1, + '5': 13, + '9': 0, + '10': 'hopsAway', + '17': true + }, + {'1': 'is_favorite', '3': 10, '4': 1, '5': 8, '10': 'isFavorite'}, + {'1': 'is_ignored', '3': 11, '4': 1, '5': 8, '10': 'isIgnored'}, + {'1': 'next_hop', '3': 12, '4': 1, '5': 13, '10': 'nextHop'}, + {'1': 'bitfield', '3': 13, '4': 1, '5': 13, '10': 'bitfield'}, + ], + '8': [ + {'1': '_hops_away'}, + ], +}; + +/// Descriptor for `NodeInfoLite`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeInfoLiteDescriptor = $convert.base64Decode( + 'CgxOb2RlSW5mb0xpdGUSEAoDbnVtGAEgASgNUgNudW0SKAoEdXNlchgCIAEoCzIULm1lc2h0YX' + 'N0aWMuVXNlckxpdGVSBHVzZXISNAoIcG9zaXRpb24YAyABKAsyGC5tZXNodGFzdGljLlBvc2l0' + 'aW9uTGl0ZVIIcG9zaXRpb24SEAoDc25yGAQgASgCUgNzbnISHQoKbGFzdF9oZWFyZBgFIAEoB1' + 'IJbGFzdEhlYXJkEkAKDmRldmljZV9tZXRyaWNzGAYgASgLMhkubWVzaHRhc3RpYy5EZXZpY2VN' + 'ZXRyaWNzUg1kZXZpY2VNZXRyaWNzEhgKB2NoYW5uZWwYByABKA1SB2NoYW5uZWwSGQoIdmlhX2' + '1xdHQYCCABKAhSB3ZpYU1xdHQSIAoJaG9wc19hd2F5GAkgASgNSABSCGhvcHNBd2F5iAEBEh8K' + 'C2lzX2Zhdm9yaXRlGAogASgIUgppc0Zhdm9yaXRlEh0KCmlzX2lnbm9yZWQYCyABKAhSCWlzSW' + 'dub3JlZBIZCghuZXh0X2hvcBgMIAEoDVIHbmV4dEhvcBIaCghiaXRmaWVsZBgNIAEoDVIIYml0' + 'ZmllbGRCDAoKX2hvcHNfYXdheQ=='); + +@$core.Deprecated('Use deviceStateDescriptor instead') +const DeviceState$json = { + '1': 'DeviceState', + '2': [ + { + '1': 'my_node', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.MyNodeInfo', + '10': 'myNode' + }, + { + '1': 'owner', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.User', + '10': 'owner' + }, + { + '1': 'receive_queue', + '3': 5, + '4': 3, + '5': 11, + '6': '.meshtastic.MeshPacket', + '10': 'receiveQueue' + }, + {'1': 'version', '3': 8, '4': 1, '5': 13, '10': 'version'}, + { + '1': 'rx_text_message', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.MeshPacket', + '10': 'rxTextMessage' + }, + { + '1': 'no_save', + '3': 9, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'noSave', + }, + { + '1': 'did_gps_reset', + '3': 11, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'didGpsReset', + }, + { + '1': 'rx_waypoint', + '3': 12, + '4': 1, + '5': 11, + '6': '.meshtastic.MeshPacket', + '10': 'rxWaypoint' + }, + { + '1': 'node_remote_hardware_pins', + '3': 13, + '4': 3, + '5': 11, + '6': '.meshtastic.NodeRemoteHardwarePin', + '10': 'nodeRemoteHardwarePins' + }, + ], +}; + +/// Descriptor for `DeviceState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceStateDescriptor = $convert.base64Decode( + 'CgtEZXZpY2VTdGF0ZRIvCgdteV9ub2RlGAIgASgLMhYubWVzaHRhc3RpYy5NeU5vZGVJbmZvUg' + 'ZteU5vZGUSJgoFb3duZXIYAyABKAsyEC5tZXNodGFzdGljLlVzZXJSBW93bmVyEjsKDXJlY2Vp' + 'dmVfcXVldWUYBSADKAsyFi5tZXNodGFzdGljLk1lc2hQYWNrZXRSDHJlY2VpdmVRdWV1ZRIYCg' + 'd2ZXJzaW9uGAggASgNUgd2ZXJzaW9uEj4KD3J4X3RleHRfbWVzc2FnZRgHIAEoCzIWLm1lc2h0' + 'YXN0aWMuTWVzaFBhY2tldFINcnhUZXh0TWVzc2FnZRIbCgdub19zYXZlGAkgASgIQgIYAVIGbm' + '9TYXZlEiYKDWRpZF9ncHNfcmVzZXQYCyABKAhCAhgBUgtkaWRHcHNSZXNldBI3CgtyeF93YXlw' + 'b2ludBgMIAEoCzIWLm1lc2h0YXN0aWMuTWVzaFBhY2tldFIKcnhXYXlwb2ludBJcChlub2RlX3' + 'JlbW90ZV9oYXJkd2FyZV9waW5zGA0gAygLMiEubWVzaHRhc3RpYy5Ob2RlUmVtb3RlSGFyZHdh' + 'cmVQaW5SFm5vZGVSZW1vdGVIYXJkd2FyZVBpbnM='); + +@$core.Deprecated('Use nodeDatabaseDescriptor instead') +const NodeDatabase$json = { + '1': 'NodeDatabase', + '2': [ + {'1': 'version', '3': 1, '4': 1, '5': 13, '10': 'version'}, + { + '1': 'nodes', + '3': 2, + '4': 3, + '5': 11, + '6': '.meshtastic.NodeInfoLite', + '8': {}, + '10': 'nodes' + }, + ], +}; + +/// Descriptor for `NodeDatabase`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeDatabaseDescriptor = $convert.base64Decode( + 'CgxOb2RlRGF0YWJhc2USGAoHdmVyc2lvbhgBIAEoDVIHdmVyc2lvbhJaCgVub2RlcxgCIAMoCz' + 'IYLm1lc2h0YXN0aWMuTm9kZUluZm9MaXRlQiqSPyeSASRzdGQ6OnZlY3RvcjxtZXNodGFzdGlj' + 'X05vZGVJbmZvTGl0ZT5SBW5vZGVz'); + +@$core.Deprecated('Use channelFileDescriptor instead') +const ChannelFile$json = { + '1': 'ChannelFile', + '2': [ + { + '1': 'channels', + '3': 1, + '4': 3, + '5': 11, + '6': '.meshtastic.Channel', + '10': 'channels' + }, + {'1': 'version', '3': 2, '4': 1, '5': 13, '10': 'version'}, + ], +}; + +/// Descriptor for `ChannelFile`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List channelFileDescriptor = $convert.base64Decode( + 'CgtDaGFubmVsRmlsZRIvCghjaGFubmVscxgBIAMoCzITLm1lc2h0YXN0aWMuQ2hhbm5lbFIIY2' + 'hhbm5lbHMSGAoHdmVyc2lvbhgCIAEoDVIHdmVyc2lvbg=='); + +@$core.Deprecated('Use backupPreferencesDescriptor instead') +const BackupPreferences$json = { + '1': 'BackupPreferences', + '2': [ + {'1': 'version', '3': 1, '4': 1, '5': 13, '10': 'version'}, + {'1': 'timestamp', '3': 2, '4': 1, '5': 7, '10': 'timestamp'}, + { + '1': 'config', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.LocalConfig', + '10': 'config' + }, + { + '1': 'module_config', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.LocalModuleConfig', + '10': 'moduleConfig' + }, + { + '1': 'channels', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.ChannelFile', + '10': 'channels' + }, + { + '1': 'owner', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.User', + '10': 'owner' + }, + ], +}; + +/// Descriptor for `BackupPreferences`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List backupPreferencesDescriptor = $convert.base64Decode( + 'ChFCYWNrdXBQcmVmZXJlbmNlcxIYCgd2ZXJzaW9uGAEgASgNUgd2ZXJzaW9uEhwKCXRpbWVzdG' + 'FtcBgCIAEoB1IJdGltZXN0YW1wEi8KBmNvbmZpZxgDIAEoCzIXLm1lc2h0YXN0aWMuTG9jYWxD' + 'b25maWdSBmNvbmZpZxJCCg1tb2R1bGVfY29uZmlnGAQgASgLMh0ubWVzaHRhc3RpYy5Mb2NhbE' + '1vZHVsZUNvbmZpZ1IMbW9kdWxlQ29uZmlnEjMKCGNoYW5uZWxzGAUgASgLMhcubWVzaHRhc3Rp' + 'Yy5DaGFubmVsRmlsZVIIY2hhbm5lbHMSJgoFb3duZXIYBiABKAsyEC5tZXNodGFzdGljLlVzZX' + 'JSBW93bmVy'); diff --git a/third_party/meshtastic_flutter/lib/generated/interdevice.pb.dart b/third_party/meshtastic_flutter/lib/generated/interdevice.pb.dart new file mode 100644 index 000000000..1cb0ff8dc --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/interdevice.pb.dart @@ -0,0 +1,204 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/interdevice.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'interdevice.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'interdevice.pbenum.dart'; + +enum SensorData_Data { floatValue, uint32Value, notSet } + +class SensorData extends $pb.GeneratedMessage { + factory SensorData({ + MessageType? type, + $core.double? floatValue, + $core.int? uint32Value, + }) { + final result = create(); + if (type != null) result.type = type; + if (floatValue != null) result.floatValue = floatValue; + if (uint32Value != null) result.uint32Value = uint32Value; + return result; + } + + SensorData._(); + + factory SensorData.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory SensorData.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, SensorData_Data> _SensorData_DataByTag = { + 2: SensorData_Data.floatValue, + 3: SensorData_Data.uint32Value, + 0: SensorData_Data.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SensorData', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [2, 3]) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, + defaultOrMaker: MessageType.ACK, + valueOf: MessageType.valueOf, + enumValues: MessageType.values) + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'floatValue', $pb.PbFieldType.OF) + ..a<$core.int>(3, _omitFieldNames ? '' : 'uint32Value', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SensorData clone() => SensorData()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SensorData copyWith(void Function(SensorData) updates) => + super.copyWith((message) => updates(message as SensorData)) as SensorData; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorData create() => SensorData._(); + @$core.override + SensorData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorData getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SensorData? _defaultInstance; + + SensorData_Data whichData() => _SensorData_DataByTag[$_whichOneof(0)]!; + void clearData() => $_clearField($_whichOneof(0)); + + /// The message type + @$pb.TagNumber(1) + MessageType get type => $_getN(0); + @$pb.TagNumber(1) + set type(MessageType value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => $_clearField(1); + + @$pb.TagNumber(2) + $core.double get floatValue => $_getN(1); + @$pb.TagNumber(2) + set floatValue($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasFloatValue() => $_has(1); + @$pb.TagNumber(2) + void clearFloatValue() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get uint32Value => $_getIZ(2); + @$pb.TagNumber(3) + set uint32Value($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasUint32Value() => $_has(2); + @$pb.TagNumber(3) + void clearUint32Value() => $_clearField(3); +} + +enum InterdeviceMessage_Data { nmea, sensor, notSet } + +class InterdeviceMessage extends $pb.GeneratedMessage { + factory InterdeviceMessage({ + $core.String? nmea, + SensorData? sensor, + }) { + final result = create(); + if (nmea != null) result.nmea = nmea; + if (sensor != null) result.sensor = sensor; + return result; + } + + InterdeviceMessage._(); + + factory InterdeviceMessage.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory InterdeviceMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, InterdeviceMessage_Data> + _InterdeviceMessage_DataByTag = { + 1: InterdeviceMessage_Data.nmea, + 2: InterdeviceMessage_Data.sensor, + 0: InterdeviceMessage_Data.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'InterdeviceMessage', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOS(1, _omitFieldNames ? '' : 'nmea') + ..aOM(2, _omitFieldNames ? '' : 'sensor', + subBuilder: SensorData.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + InterdeviceMessage clone() => InterdeviceMessage()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + InterdeviceMessage copyWith(void Function(InterdeviceMessage) updates) => + super.copyWith((message) => updates(message as InterdeviceMessage)) + as InterdeviceMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static InterdeviceMessage create() => InterdeviceMessage._(); + @$core.override + InterdeviceMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static InterdeviceMessage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static InterdeviceMessage? _defaultInstance; + + InterdeviceMessage_Data whichData() => + _InterdeviceMessage_DataByTag[$_whichOneof(0)]!; + void clearData() => $_clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.String get nmea => $_getSZ(0); + @$pb.TagNumber(1) + set nmea($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasNmea() => $_has(0); + @$pb.TagNumber(1) + void clearNmea() => $_clearField(1); + + @$pb.TagNumber(2) + SensorData get sensor => $_getN(1); + @$pb.TagNumber(2) + set sensor(SensorData value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasSensor() => $_has(1); + @$pb.TagNumber(2) + void clearSensor() => $_clearField(2); + @$pb.TagNumber(2) + SensorData ensureSensor() => $_ensure(1); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/interdevice.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/interdevice.pbenum.dart new file mode 100644 index 000000000..c6dc5fc48 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/interdevice.pbenum.dart @@ -0,0 +1,65 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/interdevice.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class MessageType extends $pb.ProtobufEnum { + static const MessageType ACK = MessageType._(0, _omitEnumNames ? '' : 'ACK'); + static const MessageType COLLECT_INTERVAL = + MessageType._(160, _omitEnumNames ? '' : 'COLLECT_INTERVAL'); + static const MessageType BEEP_ON = + MessageType._(161, _omitEnumNames ? '' : 'BEEP_ON'); + static const MessageType BEEP_OFF = + MessageType._(162, _omitEnumNames ? '' : 'BEEP_OFF'); + static const MessageType SHUTDOWN = + MessageType._(163, _omitEnumNames ? '' : 'SHUTDOWN'); + static const MessageType POWER_ON = + MessageType._(164, _omitEnumNames ? '' : 'POWER_ON'); + static const MessageType SCD41_TEMP = + MessageType._(176, _omitEnumNames ? '' : 'SCD41_TEMP'); + static const MessageType SCD41_HUMIDITY = + MessageType._(177, _omitEnumNames ? '' : 'SCD41_HUMIDITY'); + static const MessageType SCD41_CO2 = + MessageType._(178, _omitEnumNames ? '' : 'SCD41_CO2'); + static const MessageType AHT20_TEMP = + MessageType._(179, _omitEnumNames ? '' : 'AHT20_TEMP'); + static const MessageType AHT20_HUMIDITY = + MessageType._(180, _omitEnumNames ? '' : 'AHT20_HUMIDITY'); + static const MessageType TVOC_INDEX = + MessageType._(181, _omitEnumNames ? '' : 'TVOC_INDEX'); + + static const $core.List values = [ + ACK, + COLLECT_INTERVAL, + BEEP_ON, + BEEP_OFF, + SHUTDOWN, + POWER_ON, + SCD41_TEMP, + SCD41_HUMIDITY, + SCD41_CO2, + AHT20_TEMP, + AHT20_HUMIDITY, + TVOC_INDEX, + ]; + + static final $core.Map<$core.int, MessageType> _byValue = + $pb.ProtobufEnum.initByValue(values); + static MessageType? valueOf($core.int value) => _byValue[value]; + + const MessageType._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/interdevice.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/interdevice.pbjson.dart new file mode 100644 index 000000000..f8877d5a6 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/interdevice.pbjson.dart @@ -0,0 +1,92 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/interdevice.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use messageTypeDescriptor instead') +const MessageType$json = { + '1': 'MessageType', + '2': [ + {'1': 'ACK', '2': 0}, + {'1': 'COLLECT_INTERVAL', '2': 160}, + {'1': 'BEEP_ON', '2': 161}, + {'1': 'BEEP_OFF', '2': 162}, + {'1': 'SHUTDOWN', '2': 163}, + {'1': 'POWER_ON', '2': 164}, + {'1': 'SCD41_TEMP', '2': 176}, + {'1': 'SCD41_HUMIDITY', '2': 177}, + {'1': 'SCD41_CO2', '2': 178}, + {'1': 'AHT20_TEMP', '2': 179}, + {'1': 'AHT20_HUMIDITY', '2': 180}, + {'1': 'TVOC_INDEX', '2': 181}, + ], +}; + +/// Descriptor for `MessageType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List messageTypeDescriptor = $convert.base64Decode( + 'CgtNZXNzYWdlVHlwZRIHCgNBQ0sQABIVChBDT0xMRUNUX0lOVEVSVkFMEKABEgwKB0JFRVBfT0' + '4QoQESDQoIQkVFUF9PRkYQogESDQoIU0hVVERPV04QowESDQoIUE9XRVJfT04QpAESDwoKU0NE' + 'NDFfVEVNUBCwARITCg5TQ0Q0MV9IVU1JRElUWRCxARIOCglTQ0Q0MV9DTzIQsgESDwoKQUhUMj' + 'BfVEVNUBCzARITCg5BSFQyMF9IVU1JRElUWRC0ARIPCgpUVk9DX0lOREVYELUB'); + +@$core.Deprecated('Use sensorDataDescriptor instead') +const SensorData$json = { + '1': 'SensorData', + '2': [ + { + '1': 'type', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.MessageType', + '10': 'type' + }, + {'1': 'float_value', '3': 2, '4': 1, '5': 2, '9': 0, '10': 'floatValue'}, + {'1': 'uint32_value', '3': 3, '4': 1, '5': 13, '9': 0, '10': 'uint32Value'}, + ], + '8': [ + {'1': 'data'}, + ], +}; + +/// Descriptor for `SensorData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorDataDescriptor = $convert.base64Decode( + 'CgpTZW5zb3JEYXRhEisKBHR5cGUYASABKA4yFy5tZXNodGFzdGljLk1lc3NhZ2VUeXBlUgR0eX' + 'BlEiEKC2Zsb2F0X3ZhbHVlGAIgASgCSABSCmZsb2F0VmFsdWUSIwoMdWludDMyX3ZhbHVlGAMg' + 'ASgNSABSC3VpbnQzMlZhbHVlQgYKBGRhdGE='); + +@$core.Deprecated('Use interdeviceMessageDescriptor instead') +const InterdeviceMessage$json = { + '1': 'InterdeviceMessage', + '2': [ + {'1': 'nmea', '3': 1, '4': 1, '5': 9, '9': 0, '10': 'nmea'}, + { + '1': 'sensor', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.SensorData', + '9': 0, + '10': 'sensor' + }, + ], + '8': [ + {'1': 'data'}, + ], +}; + +/// Descriptor for `InterdeviceMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List interdeviceMessageDescriptor = $convert.base64Decode( + 'ChJJbnRlcmRldmljZU1lc3NhZ2USFAoEbm1lYRgBIAEoCUgAUgRubWVhEjAKBnNlbnNvchgCIA' + 'EoCzIWLm1lc2h0YXN0aWMuU2Vuc29yRGF0YUgAUgZzZW5zb3JCBgoEZGF0YQ=='); diff --git a/third_party/meshtastic_flutter/lib/generated/localonly.pb.dart b/third_party/meshtastic_flutter/lib/generated/localonly.pb.dart new file mode 100644 index 000000000..00c8f42dc --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/localonly.pb.dart @@ -0,0 +1,522 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/localonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'config.pb.dart' as $0; +import 'module_config.pb.dart' as $1; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +class LocalConfig extends $pb.GeneratedMessage { + factory LocalConfig({ + $0.Config_DeviceConfig? device, + $0.Config_PositionConfig? position, + $0.Config_PowerConfig? power, + $0.Config_NetworkConfig? network, + $0.Config_DisplayConfig? display, + $0.Config_LoRaConfig? lora, + $0.Config_BluetoothConfig? bluetooth, + $core.int? version, + $0.Config_SecurityConfig? security, + }) { + final result = create(); + if (device != null) result.device = device; + if (position != null) result.position = position; + if (power != null) result.power = power; + if (network != null) result.network = network; + if (display != null) result.display = display; + if (lora != null) result.lora = lora; + if (bluetooth != null) result.bluetooth = bluetooth; + if (version != null) result.version = version; + if (security != null) result.security = security; + return result; + } + + LocalConfig._(); + + factory LocalConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory LocalConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'LocalConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM<$0.Config_DeviceConfig>(1, _omitFieldNames ? '' : 'device', + subBuilder: $0.Config_DeviceConfig.create) + ..aOM<$0.Config_PositionConfig>(2, _omitFieldNames ? '' : 'position', + subBuilder: $0.Config_PositionConfig.create) + ..aOM<$0.Config_PowerConfig>(3, _omitFieldNames ? '' : 'power', + subBuilder: $0.Config_PowerConfig.create) + ..aOM<$0.Config_NetworkConfig>(4, _omitFieldNames ? '' : 'network', + subBuilder: $0.Config_NetworkConfig.create) + ..aOM<$0.Config_DisplayConfig>(5, _omitFieldNames ? '' : 'display', + subBuilder: $0.Config_DisplayConfig.create) + ..aOM<$0.Config_LoRaConfig>(6, _omitFieldNames ? '' : 'lora', + subBuilder: $0.Config_LoRaConfig.create) + ..aOM<$0.Config_BluetoothConfig>(7, _omitFieldNames ? '' : 'bluetooth', + subBuilder: $0.Config_BluetoothConfig.create) + ..a<$core.int>(8, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..aOM<$0.Config_SecurityConfig>(9, _omitFieldNames ? '' : 'security', + subBuilder: $0.Config_SecurityConfig.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LocalConfig clone() => LocalConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LocalConfig copyWith(void Function(LocalConfig) updates) => + super.copyWith((message) => updates(message as LocalConfig)) + as LocalConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LocalConfig create() => LocalConfig._(); + @$core.override + LocalConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LocalConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static LocalConfig? _defaultInstance; + + /// + /// The part of the config that is specific to the Device + @$pb.TagNumber(1) + $0.Config_DeviceConfig get device => $_getN(0); + @$pb.TagNumber(1) + set device($0.Config_DeviceConfig value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasDevice() => $_has(0); + @$pb.TagNumber(1) + void clearDevice() => $_clearField(1); + @$pb.TagNumber(1) + $0.Config_DeviceConfig ensureDevice() => $_ensure(0); + + /// + /// The part of the config that is specific to the GPS Position + @$pb.TagNumber(2) + $0.Config_PositionConfig get position => $_getN(1); + @$pb.TagNumber(2) + set position($0.Config_PositionConfig value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasPosition() => $_has(1); + @$pb.TagNumber(2) + void clearPosition() => $_clearField(2); + @$pb.TagNumber(2) + $0.Config_PositionConfig ensurePosition() => $_ensure(1); + + /// + /// The part of the config that is specific to the Power settings + @$pb.TagNumber(3) + $0.Config_PowerConfig get power => $_getN(2); + @$pb.TagNumber(3) + set power($0.Config_PowerConfig value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasPower() => $_has(2); + @$pb.TagNumber(3) + void clearPower() => $_clearField(3); + @$pb.TagNumber(3) + $0.Config_PowerConfig ensurePower() => $_ensure(2); + + /// + /// The part of the config that is specific to the Wifi Settings + @$pb.TagNumber(4) + $0.Config_NetworkConfig get network => $_getN(3); + @$pb.TagNumber(4) + set network($0.Config_NetworkConfig value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasNetwork() => $_has(3); + @$pb.TagNumber(4) + void clearNetwork() => $_clearField(4); + @$pb.TagNumber(4) + $0.Config_NetworkConfig ensureNetwork() => $_ensure(3); + + /// + /// The part of the config that is specific to the Display + @$pb.TagNumber(5) + $0.Config_DisplayConfig get display => $_getN(4); + @$pb.TagNumber(5) + set display($0.Config_DisplayConfig value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasDisplay() => $_has(4); + @$pb.TagNumber(5) + void clearDisplay() => $_clearField(5); + @$pb.TagNumber(5) + $0.Config_DisplayConfig ensureDisplay() => $_ensure(4); + + /// + /// The part of the config that is specific to the Lora Radio + @$pb.TagNumber(6) + $0.Config_LoRaConfig get lora => $_getN(5); + @$pb.TagNumber(6) + set lora($0.Config_LoRaConfig value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasLora() => $_has(5); + @$pb.TagNumber(6) + void clearLora() => $_clearField(6); + @$pb.TagNumber(6) + $0.Config_LoRaConfig ensureLora() => $_ensure(5); + + /// + /// The part of the config that is specific to the Bluetooth settings + @$pb.TagNumber(7) + $0.Config_BluetoothConfig get bluetooth => $_getN(6); + @$pb.TagNumber(7) + set bluetooth($0.Config_BluetoothConfig value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasBluetooth() => $_has(6); + @$pb.TagNumber(7) + void clearBluetooth() => $_clearField(7); + @$pb.TagNumber(7) + $0.Config_BluetoothConfig ensureBluetooth() => $_ensure(6); + + /// + /// A version integer used to invalidate old save files when we make + /// incompatible changes This integer is set at build time and is private to + /// NodeDB.cpp in the device code. + @$pb.TagNumber(8) + $core.int get version => $_getIZ(7); + @$pb.TagNumber(8) + set version($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasVersion() => $_has(7); + @$pb.TagNumber(8) + void clearVersion() => $_clearField(8); + + /// + /// The part of the config that is specific to Security settings + @$pb.TagNumber(9) + $0.Config_SecurityConfig get security => $_getN(8); + @$pb.TagNumber(9) + set security($0.Config_SecurityConfig value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasSecurity() => $_has(8); + @$pb.TagNumber(9) + void clearSecurity() => $_clearField(9); + @$pb.TagNumber(9) + $0.Config_SecurityConfig ensureSecurity() => $_ensure(8); +} + +class LocalModuleConfig extends $pb.GeneratedMessage { + factory LocalModuleConfig({ + $1.ModuleConfig_MQTTConfig? mqtt, + $1.ModuleConfig_SerialConfig? serial, + $1.ModuleConfig_ExternalNotificationConfig? externalNotification, + $1.ModuleConfig_StoreForwardConfig? storeForward, + $1.ModuleConfig_RangeTestConfig? rangeTest, + $1.ModuleConfig_TelemetryConfig? telemetry, + $1.ModuleConfig_CannedMessageConfig? cannedMessage, + $core.int? version, + $1.ModuleConfig_AudioConfig? audio, + $1.ModuleConfig_RemoteHardwareConfig? remoteHardware, + $1.ModuleConfig_NeighborInfoConfig? neighborInfo, + $1.ModuleConfig_AmbientLightingConfig? ambientLighting, + $1.ModuleConfig_DetectionSensorConfig? detectionSensor, + $1.ModuleConfig_PaxcounterConfig? paxcounter, + }) { + final result = create(); + if (mqtt != null) result.mqtt = mqtt; + if (serial != null) result.serial = serial; + if (externalNotification != null) + result.externalNotification = externalNotification; + if (storeForward != null) result.storeForward = storeForward; + if (rangeTest != null) result.rangeTest = rangeTest; + if (telemetry != null) result.telemetry = telemetry; + if (cannedMessage != null) result.cannedMessage = cannedMessage; + if (version != null) result.version = version; + if (audio != null) result.audio = audio; + if (remoteHardware != null) result.remoteHardware = remoteHardware; + if (neighborInfo != null) result.neighborInfo = neighborInfo; + if (ambientLighting != null) result.ambientLighting = ambientLighting; + if (detectionSensor != null) result.detectionSensor = detectionSensor; + if (paxcounter != null) result.paxcounter = paxcounter; + return result; + } + + LocalModuleConfig._(); + + factory LocalModuleConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory LocalModuleConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'LocalModuleConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM<$1.ModuleConfig_MQTTConfig>(1, _omitFieldNames ? '' : 'mqtt', + subBuilder: $1.ModuleConfig_MQTTConfig.create) + ..aOM<$1.ModuleConfig_SerialConfig>(2, _omitFieldNames ? '' : 'serial', + subBuilder: $1.ModuleConfig_SerialConfig.create) + ..aOM<$1.ModuleConfig_ExternalNotificationConfig>( + 3, _omitFieldNames ? '' : 'externalNotification', + subBuilder: $1.ModuleConfig_ExternalNotificationConfig.create) + ..aOM<$1.ModuleConfig_StoreForwardConfig>( + 4, _omitFieldNames ? '' : 'storeForward', + subBuilder: $1.ModuleConfig_StoreForwardConfig.create) + ..aOM<$1.ModuleConfig_RangeTestConfig>( + 5, _omitFieldNames ? '' : 'rangeTest', + subBuilder: $1.ModuleConfig_RangeTestConfig.create) + ..aOM<$1.ModuleConfig_TelemetryConfig>( + 6, _omitFieldNames ? '' : 'telemetry', + subBuilder: $1.ModuleConfig_TelemetryConfig.create) + ..aOM<$1.ModuleConfig_CannedMessageConfig>( + 7, _omitFieldNames ? '' : 'cannedMessage', + subBuilder: $1.ModuleConfig_CannedMessageConfig.create) + ..a<$core.int>(8, _omitFieldNames ? '' : 'version', $pb.PbFieldType.OU3) + ..aOM<$1.ModuleConfig_AudioConfig>(9, _omitFieldNames ? '' : 'audio', + subBuilder: $1.ModuleConfig_AudioConfig.create) + ..aOM<$1.ModuleConfig_RemoteHardwareConfig>( + 10, _omitFieldNames ? '' : 'remoteHardware', + subBuilder: $1.ModuleConfig_RemoteHardwareConfig.create) + ..aOM<$1.ModuleConfig_NeighborInfoConfig>( + 11, _omitFieldNames ? '' : 'neighborInfo', + subBuilder: $1.ModuleConfig_NeighborInfoConfig.create) + ..aOM<$1.ModuleConfig_AmbientLightingConfig>( + 12, _omitFieldNames ? '' : 'ambientLighting', + subBuilder: $1.ModuleConfig_AmbientLightingConfig.create) + ..aOM<$1.ModuleConfig_DetectionSensorConfig>( + 13, _omitFieldNames ? '' : 'detectionSensor', + subBuilder: $1.ModuleConfig_DetectionSensorConfig.create) + ..aOM<$1.ModuleConfig_PaxcounterConfig>( + 14, _omitFieldNames ? '' : 'paxcounter', + subBuilder: $1.ModuleConfig_PaxcounterConfig.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LocalModuleConfig clone() => LocalModuleConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LocalModuleConfig copyWith(void Function(LocalModuleConfig) updates) => + super.copyWith((message) => updates(message as LocalModuleConfig)) + as LocalModuleConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LocalModuleConfig create() => LocalModuleConfig._(); + @$core.override + LocalModuleConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LocalModuleConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static LocalModuleConfig? _defaultInstance; + + /// + /// The part of the config that is specific to the MQTT module + @$pb.TagNumber(1) + $1.ModuleConfig_MQTTConfig get mqtt => $_getN(0); + @$pb.TagNumber(1) + set mqtt($1.ModuleConfig_MQTTConfig value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasMqtt() => $_has(0); + @$pb.TagNumber(1) + void clearMqtt() => $_clearField(1); + @$pb.TagNumber(1) + $1.ModuleConfig_MQTTConfig ensureMqtt() => $_ensure(0); + + /// + /// The part of the config that is specific to the Serial module + @$pb.TagNumber(2) + $1.ModuleConfig_SerialConfig get serial => $_getN(1); + @$pb.TagNumber(2) + set serial($1.ModuleConfig_SerialConfig value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasSerial() => $_has(1); + @$pb.TagNumber(2) + void clearSerial() => $_clearField(2); + @$pb.TagNumber(2) + $1.ModuleConfig_SerialConfig ensureSerial() => $_ensure(1); + + /// + /// The part of the config that is specific to the ExternalNotification module + @$pb.TagNumber(3) + $1.ModuleConfig_ExternalNotificationConfig get externalNotification => + $_getN(2); + @$pb.TagNumber(3) + set externalNotification($1.ModuleConfig_ExternalNotificationConfig value) => + $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasExternalNotification() => $_has(2); + @$pb.TagNumber(3) + void clearExternalNotification() => $_clearField(3); + @$pb.TagNumber(3) + $1.ModuleConfig_ExternalNotificationConfig ensureExternalNotification() => + $_ensure(2); + + /// + /// The part of the config that is specific to the Store & Forward module + @$pb.TagNumber(4) + $1.ModuleConfig_StoreForwardConfig get storeForward => $_getN(3); + @$pb.TagNumber(4) + set storeForward($1.ModuleConfig_StoreForwardConfig value) => + $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasStoreForward() => $_has(3); + @$pb.TagNumber(4) + void clearStoreForward() => $_clearField(4); + @$pb.TagNumber(4) + $1.ModuleConfig_StoreForwardConfig ensureStoreForward() => $_ensure(3); + + /// + /// The part of the config that is specific to the RangeTest module + @$pb.TagNumber(5) + $1.ModuleConfig_RangeTestConfig get rangeTest => $_getN(4); + @$pb.TagNumber(5) + set rangeTest($1.ModuleConfig_RangeTestConfig value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasRangeTest() => $_has(4); + @$pb.TagNumber(5) + void clearRangeTest() => $_clearField(5); + @$pb.TagNumber(5) + $1.ModuleConfig_RangeTestConfig ensureRangeTest() => $_ensure(4); + + /// + /// The part of the config that is specific to the Telemetry module + @$pb.TagNumber(6) + $1.ModuleConfig_TelemetryConfig get telemetry => $_getN(5); + @$pb.TagNumber(6) + set telemetry($1.ModuleConfig_TelemetryConfig value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasTelemetry() => $_has(5); + @$pb.TagNumber(6) + void clearTelemetry() => $_clearField(6); + @$pb.TagNumber(6) + $1.ModuleConfig_TelemetryConfig ensureTelemetry() => $_ensure(5); + + /// + /// The part of the config that is specific to the Canned Message module + @$pb.TagNumber(7) + $1.ModuleConfig_CannedMessageConfig get cannedMessage => $_getN(6); + @$pb.TagNumber(7) + set cannedMessage($1.ModuleConfig_CannedMessageConfig value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasCannedMessage() => $_has(6); + @$pb.TagNumber(7) + void clearCannedMessage() => $_clearField(7); + @$pb.TagNumber(7) + $1.ModuleConfig_CannedMessageConfig ensureCannedMessage() => $_ensure(6); + + /// + /// A version integer used to invalidate old save files when we make + /// incompatible changes This integer is set at build time and is private to + /// NodeDB.cpp in the device code. + @$pb.TagNumber(8) + $core.int get version => $_getIZ(7); + @$pb.TagNumber(8) + set version($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasVersion() => $_has(7); + @$pb.TagNumber(8) + void clearVersion() => $_clearField(8); + + /// + /// The part of the config that is specific to the Audio module + @$pb.TagNumber(9) + $1.ModuleConfig_AudioConfig get audio => $_getN(8); + @$pb.TagNumber(9) + set audio($1.ModuleConfig_AudioConfig value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasAudio() => $_has(8); + @$pb.TagNumber(9) + void clearAudio() => $_clearField(9); + @$pb.TagNumber(9) + $1.ModuleConfig_AudioConfig ensureAudio() => $_ensure(8); + + /// + /// The part of the config that is specific to the Remote Hardware module + @$pb.TagNumber(10) + $1.ModuleConfig_RemoteHardwareConfig get remoteHardware => $_getN(9); + @$pb.TagNumber(10) + set remoteHardware($1.ModuleConfig_RemoteHardwareConfig value) => + $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasRemoteHardware() => $_has(9); + @$pb.TagNumber(10) + void clearRemoteHardware() => $_clearField(10); + @$pb.TagNumber(10) + $1.ModuleConfig_RemoteHardwareConfig ensureRemoteHardware() => $_ensure(9); + + /// + /// The part of the config that is specific to the Neighbor Info module + @$pb.TagNumber(11) + $1.ModuleConfig_NeighborInfoConfig get neighborInfo => $_getN(10); + @$pb.TagNumber(11) + set neighborInfo($1.ModuleConfig_NeighborInfoConfig value) => + $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasNeighborInfo() => $_has(10); + @$pb.TagNumber(11) + void clearNeighborInfo() => $_clearField(11); + @$pb.TagNumber(11) + $1.ModuleConfig_NeighborInfoConfig ensureNeighborInfo() => $_ensure(10); + + /// + /// The part of the config that is specific to the Ambient Lighting module + @$pb.TagNumber(12) + $1.ModuleConfig_AmbientLightingConfig get ambientLighting => $_getN(11); + @$pb.TagNumber(12) + set ambientLighting($1.ModuleConfig_AmbientLightingConfig value) => + $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasAmbientLighting() => $_has(11); + @$pb.TagNumber(12) + void clearAmbientLighting() => $_clearField(12); + @$pb.TagNumber(12) + $1.ModuleConfig_AmbientLightingConfig ensureAmbientLighting() => $_ensure(11); + + /// + /// The part of the config that is specific to the Detection Sensor module + @$pb.TagNumber(13) + $1.ModuleConfig_DetectionSensorConfig get detectionSensor => $_getN(12); + @$pb.TagNumber(13) + set detectionSensor($1.ModuleConfig_DetectionSensorConfig value) => + $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasDetectionSensor() => $_has(12); + @$pb.TagNumber(13) + void clearDetectionSensor() => $_clearField(13); + @$pb.TagNumber(13) + $1.ModuleConfig_DetectionSensorConfig ensureDetectionSensor() => $_ensure(12); + + /// + /// Paxcounter Config + @$pb.TagNumber(14) + $1.ModuleConfig_PaxcounterConfig get paxcounter => $_getN(13); + @$pb.TagNumber(14) + set paxcounter($1.ModuleConfig_PaxcounterConfig value) => + $_setField(14, value); + @$pb.TagNumber(14) + $core.bool hasPaxcounter() => $_has(13); + @$pb.TagNumber(14) + void clearPaxcounter() => $_clearField(14); + @$pb.TagNumber(14) + $1.ModuleConfig_PaxcounterConfig ensurePaxcounter() => $_ensure(13); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/localonly.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/localonly.pbenum.dart new file mode 100644 index 000000000..8a6065dcc --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/localonly.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/localonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/localonly.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/localonly.pbjson.dart new file mode 100644 index 000000000..624237c54 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/localonly.pbjson.dart @@ -0,0 +1,235 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/localonly.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use localConfigDescriptor instead') +const LocalConfig$json = { + '1': 'LocalConfig', + '2': [ + { + '1': 'device', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.DeviceConfig', + '10': 'device' + }, + { + '1': 'position', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.PositionConfig', + '10': 'position' + }, + { + '1': 'power', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.PowerConfig', + '10': 'power' + }, + { + '1': 'network', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.NetworkConfig', + '10': 'network' + }, + { + '1': 'display', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.DisplayConfig', + '10': 'display' + }, + { + '1': 'lora', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.LoRaConfig', + '10': 'lora' + }, + { + '1': 'bluetooth', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.BluetoothConfig', + '10': 'bluetooth' + }, + {'1': 'version', '3': 8, '4': 1, '5': 13, '10': 'version'}, + { + '1': 'security', + '3': 9, + '4': 1, + '5': 11, + '6': '.meshtastic.Config.SecurityConfig', + '10': 'security' + }, + ], +}; + +/// Descriptor for `LocalConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List localConfigDescriptor = $convert.base64Decode( + 'CgtMb2NhbENvbmZpZxI3CgZkZXZpY2UYASABKAsyHy5tZXNodGFzdGljLkNvbmZpZy5EZXZpY2' + 'VDb25maWdSBmRldmljZRI9Cghwb3NpdGlvbhgCIAEoCzIhLm1lc2h0YXN0aWMuQ29uZmlnLlBv' + 'c2l0aW9uQ29uZmlnUghwb3NpdGlvbhI0CgVwb3dlchgDIAEoCzIeLm1lc2h0YXN0aWMuQ29uZm' + 'lnLlBvd2VyQ29uZmlnUgVwb3dlchI6CgduZXR3b3JrGAQgASgLMiAubWVzaHRhc3RpYy5Db25m' + 'aWcuTmV0d29ya0NvbmZpZ1IHbmV0d29yaxI6CgdkaXNwbGF5GAUgASgLMiAubWVzaHRhc3RpYy' + '5Db25maWcuRGlzcGxheUNvbmZpZ1IHZGlzcGxheRIxCgRsb3JhGAYgASgLMh0ubWVzaHRhc3Rp' + 'Yy5Db25maWcuTG9SYUNvbmZpZ1IEbG9yYRJACglibHVldG9vdGgYByABKAsyIi5tZXNodGFzdG' + 'ljLkNvbmZpZy5CbHVldG9vdGhDb25maWdSCWJsdWV0b290aBIYCgd2ZXJzaW9uGAggASgNUgd2' + 'ZXJzaW9uEj0KCHNlY3VyaXR5GAkgASgLMiEubWVzaHRhc3RpYy5Db25maWcuU2VjdXJpdHlDb2' + '5maWdSCHNlY3VyaXR5'); + +@$core.Deprecated('Use localModuleConfigDescriptor instead') +const LocalModuleConfig$json = { + '1': 'LocalModuleConfig', + '2': [ + { + '1': 'mqtt', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.MQTTConfig', + '10': 'mqtt' + }, + { + '1': 'serial', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.SerialConfig', + '10': 'serial' + }, + { + '1': 'external_notification', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.ExternalNotificationConfig', + '10': 'externalNotification' + }, + { + '1': 'store_forward', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.StoreForwardConfig', + '10': 'storeForward' + }, + { + '1': 'range_test', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.RangeTestConfig', + '10': 'rangeTest' + }, + { + '1': 'telemetry', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.TelemetryConfig', + '10': 'telemetry' + }, + { + '1': 'canned_message', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.CannedMessageConfig', + '10': 'cannedMessage' + }, + { + '1': 'audio', + '3': 9, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.AudioConfig', + '10': 'audio' + }, + { + '1': 'remote_hardware', + '3': 10, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.RemoteHardwareConfig', + '10': 'remoteHardware' + }, + { + '1': 'neighbor_info', + '3': 11, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.NeighborInfoConfig', + '10': 'neighborInfo' + }, + { + '1': 'ambient_lighting', + '3': 12, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.AmbientLightingConfig', + '10': 'ambientLighting' + }, + { + '1': 'detection_sensor', + '3': 13, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.DetectionSensorConfig', + '10': 'detectionSensor' + }, + { + '1': 'paxcounter', + '3': 14, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.PaxcounterConfig', + '10': 'paxcounter' + }, + {'1': 'version', '3': 8, '4': 1, '5': 13, '10': 'version'}, + ], +}; + +/// Descriptor for `LocalModuleConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List localModuleConfigDescriptor = $convert.base64Decode( + 'ChFMb2NhbE1vZHVsZUNvbmZpZxI3CgRtcXR0GAEgASgLMiMubWVzaHRhc3RpYy5Nb2R1bGVDb2' + '5maWcuTVFUVENvbmZpZ1IEbXF0dBI9CgZzZXJpYWwYAiABKAsyJS5tZXNodGFzdGljLk1vZHVs' + 'ZUNvbmZpZy5TZXJpYWxDb25maWdSBnNlcmlhbBJoChVleHRlcm5hbF9ub3RpZmljYXRpb24YAy' + 'ABKAsyMy5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5FeHRlcm5hbE5vdGlmaWNhdGlvbkNvbmZp' + 'Z1IUZXh0ZXJuYWxOb3RpZmljYXRpb24SUAoNc3RvcmVfZm9yd2FyZBgEIAEoCzIrLm1lc2h0YX' + 'N0aWMuTW9kdWxlQ29uZmlnLlN0b3JlRm9yd2FyZENvbmZpZ1IMc3RvcmVGb3J3YXJkEkcKCnJh' + 'bmdlX3Rlc3QYBSABKAsyKC5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5SYW5nZVRlc3RDb25maW' + 'dSCXJhbmdlVGVzdBJGCgl0ZWxlbWV0cnkYBiABKAsyKC5tZXNodGFzdGljLk1vZHVsZUNvbmZp' + 'Zy5UZWxlbWV0cnlDb25maWdSCXRlbGVtZXRyeRJTCg5jYW5uZWRfbWVzc2FnZRgHIAEoCzIsLm' + '1lc2h0YXN0aWMuTW9kdWxlQ29uZmlnLkNhbm5lZE1lc3NhZ2VDb25maWdSDWNhbm5lZE1lc3Nh' + 'Z2USOgoFYXVkaW8YCSABKAsyJC5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5BdWRpb0NvbmZpZ1' + 'IFYXVkaW8SVgoPcmVtb3RlX2hhcmR3YXJlGAogASgLMi0ubWVzaHRhc3RpYy5Nb2R1bGVDb25m' + 'aWcuUmVtb3RlSGFyZHdhcmVDb25maWdSDnJlbW90ZUhhcmR3YXJlElAKDW5laWdoYm9yX2luZm' + '8YCyABKAsyKy5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5OZWlnaGJvckluZm9Db25maWdSDG5l' + 'aWdoYm9ySW5mbxJZChBhbWJpZW50X2xpZ2h0aW5nGAwgASgLMi4ubWVzaHRhc3RpYy5Nb2R1bG' + 'VDb25maWcuQW1iaWVudExpZ2h0aW5nQ29uZmlnUg9hbWJpZW50TGlnaHRpbmcSWQoQZGV0ZWN0' + 'aW9uX3NlbnNvchgNIAEoCzIuLm1lc2h0YXN0aWMuTW9kdWxlQ29uZmlnLkRldGVjdGlvblNlbn' + 'NvckNvbmZpZ1IPZGV0ZWN0aW9uU2Vuc29yEkkKCnBheGNvdW50ZXIYDiABKAsyKS5tZXNodGFz' + 'dGljLk1vZHVsZUNvbmZpZy5QYXhjb3VudGVyQ29uZmlnUgpwYXhjb3VudGVyEhgKB3ZlcnNpb2' + '4YCCABKA1SB3ZlcnNpb24='); diff --git a/third_party/meshtastic_flutter/lib/generated/mesh.pb.dart b/third_party/meshtastic_flutter/lib/generated/mesh.pb.dart new file mode 100644 index 000000000..3908e81c2 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/mesh.pb.dart @@ -0,0 +1,4519 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/mesh.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'channel.pb.dart' as $3; +import 'config.pb.dart' as $1; +import 'device_ui.pb.dart' as $5; +import 'mesh.pbenum.dart'; +import 'module_config.pb.dart' as $2; +import 'portnums.pbenum.dart' as $6; +import 'telemetry.pb.dart' as $0; +import 'xmodem.pb.dart' as $4; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'mesh.pbenum.dart'; + +/// +/// A GPS Position +class Position extends $pb.GeneratedMessage { + factory Position({ + $core.int? latitudeI, + $core.int? longitudeI, + $core.int? altitude, + $core.int? time, + Position_LocSource? locationSource, + Position_AltSource? altitudeSource, + $core.int? timestamp, + $core.int? timestampMillisAdjust, + $core.int? altitudeHae, + $core.int? altitudeGeoidalSeparation, + $core.int? pDOP, + $core.int? hDOP, + $core.int? vDOP, + $core.int? gpsAccuracy, + $core.int? groundSpeed, + $core.int? groundTrack, + $core.int? fixQuality, + $core.int? fixType, + $core.int? satsInView, + $core.int? sensorId, + $core.int? nextUpdate, + $core.int? seqNumber, + $core.int? precisionBits, + }) { + final result = create(); + if (latitudeI != null) result.latitudeI = latitudeI; + if (longitudeI != null) result.longitudeI = longitudeI; + if (altitude != null) result.altitude = altitude; + if (time != null) result.time = time; + if (locationSource != null) result.locationSource = locationSource; + if (altitudeSource != null) result.altitudeSource = altitudeSource; + if (timestamp != null) result.timestamp = timestamp; + if (timestampMillisAdjust != null) + result.timestampMillisAdjust = timestampMillisAdjust; + if (altitudeHae != null) result.altitudeHae = altitudeHae; + if (altitudeGeoidalSeparation != null) + result.altitudeGeoidalSeparation = altitudeGeoidalSeparation; + if (pDOP != null) result.pDOP = pDOP; + if (hDOP != null) result.hDOP = hDOP; + if (vDOP != null) result.vDOP = vDOP; + if (gpsAccuracy != null) result.gpsAccuracy = gpsAccuracy; + if (groundSpeed != null) result.groundSpeed = groundSpeed; + if (groundTrack != null) result.groundTrack = groundTrack; + if (fixQuality != null) result.fixQuality = fixQuality; + if (fixType != null) result.fixType = fixType; + if (satsInView != null) result.satsInView = satsInView; + if (sensorId != null) result.sensorId = sensorId; + if (nextUpdate != null) result.nextUpdate = nextUpdate; + if (seqNumber != null) result.seqNumber = seqNumber; + if (precisionBits != null) result.precisionBits = precisionBits; + return result; + } + + Position._(); + + factory Position.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Position.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Position', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'latitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'longitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'altitude', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'time', $pb.PbFieldType.OF3) + ..e( + 5, _omitFieldNames ? '' : 'locationSource', $pb.PbFieldType.OE, + defaultOrMaker: Position_LocSource.LOC_UNSET, + valueOf: Position_LocSource.valueOf, + enumValues: Position_LocSource.values) + ..e( + 6, _omitFieldNames ? '' : 'altitudeSource', $pb.PbFieldType.OE, + defaultOrMaker: Position_AltSource.ALT_UNSET, + valueOf: Position_AltSource.valueOf, + enumValues: Position_AltSource.values) + ..a<$core.int>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OF3) + ..a<$core.int>( + 8, _omitFieldNames ? '' : 'timestampMillisAdjust', $pb.PbFieldType.O3) + ..a<$core.int>(9, _omitFieldNames ? '' : 'altitudeHae', $pb.PbFieldType.OS3) + ..a<$core.int>(10, _omitFieldNames ? '' : 'altitudeGeoidalSeparation', + $pb.PbFieldType.OS3) + ..a<$core.int>(11, _omitFieldNames ? '' : 'PDOP', $pb.PbFieldType.OU3, + protoName: 'PDOP') + ..a<$core.int>(12, _omitFieldNames ? '' : 'HDOP', $pb.PbFieldType.OU3, + protoName: 'HDOP') + ..a<$core.int>(13, _omitFieldNames ? '' : 'VDOP', $pb.PbFieldType.OU3, + protoName: 'VDOP') + ..a<$core.int>( + 14, _omitFieldNames ? '' : 'gpsAccuracy', $pb.PbFieldType.OU3) + ..a<$core.int>( + 15, _omitFieldNames ? '' : 'groundSpeed', $pb.PbFieldType.OU3) + ..a<$core.int>( + 16, _omitFieldNames ? '' : 'groundTrack', $pb.PbFieldType.OU3) + ..a<$core.int>(17, _omitFieldNames ? '' : 'fixQuality', $pb.PbFieldType.OU3) + ..a<$core.int>(18, _omitFieldNames ? '' : 'fixType', $pb.PbFieldType.OU3) + ..a<$core.int>(19, _omitFieldNames ? '' : 'satsInView', $pb.PbFieldType.OU3) + ..a<$core.int>(20, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3) + ..a<$core.int>(21, _omitFieldNames ? '' : 'nextUpdate', $pb.PbFieldType.OU3) + ..a<$core.int>(22, _omitFieldNames ? '' : 'seqNumber', $pb.PbFieldType.OU3) + ..a<$core.int>( + 23, _omitFieldNames ? '' : 'precisionBits', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Position clone() => Position()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Position copyWith(void Function(Position) updates) => + super.copyWith((message) => updates(message as Position)) as Position; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Position create() => Position._(); + @$core.override + Position createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Position getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Position? _defaultInstance; + + /// + /// The new preferred location encoding, multiply by 1e-7 to get degrees + /// in floating point + @$pb.TagNumber(1) + $core.int get latitudeI => $_getIZ(0); + @$pb.TagNumber(1) + set latitudeI($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasLatitudeI() => $_has(0); + @$pb.TagNumber(1) + void clearLatitudeI() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + $core.int get longitudeI => $_getIZ(1); + @$pb.TagNumber(2) + set longitudeI($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasLongitudeI() => $_has(1); + @$pb.TagNumber(2) + void clearLongitudeI() => $_clearField(2); + + /// + /// In meters above MSL (but see issue #359) + @$pb.TagNumber(3) + $core.int get altitude => $_getIZ(2); + @$pb.TagNumber(3) + set altitude($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasAltitude() => $_has(2); + @$pb.TagNumber(3) + void clearAltitude() => $_clearField(3); + + /// + /// This is usually not sent over the mesh (to save space), but it is sent + /// from the phone so that the local device can set its time if it is sent over + /// the mesh (because there are devices on the mesh without GPS or RTC). + /// seconds since 1970 + @$pb.TagNumber(4) + $core.int get time => $_getIZ(3); + @$pb.TagNumber(4) + set time($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasTime() => $_has(3); + @$pb.TagNumber(4) + void clearTime() => $_clearField(4); + + /// + /// TODO: REPLACE + @$pb.TagNumber(5) + Position_LocSource get locationSource => $_getN(4); + @$pb.TagNumber(5) + set locationSource(Position_LocSource value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasLocationSource() => $_has(4); + @$pb.TagNumber(5) + void clearLocationSource() => $_clearField(5); + + /// + /// TODO: REPLACE + @$pb.TagNumber(6) + Position_AltSource get altitudeSource => $_getN(5); + @$pb.TagNumber(6) + set altitudeSource(Position_AltSource value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasAltitudeSource() => $_has(5); + @$pb.TagNumber(6) + void clearAltitudeSource() => $_clearField(6); + + /// + /// Positional timestamp (actual timestamp of GPS solution) in integer epoch seconds + @$pb.TagNumber(7) + $core.int get timestamp => $_getIZ(6); + @$pb.TagNumber(7) + set timestamp($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasTimestamp() => $_has(6); + @$pb.TagNumber(7) + void clearTimestamp() => $_clearField(7); + + /// + /// Pos. timestamp milliseconds adjustment (rarely available or required) + @$pb.TagNumber(8) + $core.int get timestampMillisAdjust => $_getIZ(7); + @$pb.TagNumber(8) + set timestampMillisAdjust($core.int value) => $_setSignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasTimestampMillisAdjust() => $_has(7); + @$pb.TagNumber(8) + void clearTimestampMillisAdjust() => $_clearField(8); + + /// + /// HAE altitude in meters - can be used instead of MSL altitude + @$pb.TagNumber(9) + $core.int get altitudeHae => $_getIZ(8); + @$pb.TagNumber(9) + set altitudeHae($core.int value) => $_setSignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasAltitudeHae() => $_has(8); + @$pb.TagNumber(9) + void clearAltitudeHae() => $_clearField(9); + + /// + /// Geoidal separation in meters + @$pb.TagNumber(10) + $core.int get altitudeGeoidalSeparation => $_getIZ(9); + @$pb.TagNumber(10) + set altitudeGeoidalSeparation($core.int value) => $_setSignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasAltitudeGeoidalSeparation() => $_has(9); + @$pb.TagNumber(10) + void clearAltitudeGeoidalSeparation() => $_clearField(10); + + /// + /// Horizontal, Vertical and Position Dilution of Precision, in 1/100 units + /// - PDOP is sufficient for most cases + /// - for higher precision scenarios, HDOP and VDOP can be used instead, + /// in which case PDOP becomes redundant (PDOP=sqrt(HDOP^2 + VDOP^2)) + /// TODO: REMOVE/INTEGRATE + @$pb.TagNumber(11) + $core.int get pDOP => $_getIZ(10); + @$pb.TagNumber(11) + set pDOP($core.int value) => $_setUnsignedInt32(10, value); + @$pb.TagNumber(11) + $core.bool hasPDOP() => $_has(10); + @$pb.TagNumber(11) + void clearPDOP() => $_clearField(11); + + /// + /// TODO: REPLACE + @$pb.TagNumber(12) + $core.int get hDOP => $_getIZ(11); + @$pb.TagNumber(12) + set hDOP($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasHDOP() => $_has(11); + @$pb.TagNumber(12) + void clearHDOP() => $_clearField(12); + + /// + /// TODO: REPLACE + @$pb.TagNumber(13) + $core.int get vDOP => $_getIZ(12); + @$pb.TagNumber(13) + set vDOP($core.int value) => $_setUnsignedInt32(12, value); + @$pb.TagNumber(13) + $core.bool hasVDOP() => $_has(12); + @$pb.TagNumber(13) + void clearVDOP() => $_clearField(13); + + /// + /// GPS accuracy (a hardware specific constant) in mm + /// multiplied with DOP to calculate positional accuracy + /// Default: "'bout three meters-ish" :) + @$pb.TagNumber(14) + $core.int get gpsAccuracy => $_getIZ(13); + @$pb.TagNumber(14) + set gpsAccuracy($core.int value) => $_setUnsignedInt32(13, value); + @$pb.TagNumber(14) + $core.bool hasGpsAccuracy() => $_has(13); + @$pb.TagNumber(14) + void clearGpsAccuracy() => $_clearField(14); + + /// + /// Ground speed in m/s and True North TRACK in 1/100 degrees + /// Clarification of terms: + /// - "track" is the direction of motion (measured in horizontal plane) + /// - "heading" is where the fuselage points (measured in horizontal plane) + /// - "yaw" indicates a relative rotation about the vertical axis + /// TODO: REMOVE/INTEGRATE + @$pb.TagNumber(15) + $core.int get groundSpeed => $_getIZ(14); + @$pb.TagNumber(15) + set groundSpeed($core.int value) => $_setUnsignedInt32(14, value); + @$pb.TagNumber(15) + $core.bool hasGroundSpeed() => $_has(14); + @$pb.TagNumber(15) + void clearGroundSpeed() => $_clearField(15); + + /// + /// TODO: REPLACE + @$pb.TagNumber(16) + $core.int get groundTrack => $_getIZ(15); + @$pb.TagNumber(16) + set groundTrack($core.int value) => $_setUnsignedInt32(15, value); + @$pb.TagNumber(16) + $core.bool hasGroundTrack() => $_has(15); + @$pb.TagNumber(16) + void clearGroundTrack() => $_clearField(16); + + /// + /// GPS fix quality (from NMEA GxGGA statement or similar) + @$pb.TagNumber(17) + $core.int get fixQuality => $_getIZ(16); + @$pb.TagNumber(17) + set fixQuality($core.int value) => $_setUnsignedInt32(16, value); + @$pb.TagNumber(17) + $core.bool hasFixQuality() => $_has(16); + @$pb.TagNumber(17) + void clearFixQuality() => $_clearField(17); + + /// + /// GPS fix type 2D/3D (from NMEA GxGSA statement) + @$pb.TagNumber(18) + $core.int get fixType => $_getIZ(17); + @$pb.TagNumber(18) + set fixType($core.int value) => $_setUnsignedInt32(17, value); + @$pb.TagNumber(18) + $core.bool hasFixType() => $_has(17); + @$pb.TagNumber(18) + void clearFixType() => $_clearField(18); + + /// + /// GPS "Satellites in View" number + @$pb.TagNumber(19) + $core.int get satsInView => $_getIZ(18); + @$pb.TagNumber(19) + set satsInView($core.int value) => $_setUnsignedInt32(18, value); + @$pb.TagNumber(19) + $core.bool hasSatsInView() => $_has(18); + @$pb.TagNumber(19) + void clearSatsInView() => $_clearField(19); + + /// + /// Sensor ID - in case multiple positioning sensors are being used + @$pb.TagNumber(20) + $core.int get sensorId => $_getIZ(19); + @$pb.TagNumber(20) + set sensorId($core.int value) => $_setUnsignedInt32(19, value); + @$pb.TagNumber(20) + $core.bool hasSensorId() => $_has(19); + @$pb.TagNumber(20) + void clearSensorId() => $_clearField(20); + + /// + /// Estimated/expected time (in seconds) until next update: + /// - if we update at fixed intervals of X seconds, use X + /// - if we update at dynamic intervals (based on relative movement etc), + /// but "AT LEAST every Y seconds", use Y + @$pb.TagNumber(21) + $core.int get nextUpdate => $_getIZ(20); + @$pb.TagNumber(21) + set nextUpdate($core.int value) => $_setUnsignedInt32(20, value); + @$pb.TagNumber(21) + $core.bool hasNextUpdate() => $_has(20); + @$pb.TagNumber(21) + void clearNextUpdate() => $_clearField(21); + + /// + /// A sequence number, incremented with each Position message to help + /// detect lost updates if needed + @$pb.TagNumber(22) + $core.int get seqNumber => $_getIZ(21); + @$pb.TagNumber(22) + set seqNumber($core.int value) => $_setUnsignedInt32(21, value); + @$pb.TagNumber(22) + $core.bool hasSeqNumber() => $_has(21); + @$pb.TagNumber(22) + void clearSeqNumber() => $_clearField(22); + + /// + /// Indicates the bits of precision set by the sending node + @$pb.TagNumber(23) + $core.int get precisionBits => $_getIZ(22); + @$pb.TagNumber(23) + set precisionBits($core.int value) => $_setUnsignedInt32(22, value); + @$pb.TagNumber(23) + $core.bool hasPrecisionBits() => $_has(22); + @$pb.TagNumber(23) + void clearPrecisionBits() => $_clearField(23); +} + +/// +/// Broadcast when a newly powered mesh node wants to find a node num it can use +/// Sent from the phone over bluetooth to set the user id for the owner of this node. +/// Also sent from nodes to each other when a new node signs on (so all clients can have this info) +/// The algorithm is as follows: +/// when a node starts up, it broadcasts their user and the normal flow is for all +/// other nodes to reply with their User as well (so the new node can build its nodedb) +/// If a node ever receives a User (not just the first broadcast) message where +/// the sender node number equals our node number, that indicates a collision has +/// occurred and the following steps should happen: +/// If the receiving node (that was already in the mesh)'s macaddr is LOWER than the +/// new User who just tried to sign in: it gets to keep its nodenum. +/// We send a broadcast message of OUR User (we use a broadcast so that the other node can +/// receive our message, considering we have the same id - it also serves to let +/// observers correct their nodedb) - this case is rare so it should be okay. +/// If any node receives a User where the macaddr is GTE than their local macaddr, +/// they have been vetoed and should pick a new random nodenum (filtering against +/// whatever it knows about the nodedb) and rebroadcast their User. +/// A few nodenums are reserved and will never be requested: +/// 0xff - broadcast +/// 0 through 3 - for future use +class User extends $pb.GeneratedMessage { + factory User({ + $core.String? id, + $core.String? longName, + $core.String? shortName, + @$core.Deprecated('This field is deprecated.') + $core.List<$core.int>? macaddr, + HardwareModel? hwModel, + $core.bool? isLicensed, + $1.Config_DeviceConfig_Role? role, + $core.List<$core.int>? publicKey, + $core.bool? isUnmessagable, + }) { + final result = create(); + if (id != null) result.id = id; + if (longName != null) result.longName = longName; + if (shortName != null) result.shortName = shortName; + if (macaddr != null) result.macaddr = macaddr; + if (hwModel != null) result.hwModel = hwModel; + if (isLicensed != null) result.isLicensed = isLicensed; + if (role != null) result.role = role; + if (publicKey != null) result.publicKey = publicKey; + if (isUnmessagable != null) result.isUnmessagable = isUnmessagable; + return result; + } + + User._(); + + factory User.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory User.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'User', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'id') + ..aOS(2, _omitFieldNames ? '' : 'longName') + ..aOS(3, _omitFieldNames ? '' : 'shortName') + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'macaddr', $pb.PbFieldType.OY) + ..e(5, _omitFieldNames ? '' : 'hwModel', $pb.PbFieldType.OE, + defaultOrMaker: HardwareModel.UNSET, + valueOf: HardwareModel.valueOf, + enumValues: HardwareModel.values) + ..aOB(6, _omitFieldNames ? '' : 'isLicensed') + ..e<$1.Config_DeviceConfig_Role>( + 7, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: $1.Config_DeviceConfig_Role.CLIENT, + valueOf: $1.Config_DeviceConfig_Role.valueOf, + enumValues: $1.Config_DeviceConfig_Role.values) + ..a<$core.List<$core.int>>( + 8, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..aOB(9, _omitFieldNames ? '' : 'isUnmessagable') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + User clone() => User()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + User copyWith(void Function(User) updates) => + super.copyWith((message) => updates(message as User)) as User; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static User create() => User._(); + @$core.override + User createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static User getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static User? _defaultInstance; + + /// + /// A globally unique ID string for this user. + /// In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexidecimal bytes>. + /// Note: app developers are encouraged to also use the following standard + /// node IDs "^all" (for broadcast), "^local" (for the locally connected node) + @$pb.TagNumber(1) + $core.String get id => $_getSZ(0); + @$pb.TagNumber(1) + set id($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => $_clearField(1); + + /// + /// A full name for this user, i.e. "Kevin Hester" + @$pb.TagNumber(2) + $core.String get longName => $_getSZ(1); + @$pb.TagNumber(2) + set longName($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasLongName() => $_has(1); + @$pb.TagNumber(2) + void clearLongName() => $_clearField(2); + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + @$pb.TagNumber(3) + $core.String get shortName => $_getSZ(2); + @$pb.TagNumber(3) + set shortName($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasShortName() => $_has(2); + @$pb.TagNumber(3) + void clearShortName() => $_clearField(3); + + /// + /// Deprecated in Meshtastic 2.1.x + /// This is the addr of the radio. + /// Not populated by the phone, but added by the esp32 when broadcasting + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + $core.List<$core.int> get macaddr => $_getN(3); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + set macaddr($core.List<$core.int> value) => $_setBytes(3, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + $core.bool hasMacaddr() => $_has(3); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(4) + void clearMacaddr() => $_clearField(4); + + /// + /// TBEAM, HELTEC, etc... + /// Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. + /// Apps will still need the string here for older builds + /// (so OTA update can find the right image), but if the enum is available it will be used instead. + @$pb.TagNumber(5) + HardwareModel get hwModel => $_getN(4); + @$pb.TagNumber(5) + set hwModel(HardwareModel value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasHwModel() => $_has(4); + @$pb.TagNumber(5) + void clearHwModel() => $_clearField(5); + + /// + /// In some regions Ham radio operators have different bandwidth limitations than others. + /// If this user is a licensed operator, set this flag. + /// Also, "long_name" should be their licence number. + @$pb.TagNumber(6) + $core.bool get isLicensed => $_getBF(5); + @$pb.TagNumber(6) + set isLicensed($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasIsLicensed() => $_has(5); + @$pb.TagNumber(6) + void clearIsLicensed() => $_clearField(6); + + /// + /// Indicates that the user's role in the mesh + @$pb.TagNumber(7) + $1.Config_DeviceConfig_Role get role => $_getN(6); + @$pb.TagNumber(7) + set role($1.Config_DeviceConfig_Role value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasRole() => $_has(6); + @$pb.TagNumber(7) + void clearRole() => $_clearField(7); + + /// + /// The public key of the user's device. + /// This is sent out to other nodes on the mesh to allow them to compute a shared secret key. + @$pb.TagNumber(8) + $core.List<$core.int> get publicKey => $_getN(7); + @$pb.TagNumber(8) + set publicKey($core.List<$core.int> value) => $_setBytes(7, value); + @$pb.TagNumber(8) + $core.bool hasPublicKey() => $_has(7); + @$pb.TagNumber(8) + void clearPublicKey() => $_clearField(8); + + /// + /// Whether or not the node can be messaged + @$pb.TagNumber(9) + $core.bool get isUnmessagable => $_getBF(8); + @$pb.TagNumber(9) + set isUnmessagable($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasIsUnmessagable() => $_has(8); + @$pb.TagNumber(9) + void clearIsUnmessagable() => $_clearField(9); +} + +/// +/// A message used in a traceroute +class RouteDiscovery extends $pb.GeneratedMessage { + factory RouteDiscovery({ + $core.Iterable<$core.int>? route, + $core.Iterable<$core.int>? snrTowards, + $core.Iterable<$core.int>? routeBack, + $core.Iterable<$core.int>? snrBack, + }) { + final result = create(); + if (route != null) result.route.addAll(route); + if (snrTowards != null) result.snrTowards.addAll(snrTowards); + if (routeBack != null) result.routeBack.addAll(routeBack); + if (snrBack != null) result.snrBack.addAll(snrBack); + return result; + } + + RouteDiscovery._(); + + factory RouteDiscovery.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RouteDiscovery.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RouteDiscovery', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..p<$core.int>(1, _omitFieldNames ? '' : 'route', $pb.PbFieldType.KF3) + ..p<$core.int>(2, _omitFieldNames ? '' : 'snrTowards', $pb.PbFieldType.K3) + ..p<$core.int>(3, _omitFieldNames ? '' : 'routeBack', $pb.PbFieldType.KF3) + ..p<$core.int>(4, _omitFieldNames ? '' : 'snrBack', $pb.PbFieldType.K3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RouteDiscovery clone() => RouteDiscovery()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RouteDiscovery copyWith(void Function(RouteDiscovery) updates) => + super.copyWith((message) => updates(message as RouteDiscovery)) + as RouteDiscovery; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RouteDiscovery create() => RouteDiscovery._(); + @$core.override + RouteDiscovery createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RouteDiscovery getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RouteDiscovery? _defaultInstance; + + /// + /// The list of nodenums this packet has visited so far to the destination. + @$pb.TagNumber(1) + $pb.PbList<$core.int> get route => $_getList(0); + + /// + /// The list of SNRs (in dB, scaled by 4) in the route towards the destination. + @$pb.TagNumber(2) + $pb.PbList<$core.int> get snrTowards => $_getList(1); + + /// + /// The list of nodenums the packet has visited on the way back from the destination. + @$pb.TagNumber(3) + $pb.PbList<$core.int> get routeBack => $_getList(2); + + /// + /// The list of SNRs (in dB, scaled by 4) in the route back from the destination. + @$pb.TagNumber(4) + $pb.PbList<$core.int> get snrBack => $_getList(3); +} + +enum Routing_Variant { routeRequest, routeReply, errorReason, notSet } + +/// +/// A Routing control Data packet handled by the routing module +class Routing extends $pb.GeneratedMessage { + factory Routing({ + RouteDiscovery? routeRequest, + RouteDiscovery? routeReply, + Routing_Error? errorReason, + }) { + final result = create(); + if (routeRequest != null) result.routeRequest = routeRequest; + if (routeReply != null) result.routeReply = routeReply; + if (errorReason != null) result.errorReason = errorReason; + return result; + } + + Routing._(); + + factory Routing.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Routing.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, Routing_Variant> _Routing_VariantByTag = { + 1: Routing_Variant.routeRequest, + 2: Routing_Variant.routeReply, + 3: Routing_Variant.errorReason, + 0: Routing_Variant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Routing', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [1, 2, 3]) + ..aOM(1, _omitFieldNames ? '' : 'routeRequest', + subBuilder: RouteDiscovery.create) + ..aOM(2, _omitFieldNames ? '' : 'routeReply', + subBuilder: RouteDiscovery.create) + ..e( + 3, _omitFieldNames ? '' : 'errorReason', $pb.PbFieldType.OE, + defaultOrMaker: Routing_Error.NONE, + valueOf: Routing_Error.valueOf, + enumValues: Routing_Error.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Routing clone() => Routing()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Routing copyWith(void Function(Routing) updates) => + super.copyWith((message) => updates(message as Routing)) as Routing; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Routing create() => Routing._(); + @$core.override + Routing createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Routing getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Routing? _defaultInstance; + + Routing_Variant whichVariant() => _Routing_VariantByTag[$_whichOneof(0)]!; + void clearVariant() => $_clearField($_whichOneof(0)); + + /// + /// A route request going from the requester + @$pb.TagNumber(1) + RouteDiscovery get routeRequest => $_getN(0); + @$pb.TagNumber(1) + set routeRequest(RouteDiscovery value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasRouteRequest() => $_has(0); + @$pb.TagNumber(1) + void clearRouteRequest() => $_clearField(1); + @$pb.TagNumber(1) + RouteDiscovery ensureRouteRequest() => $_ensure(0); + + /// + /// A route reply + @$pb.TagNumber(2) + RouteDiscovery get routeReply => $_getN(1); + @$pb.TagNumber(2) + set routeReply(RouteDiscovery value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasRouteReply() => $_has(1); + @$pb.TagNumber(2) + void clearRouteReply() => $_clearField(2); + @$pb.TagNumber(2) + RouteDiscovery ensureRouteReply() => $_ensure(1); + + /// + /// A failure in delivering a message (usually used for routing control messages, but might be provided + /// in addition to ack.fail_id to provide details on the type of failure). + @$pb.TagNumber(3) + Routing_Error get errorReason => $_getN(2); + @$pb.TagNumber(3) + set errorReason(Routing_Error value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasErrorReason() => $_has(2); + @$pb.TagNumber(3) + void clearErrorReason() => $_clearField(3); +} + +/// +/// (Formerly called SubPacket) +/// The payload portion fo a packet, this is the actual bytes that are sent +/// inside a radio packet (because from/to are broken out by the comms library) +class Data extends $pb.GeneratedMessage { + factory Data({ + $6.PortNum? portnum, + $core.List<$core.int>? payload, + $core.bool? wantResponse, + $core.int? dest, + $core.int? source, + $core.int? requestId, + $core.int? replyId, + $core.int? emoji, + $core.int? bitfield, + }) { + final result = create(); + if (portnum != null) result.portnum = portnum; + if (payload != null) result.payload = payload; + if (wantResponse != null) result.wantResponse = wantResponse; + if (dest != null) result.dest = dest; + if (source != null) result.source = source; + if (requestId != null) result.requestId = requestId; + if (replyId != null) result.replyId = replyId; + if (emoji != null) result.emoji = emoji; + if (bitfield != null) result.bitfield = bitfield; + return result; + } + + Data._(); + + factory Data.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Data.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Data', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e<$6.PortNum>(1, _omitFieldNames ? '' : 'portnum', $pb.PbFieldType.OE, + defaultOrMaker: $6.PortNum.UNKNOWN_APP, + valueOf: $6.PortNum.valueOf, + enumValues: $6.PortNum.values) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'payload', $pb.PbFieldType.OY) + ..aOB(3, _omitFieldNames ? '' : 'wantResponse') + ..a<$core.int>(4, _omitFieldNames ? '' : 'dest', $pb.PbFieldType.OF3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'source', $pb.PbFieldType.OF3) + ..a<$core.int>(6, _omitFieldNames ? '' : 'requestId', $pb.PbFieldType.OF3) + ..a<$core.int>(7, _omitFieldNames ? '' : 'replyId', $pb.PbFieldType.OF3) + ..a<$core.int>(8, _omitFieldNames ? '' : 'emoji', $pb.PbFieldType.OF3) + ..a<$core.int>(9, _omitFieldNames ? '' : 'bitfield', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Data clone() => Data()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Data copyWith(void Function(Data) updates) => + super.copyWith((message) => updates(message as Data)) as Data; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Data create() => Data._(); + @$core.override + Data createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Data getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Data? _defaultInstance; + + /// + /// Formerly named typ and of type Type + @$pb.TagNumber(1) + $6.PortNum get portnum => $_getN(0); + @$pb.TagNumber(1) + set portnum($6.PortNum value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasPortnum() => $_has(0); + @$pb.TagNumber(1) + void clearPortnum() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + $core.List<$core.int> get payload => $_getN(1); + @$pb.TagNumber(2) + set payload($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasPayload() => $_has(1); + @$pb.TagNumber(2) + void clearPayload() => $_clearField(2); + + /// + /// Not normally used, but for testing a sender can request that recipient + /// responds in kind (i.e. if it received a position, it should unicast back it's position). + /// Note: that if you set this on a broadcast you will receive many replies. + @$pb.TagNumber(3) + $core.bool get wantResponse => $_getBF(2); + @$pb.TagNumber(3) + set wantResponse($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasWantResponse() => $_has(2); + @$pb.TagNumber(3) + void clearWantResponse() => $_clearField(3); + + /// + /// The address of the destination node. + /// This field is is filled in by the mesh radio device software, application + /// layer software should never need it. + /// RouteDiscovery messages _must_ populate this. + /// Other message types might need to if they are doing multihop routing. + @$pb.TagNumber(4) + $core.int get dest => $_getIZ(3); + @$pb.TagNumber(4) + set dest($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasDest() => $_has(3); + @$pb.TagNumber(4) + void clearDest() => $_clearField(4); + + /// + /// The address of the original sender for this message. + /// This field should _only_ be populated for reliable multihop packets (to keep + /// packets small). + @$pb.TagNumber(5) + $core.int get source => $_getIZ(4); + @$pb.TagNumber(5) + set source($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasSource() => $_has(4); + @$pb.TagNumber(5) + void clearSource() => $_clearField(5); + + /// + /// Only used in routing or response messages. + /// Indicates the original message ID that this message is reporting failure on. (formerly called original_id) + @$pb.TagNumber(6) + $core.int get requestId => $_getIZ(5); + @$pb.TagNumber(6) + set requestId($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasRequestId() => $_has(5); + @$pb.TagNumber(6) + void clearRequestId() => $_clearField(6); + + /// + /// If set, this message is intened to be a reply to a previously sent message with the defined id. + @$pb.TagNumber(7) + $core.int get replyId => $_getIZ(6); + @$pb.TagNumber(7) + set replyId($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasReplyId() => $_has(6); + @$pb.TagNumber(7) + void clearReplyId() => $_clearField(7); + + /// + /// Defaults to false. If true, then what is in the payload should be treated as an emoji like giving + /// a message a heart or poop emoji. + @$pb.TagNumber(8) + $core.int get emoji => $_getIZ(7); + @$pb.TagNumber(8) + set emoji($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasEmoji() => $_has(7); + @$pb.TagNumber(8) + void clearEmoji() => $_clearField(8); + + /// + /// Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. + @$pb.TagNumber(9) + $core.int get bitfield => $_getIZ(8); + @$pb.TagNumber(9) + set bitfield($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasBitfield() => $_has(8); + @$pb.TagNumber(9) + void clearBitfield() => $_clearField(9); +} + +/// +/// The actual over-the-mesh message doing KeyVerification +class KeyVerification extends $pb.GeneratedMessage { + factory KeyVerification({ + $fixnum.Int64? nonce, + $core.List<$core.int>? hash1, + $core.List<$core.int>? hash2, + }) { + final result = create(); + if (nonce != null) result.nonce = nonce; + if (hash1 != null) result.hash1 = hash1; + if (hash2 != null) result.hash2 = hash2; + return result; + } + + KeyVerification._(); + + factory KeyVerification.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory KeyVerification.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'KeyVerification', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'hash1', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 3, _omitFieldNames ? '' : 'hash2', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerification clone() => KeyVerification()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerification copyWith(void Function(KeyVerification) updates) => + super.copyWith((message) => updates(message as KeyVerification)) + as KeyVerification; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static KeyVerification create() => KeyVerification._(); + @$core.override + KeyVerification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static KeyVerification getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static KeyVerification? _defaultInstance; + + /// + /// random value Selected by the requesting node + @$pb.TagNumber(1) + $fixnum.Int64 get nonce => $_getI64(0); + @$pb.TagNumber(1) + set nonce($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => $_clearField(1); + + /// + /// The final authoritative hash, only to be sent by NodeA at the end of the handshake + @$pb.TagNumber(2) + $core.List<$core.int> get hash1 => $_getN(1); + @$pb.TagNumber(2) + set hash1($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasHash1() => $_has(1); + @$pb.TagNumber(2) + void clearHash1() => $_clearField(2); + + /// + /// The intermediary hash (actually derived from hash1), + /// sent from NodeB to NodeA in response to the initial message. + @$pb.TagNumber(3) + $core.List<$core.int> get hash2 => $_getN(2); + @$pb.TagNumber(3) + set hash2($core.List<$core.int> value) => $_setBytes(2, value); + @$pb.TagNumber(3) + $core.bool hasHash2() => $_has(2); + @$pb.TagNumber(3) + void clearHash2() => $_clearField(3); +} + +/// +/// Waypoint message, used to share arbitrary locations across the mesh +class Waypoint extends $pb.GeneratedMessage { + factory Waypoint({ + $core.int? id, + $core.int? latitudeI, + $core.int? longitudeI, + $core.int? expire, + $core.int? lockedTo, + $core.String? name, + $core.String? description, + $core.int? icon, + }) { + final result = create(); + if (id != null) result.id = id; + if (latitudeI != null) result.latitudeI = latitudeI; + if (longitudeI != null) result.longitudeI = longitudeI; + if (expire != null) result.expire = expire; + if (lockedTo != null) result.lockedTo = lockedTo; + if (name != null) result.name = name; + if (description != null) result.description = description; + if (icon != null) result.icon = icon; + return result; + } + + Waypoint._(); + + factory Waypoint.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Waypoint.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Waypoint', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'latitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'longitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'expire', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'lockedTo', $pb.PbFieldType.OU3) + ..aOS(6, _omitFieldNames ? '' : 'name') + ..aOS(7, _omitFieldNames ? '' : 'description') + ..a<$core.int>(8, _omitFieldNames ? '' : 'icon', $pb.PbFieldType.OF3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Waypoint clone() => Waypoint()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Waypoint copyWith(void Function(Waypoint) updates) => + super.copyWith((message) => updates(message as Waypoint)) as Waypoint; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Waypoint create() => Waypoint._(); + @$core.override + Waypoint createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Waypoint getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Waypoint? _defaultInstance; + + /// + /// Id of the waypoint + @$pb.TagNumber(1) + $core.int get id => $_getIZ(0); + @$pb.TagNumber(1) + set id($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => $_clearField(1); + + /// + /// latitude_i + @$pb.TagNumber(2) + $core.int get latitudeI => $_getIZ(1); + @$pb.TagNumber(2) + set latitudeI($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasLatitudeI() => $_has(1); + @$pb.TagNumber(2) + void clearLatitudeI() => $_clearField(2); + + /// + /// longitude_i + @$pb.TagNumber(3) + $core.int get longitudeI => $_getIZ(2); + @$pb.TagNumber(3) + set longitudeI($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasLongitudeI() => $_has(2); + @$pb.TagNumber(3) + void clearLongitudeI() => $_clearField(3); + + /// + /// Time the waypoint is to expire (epoch) + @$pb.TagNumber(4) + $core.int get expire => $_getIZ(3); + @$pb.TagNumber(4) + set expire($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasExpire() => $_has(3); + @$pb.TagNumber(4) + void clearExpire() => $_clearField(4); + + /// + /// If greater than zero, treat the value as a nodenum only allowing them to update the waypoint. + /// If zero, the waypoint is open to be edited by any member of the mesh. + @$pb.TagNumber(5) + $core.int get lockedTo => $_getIZ(4); + @$pb.TagNumber(5) + set lockedTo($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasLockedTo() => $_has(4); + @$pb.TagNumber(5) + void clearLockedTo() => $_clearField(5); + + /// + /// Name of the waypoint - max 30 chars + @$pb.TagNumber(6) + $core.String get name => $_getSZ(5); + @$pb.TagNumber(6) + set name($core.String value) => $_setString(5, value); + @$pb.TagNumber(6) + $core.bool hasName() => $_has(5); + @$pb.TagNumber(6) + void clearName() => $_clearField(6); + + /// + /// Description of the waypoint - max 100 chars + @$pb.TagNumber(7) + $core.String get description => $_getSZ(6); + @$pb.TagNumber(7) + set description($core.String value) => $_setString(6, value); + @$pb.TagNumber(7) + $core.bool hasDescription() => $_has(6); + @$pb.TagNumber(7) + void clearDescription() => $_clearField(7); + + /// + /// Designator icon for the waypoint in the form of a unicode emoji + @$pb.TagNumber(8) + $core.int get icon => $_getIZ(7); + @$pb.TagNumber(8) + set icon($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasIcon() => $_has(7); + @$pb.TagNumber(8) + void clearIcon() => $_clearField(8); +} + +enum MqttClientProxyMessage_PayloadVariant { data, text, notSet } + +/// +/// This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server +class MqttClientProxyMessage extends $pb.GeneratedMessage { + factory MqttClientProxyMessage({ + $core.String? topic, + $core.List<$core.int>? data, + $core.String? text, + $core.bool? retained, + }) { + final result = create(); + if (topic != null) result.topic = topic; + if (data != null) result.data = data; + if (text != null) result.text = text; + if (retained != null) result.retained = retained; + return result; + } + + MqttClientProxyMessage._(); + + factory MqttClientProxyMessage.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory MqttClientProxyMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, MqttClientProxyMessage_PayloadVariant> + _MqttClientProxyMessage_PayloadVariantByTag = { + 2: MqttClientProxyMessage_PayloadVariant.data, + 3: MqttClientProxyMessage_PayloadVariant.text, + 0: MqttClientProxyMessage_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'MqttClientProxyMessage', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [2, 3]) + ..aOS(1, _omitFieldNames ? '' : 'topic') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'text') + ..aOB(4, _omitFieldNames ? '' : 'retained') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MqttClientProxyMessage clone() => + MqttClientProxyMessage()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MqttClientProxyMessage copyWith( + void Function(MqttClientProxyMessage) updates) => + super.copyWith((message) => updates(message as MqttClientProxyMessage)) + as MqttClientProxyMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static MqttClientProxyMessage create() => MqttClientProxyMessage._(); + @$core.override + MqttClientProxyMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static MqttClientProxyMessage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static MqttClientProxyMessage? _defaultInstance; + + MqttClientProxyMessage_PayloadVariant whichPayloadVariant() => + _MqttClientProxyMessage_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// The MQTT topic this message will be sent /received on + @$pb.TagNumber(1) + $core.String get topic => $_getSZ(0); + @$pb.TagNumber(1) + set topic($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasTopic() => $_has(0); + @$pb.TagNumber(1) + void clearTopic() => $_clearField(1); + + /// + /// Bytes + @$pb.TagNumber(2) + $core.List<$core.int> get data => $_getN(1); + @$pb.TagNumber(2) + set data($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasData() => $_has(1); + @$pb.TagNumber(2) + void clearData() => $_clearField(2); + + /// + /// Text + @$pb.TagNumber(3) + $core.String get text => $_getSZ(2); + @$pb.TagNumber(3) + set text($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasText() => $_has(2); + @$pb.TagNumber(3) + void clearText() => $_clearField(3); + + /// + /// Whether the message should be retained (or not) + @$pb.TagNumber(4) + $core.bool get retained => $_getBF(3); + @$pb.TagNumber(4) + set retained($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasRetained() => $_has(3); + @$pb.TagNumber(4) + void clearRetained() => $_clearField(4); +} + +enum MeshPacket_PayloadVariant { decoded, encrypted, notSet } + +/// +/// A packet envelope sent/received over the mesh +/// only payload_variant is sent in the payload portion of the LORA packet. +/// The other fields are either not sent at all, or sent in the special 16 byte LORA header. +class MeshPacket extends $pb.GeneratedMessage { + factory MeshPacket({ + $core.int? from, + $core.int? to, + $core.int? channel, + Data? decoded, + $core.List<$core.int>? encrypted, + $core.int? id, + $core.int? rxTime, + $core.double? rxSnr, + $core.int? hopLimit, + $core.bool? wantAck, + MeshPacket_Priority? priority, + $core.int? rxRssi, + @$core.Deprecated('This field is deprecated.') MeshPacket_Delayed? delayed, + $core.bool? viaMqtt, + $core.int? hopStart, + $core.List<$core.int>? publicKey, + $core.bool? pkiEncrypted, + $core.int? nextHop, + $core.int? relayNode, + $core.int? txAfter, + MeshPacket_TransportMechanism? transportMechanism, + }) { + final result = create(); + if (from != null) result.from = from; + if (to != null) result.to = to; + if (channel != null) result.channel = channel; + if (decoded != null) result.decoded = decoded; + if (encrypted != null) result.encrypted = encrypted; + if (id != null) result.id = id; + if (rxTime != null) result.rxTime = rxTime; + if (rxSnr != null) result.rxSnr = rxSnr; + if (hopLimit != null) result.hopLimit = hopLimit; + if (wantAck != null) result.wantAck = wantAck; + if (priority != null) result.priority = priority; + if (rxRssi != null) result.rxRssi = rxRssi; + if (delayed != null) result.delayed = delayed; + if (viaMqtt != null) result.viaMqtt = viaMqtt; + if (hopStart != null) result.hopStart = hopStart; + if (publicKey != null) result.publicKey = publicKey; + if (pkiEncrypted != null) result.pkiEncrypted = pkiEncrypted; + if (nextHop != null) result.nextHop = nextHop; + if (relayNode != null) result.relayNode = relayNode; + if (txAfter != null) result.txAfter = txAfter; + if (transportMechanism != null) + result.transportMechanism = transportMechanism; + return result; + } + + MeshPacket._(); + + factory MeshPacket.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory MeshPacket.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, MeshPacket_PayloadVariant> + _MeshPacket_PayloadVariantByTag = { + 4: MeshPacket_PayloadVariant.decoded, + 5: MeshPacket_PayloadVariant.encrypted, + 0: MeshPacket_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'MeshPacket', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [4, 5]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'from', $pb.PbFieldType.OF3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'to', $pb.PbFieldType.OF3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'channel', $pb.PbFieldType.OU3) + ..aOM(4, _omitFieldNames ? '' : 'decoded', subBuilder: Data.create) + ..a<$core.List<$core.int>>( + 5, _omitFieldNames ? '' : 'encrypted', $pb.PbFieldType.OY) + ..a<$core.int>(6, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OF3) + ..a<$core.int>(7, _omitFieldNames ? '' : 'rxTime', $pb.PbFieldType.OF3) + ..a<$core.double>(8, _omitFieldNames ? '' : 'rxSnr', $pb.PbFieldType.OF) + ..a<$core.int>(9, _omitFieldNames ? '' : 'hopLimit', $pb.PbFieldType.OU3) + ..aOB(10, _omitFieldNames ? '' : 'wantAck') + ..e( + 11, _omitFieldNames ? '' : 'priority', $pb.PbFieldType.OE, + defaultOrMaker: MeshPacket_Priority.UNSET, + valueOf: MeshPacket_Priority.valueOf, + enumValues: MeshPacket_Priority.values) + ..a<$core.int>(12, _omitFieldNames ? '' : 'rxRssi', $pb.PbFieldType.O3) + ..e( + 13, _omitFieldNames ? '' : 'delayed', $pb.PbFieldType.OE, + defaultOrMaker: MeshPacket_Delayed.NO_DELAY, + valueOf: MeshPacket_Delayed.valueOf, + enumValues: MeshPacket_Delayed.values) + ..aOB(14, _omitFieldNames ? '' : 'viaMqtt') + ..a<$core.int>(15, _omitFieldNames ? '' : 'hopStart', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 16, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..aOB(17, _omitFieldNames ? '' : 'pkiEncrypted') + ..a<$core.int>(18, _omitFieldNames ? '' : 'nextHop', $pb.PbFieldType.OU3) + ..a<$core.int>(19, _omitFieldNames ? '' : 'relayNode', $pb.PbFieldType.OU3) + ..a<$core.int>(20, _omitFieldNames ? '' : 'txAfter', $pb.PbFieldType.OU3) + ..e( + 21, _omitFieldNames ? '' : 'transportMechanism', $pb.PbFieldType.OE, + defaultOrMaker: MeshPacket_TransportMechanism.TRANSPORT_INTERNAL, + valueOf: MeshPacket_TransportMechanism.valueOf, + enumValues: MeshPacket_TransportMechanism.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MeshPacket clone() => MeshPacket()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MeshPacket copyWith(void Function(MeshPacket) updates) => + super.copyWith((message) => updates(message as MeshPacket)) as MeshPacket; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static MeshPacket create() => MeshPacket._(); + @$core.override + MeshPacket createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static MeshPacket getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static MeshPacket? _defaultInstance; + + MeshPacket_PayloadVariant whichPayloadVariant() => + _MeshPacket_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// The sending node number. + /// Note: Our crypto implementation uses this field as well. + /// See [crypto](/docs/overview/encryption) for details. + @$pb.TagNumber(1) + $core.int get from => $_getIZ(0); + @$pb.TagNumber(1) + set from($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasFrom() => $_has(0); + @$pb.TagNumber(1) + void clearFrom() => $_clearField(1); + + /// + /// The (immediate) destination for this packet + @$pb.TagNumber(2) + $core.int get to => $_getIZ(1); + @$pb.TagNumber(2) + set to($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasTo() => $_has(1); + @$pb.TagNumber(2) + void clearTo() => $_clearField(2); + + /// + /// (Usually) If set, this indicates the index in the secondary_channels table that this packet was sent/received on. + /// If unset, packet was on the primary channel. + /// A particular node might know only a subset of channels in use on the mesh. + /// Therefore channel_index is inherently a local concept and meaningless to send between nodes. + /// Very briefly, while sending and receiving deep inside the device Router code, this field instead + /// contains the 'channel hash' instead of the index. + /// This 'trick' is only used while the payload_variant is an 'encrypted'. + @$pb.TagNumber(3) + $core.int get channel => $_getIZ(2); + @$pb.TagNumber(3) + set channel($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasChannel() => $_has(2); + @$pb.TagNumber(3) + void clearChannel() => $_clearField(3); + + /// + /// TODO: REPLACE + @$pb.TagNumber(4) + Data get decoded => $_getN(3); + @$pb.TagNumber(4) + set decoded(Data value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasDecoded() => $_has(3); + @$pb.TagNumber(4) + void clearDecoded() => $_clearField(4); + @$pb.TagNumber(4) + Data ensureDecoded() => $_ensure(3); + + /// + /// TODO: REPLACE + @$pb.TagNumber(5) + $core.List<$core.int> get encrypted => $_getN(4); + @$pb.TagNumber(5) + set encrypted($core.List<$core.int> value) => $_setBytes(4, value); + @$pb.TagNumber(5) + $core.bool hasEncrypted() => $_has(4); + @$pb.TagNumber(5) + void clearEncrypted() => $_clearField(5); + + /// + /// A unique ID for this packet. + /// Always 0 for no-ack packets or non broadcast packets (and therefore take zero bytes of space). + /// Otherwise a unique ID for this packet, useful for flooding algorithms. + /// ID only needs to be unique on a _per sender_ basis, and it only + /// needs to be unique for a few minutes (long enough to last for the length of + /// any ACK or the completion of a mesh broadcast flood). + /// Note: Our crypto implementation uses this id as well. + /// See [crypto](/docs/overview/encryption) for details. + @$pb.TagNumber(6) + $core.int get id => $_getIZ(5); + @$pb.TagNumber(6) + set id($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasId() => $_has(5); + @$pb.TagNumber(6) + void clearId() => $_clearField(6); + + /// + /// The time this message was received by the esp32 (secs since 1970). + /// Note: this field is _never_ sent on the radio link itself (to save space) Times + /// are typically not sent over the mesh, but they will be added to any Packet + /// (chain of SubPacket) sent to the phone (so the phone can know exact time of reception) + @$pb.TagNumber(7) + $core.int get rxTime => $_getIZ(6); + @$pb.TagNumber(7) + set rxTime($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasRxTime() => $_has(6); + @$pb.TagNumber(7) + void clearRxTime() => $_clearField(7); + + /// + /// *Never* sent over the radio links. + /// Set during reception to indicate the SNR of this packet. + /// Used to collect statistics on current link quality. + @$pb.TagNumber(8) + $core.double get rxSnr => $_getN(7); + @$pb.TagNumber(8) + set rxSnr($core.double value) => $_setFloat(7, value); + @$pb.TagNumber(8) + $core.bool hasRxSnr() => $_has(7); + @$pb.TagNumber(8) + void clearRxSnr() => $_clearField(8); + + /// + /// If unset treated as zero (no forwarding, send to direct neighbor nodes only) + /// if 1, allow hopping through one node, etc... + /// For our usecase real world topologies probably have a max of about 3. + /// This field is normally placed into a few of bits in the header. + @$pb.TagNumber(9) + $core.int get hopLimit => $_getIZ(8); + @$pb.TagNumber(9) + set hopLimit($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasHopLimit() => $_has(8); + @$pb.TagNumber(9) + void clearHopLimit() => $_clearField(9); + + /// + /// This packet is being sent as a reliable message, we would prefer it to arrive at the destination. + /// We would like to receive a ack packet in response. + /// Broadcasts messages treat this flag specially: Since acks for broadcasts would + /// rapidly flood the channel, the normal ack behavior is suppressed. + /// Instead, the original sender listens to see if at least one node is rebroadcasting this packet (because naive flooding algorithm). + /// If it hears that the odds (given typical LoRa topologies) the odds are very high that every node should eventually receive the message. + /// So FloodingRouter.cpp generates an implicit ack which is delivered to the original sender. + /// If after some time we don't hear anyone rebroadcast our packet, we will timeout and retransmit, using the regular resend logic. + /// Note: This flag is normally sent in a flag bit in the header when sent over the wire + @$pb.TagNumber(10) + $core.bool get wantAck => $_getBF(9); + @$pb.TagNumber(10) + set wantAck($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasWantAck() => $_has(9); + @$pb.TagNumber(10) + void clearWantAck() => $_clearField(10); + + /// + /// The priority of this message for sending. + /// See MeshPacket.Priority description for more details. + @$pb.TagNumber(11) + MeshPacket_Priority get priority => $_getN(10); + @$pb.TagNumber(11) + set priority(MeshPacket_Priority value) => $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasPriority() => $_has(10); + @$pb.TagNumber(11) + void clearPriority() => $_clearField(11); + + /// + /// rssi of received packet. Only sent to phone for dispay purposes. + @$pb.TagNumber(12) + $core.int get rxRssi => $_getIZ(11); + @$pb.TagNumber(12) + set rxRssi($core.int value) => $_setSignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasRxRssi() => $_has(11); + @$pb.TagNumber(12) + void clearRxRssi() => $_clearField(12); + + /// + /// Describe if this message is delayed + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(13) + MeshPacket_Delayed get delayed => $_getN(12); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(13) + set delayed(MeshPacket_Delayed value) => $_setField(13, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(13) + $core.bool hasDelayed() => $_has(12); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(13) + void clearDelayed() => $_clearField(13); + + /// + /// Describes whether this packet passed via MQTT somewhere along the path it currently took. + @$pb.TagNumber(14) + $core.bool get viaMqtt => $_getBF(13); + @$pb.TagNumber(14) + set viaMqtt($core.bool value) => $_setBool(13, value); + @$pb.TagNumber(14) + $core.bool hasViaMqtt() => $_has(13); + @$pb.TagNumber(14) + void clearViaMqtt() => $_clearField(14); + + /// + /// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header. + /// When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled. + @$pb.TagNumber(15) + $core.int get hopStart => $_getIZ(14); + @$pb.TagNumber(15) + set hopStart($core.int value) => $_setUnsignedInt32(14, value); + @$pb.TagNumber(15) + $core.bool hasHopStart() => $_has(14); + @$pb.TagNumber(15) + void clearHopStart() => $_clearField(15); + + /// + /// Records the public key the packet was encrypted with, if applicable. + @$pb.TagNumber(16) + $core.List<$core.int> get publicKey => $_getN(15); + @$pb.TagNumber(16) + set publicKey($core.List<$core.int> value) => $_setBytes(15, value); + @$pb.TagNumber(16) + $core.bool hasPublicKey() => $_has(15); + @$pb.TagNumber(16) + void clearPublicKey() => $_clearField(16); + + /// + /// Indicates whether the packet was en/decrypted using PKI + @$pb.TagNumber(17) + $core.bool get pkiEncrypted => $_getBF(16); + @$pb.TagNumber(17) + set pkiEncrypted($core.bool value) => $_setBool(16, value); + @$pb.TagNumber(17) + $core.bool hasPkiEncrypted() => $_has(16); + @$pb.TagNumber(17) + void clearPkiEncrypted() => $_clearField(17); + + /// + /// Last byte of the node number of the node that should be used as the next hop in routing. + /// Set by the firmware internally, clients are not supposed to set this. + @$pb.TagNumber(18) + $core.int get nextHop => $_getIZ(17); + @$pb.TagNumber(18) + set nextHop($core.int value) => $_setUnsignedInt32(17, value); + @$pb.TagNumber(18) + $core.bool hasNextHop() => $_has(17); + @$pb.TagNumber(18) + void clearNextHop() => $_clearField(18); + + /// + /// Last byte of the node number of the node that will relay/relayed this packet. + /// Set by the firmware internally, clients are not supposed to set this. + @$pb.TagNumber(19) + $core.int get relayNode => $_getIZ(18); + @$pb.TagNumber(19) + set relayNode($core.int value) => $_setUnsignedInt32(18, value); + @$pb.TagNumber(19) + $core.bool hasRelayNode() => $_has(18); + @$pb.TagNumber(19) + void clearRelayNode() => $_clearField(19); + + /// + /// *Never* sent over the radio links. + /// Timestamp after which this packet may be sent. + /// Set by the firmware internally, clients are not supposed to set this. + @$pb.TagNumber(20) + $core.int get txAfter => $_getIZ(19); + @$pb.TagNumber(20) + set txAfter($core.int value) => $_setUnsignedInt32(19, value); + @$pb.TagNumber(20) + $core.bool hasTxAfter() => $_has(19); + @$pb.TagNumber(20) + void clearTxAfter() => $_clearField(20); + + /// + /// Indicates which transport mechanism this packet arrived over + @$pb.TagNumber(21) + MeshPacket_TransportMechanism get transportMechanism => $_getN(20); + @$pb.TagNumber(21) + set transportMechanism(MeshPacket_TransportMechanism value) => + $_setField(21, value); + @$pb.TagNumber(21) + $core.bool hasTransportMechanism() => $_has(20); + @$pb.TagNumber(21) + void clearTransportMechanism() => $_clearField(21); +} + +/// +/// The bluetooth to device link: +/// Old BTLE protocol docs from TODO, merge in above and make real docs... +/// use protocol buffers, and NanoPB +/// messages from device to phone: +/// POSITION_UPDATE (..., time) +/// TEXT_RECEIVED(from, text, time) +/// OPAQUE_RECEIVED(from, payload, time) (for signal messages or other applications) +/// messages from phone to device: +/// SET_MYID(id, human readable long, human readable short) (send down the unique ID +/// string used for this node, a human readable string shown for that id, and a very +/// short human readable string suitable for oled screen) SEND_OPAQUE(dest, payload) +/// (for signal messages or other applications) SEND_TEXT(dest, text) Get all +/// nodes() (returns list of nodes, with full info, last time seen, loc, battery +/// level etc) SET_CONFIG (switches device to a new set of radio params and +/// preshared key, drops all existing nodes, force our node to rejoin this new group) +/// Full information about a node on the mesh +class NodeInfo extends $pb.GeneratedMessage { + factory NodeInfo({ + $core.int? num, + User? user, + Position? position, + $core.double? snr, + $core.int? lastHeard, + $0.DeviceMetrics? deviceMetrics, + $core.int? channel, + $core.bool? viaMqtt, + $core.int? hopsAway, + $core.bool? isFavorite, + $core.bool? isIgnored, + $core.bool? isKeyManuallyVerified, + }) { + final result = create(); + if (num != null) result.num = num; + if (user != null) result.user = user; + if (position != null) result.position = position; + if (snr != null) result.snr = snr; + if (lastHeard != null) result.lastHeard = lastHeard; + if (deviceMetrics != null) result.deviceMetrics = deviceMetrics; + if (channel != null) result.channel = channel; + if (viaMqtt != null) result.viaMqtt = viaMqtt; + if (hopsAway != null) result.hopsAway = hopsAway; + if (isFavorite != null) result.isFavorite = isFavorite; + if (isIgnored != null) result.isIgnored = isIgnored; + if (isKeyManuallyVerified != null) + result.isKeyManuallyVerified = isKeyManuallyVerified; + return result; + } + + NodeInfo._(); + + factory NodeInfo.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeInfo.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeInfo', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'num', $pb.PbFieldType.OU3) + ..aOM(2, _omitFieldNames ? '' : 'user', subBuilder: User.create) + ..aOM(3, _omitFieldNames ? '' : 'position', + subBuilder: Position.create) + ..a<$core.double>(4, _omitFieldNames ? '' : 'snr', $pb.PbFieldType.OF) + ..a<$core.int>(5, _omitFieldNames ? '' : 'lastHeard', $pb.PbFieldType.OF3) + ..aOM<$0.DeviceMetrics>(6, _omitFieldNames ? '' : 'deviceMetrics', + subBuilder: $0.DeviceMetrics.create) + ..a<$core.int>(7, _omitFieldNames ? '' : 'channel', $pb.PbFieldType.OU3) + ..aOB(8, _omitFieldNames ? '' : 'viaMqtt') + ..a<$core.int>(9, _omitFieldNames ? '' : 'hopsAway', $pb.PbFieldType.OU3) + ..aOB(10, _omitFieldNames ? '' : 'isFavorite') + ..aOB(11, _omitFieldNames ? '' : 'isIgnored') + ..aOB(12, _omitFieldNames ? '' : 'isKeyManuallyVerified') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeInfo clone() => NodeInfo()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeInfo copyWith(void Function(NodeInfo) updates) => + super.copyWith((message) => updates(message as NodeInfo)) as NodeInfo; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeInfo create() => NodeInfo._(); + @$core.override + NodeInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static NodeInfo? _defaultInstance; + + /// + /// The node number + @$pb.TagNumber(1) + $core.int get num => $_getIZ(0); + @$pb.TagNumber(1) + set num($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNum() => $_has(0); + @$pb.TagNumber(1) + void clearNum() => $_clearField(1); + + /// + /// The user info for this node + @$pb.TagNumber(2) + User get user => $_getN(1); + @$pb.TagNumber(2) + set user(User value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasUser() => $_has(1); + @$pb.TagNumber(2) + void clearUser() => $_clearField(2); + @$pb.TagNumber(2) + User ensureUser() => $_ensure(1); + + /// + /// This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. + /// Position.time now indicates the last time we received a POSITION from that node. + @$pb.TagNumber(3) + Position get position => $_getN(2); + @$pb.TagNumber(3) + set position(Position value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasPosition() => $_has(2); + @$pb.TagNumber(3) + void clearPosition() => $_clearField(3); + @$pb.TagNumber(3) + Position ensurePosition() => $_ensure(2); + + /// + /// Returns the Signal-to-noise ratio (SNR) of the last received message, + /// as measured by the receiver. Return SNR of the last received message in dB + @$pb.TagNumber(4) + $core.double get snr => $_getN(3); + @$pb.TagNumber(4) + set snr($core.double value) => $_setFloat(3, value); + @$pb.TagNumber(4) + $core.bool hasSnr() => $_has(3); + @$pb.TagNumber(4) + void clearSnr() => $_clearField(4); + + /// + /// Set to indicate the last time we received a packet from this node + @$pb.TagNumber(5) + $core.int get lastHeard => $_getIZ(4); + @$pb.TagNumber(5) + set lastHeard($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasLastHeard() => $_has(4); + @$pb.TagNumber(5) + void clearLastHeard() => $_clearField(5); + + /// + /// The latest device metrics for the node. + @$pb.TagNumber(6) + $0.DeviceMetrics get deviceMetrics => $_getN(5); + @$pb.TagNumber(6) + set deviceMetrics($0.DeviceMetrics value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasDeviceMetrics() => $_has(5); + @$pb.TagNumber(6) + void clearDeviceMetrics() => $_clearField(6); + @$pb.TagNumber(6) + $0.DeviceMetrics ensureDeviceMetrics() => $_ensure(5); + + /// + /// local channel index we heard that node on. Only populated if its not the default channel. + @$pb.TagNumber(7) + $core.int get channel => $_getIZ(6); + @$pb.TagNumber(7) + set channel($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasChannel() => $_has(6); + @$pb.TagNumber(7) + void clearChannel() => $_clearField(7); + + /// + /// True if we witnessed the node over MQTT instead of LoRA transport + @$pb.TagNumber(8) + $core.bool get viaMqtt => $_getBF(7); + @$pb.TagNumber(8) + set viaMqtt($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasViaMqtt() => $_has(7); + @$pb.TagNumber(8) + void clearViaMqtt() => $_clearField(8); + + /// + /// Number of hops away from us this node is (0 if direct neighbor) + @$pb.TagNumber(9) + $core.int get hopsAway => $_getIZ(8); + @$pb.TagNumber(9) + set hopsAway($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasHopsAway() => $_has(8); + @$pb.TagNumber(9) + void clearHopsAway() => $_clearField(9); + + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + @$pb.TagNumber(10) + $core.bool get isFavorite => $_getBF(9); + @$pb.TagNumber(10) + set isFavorite($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasIsFavorite() => $_has(9); + @$pb.TagNumber(10) + void clearIsFavorite() => $_clearField(10); + + /// + /// True if node is in our ignored list + /// Persists between NodeDB internal clean ups + @$pb.TagNumber(11) + $core.bool get isIgnored => $_getBF(10); + @$pb.TagNumber(11) + set isIgnored($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(11) + $core.bool hasIsIgnored() => $_has(10); + @$pb.TagNumber(11) + void clearIsIgnored() => $_clearField(11); + + /// + /// True if node public key has been verified. + /// Persists between NodeDB internal clean ups + /// LSB 0 of the bitfield + @$pb.TagNumber(12) + $core.bool get isKeyManuallyVerified => $_getBF(11); + @$pb.TagNumber(12) + set isKeyManuallyVerified($core.bool value) => $_setBool(11, value); + @$pb.TagNumber(12) + $core.bool hasIsKeyManuallyVerified() => $_has(11); + @$pb.TagNumber(12) + void clearIsKeyManuallyVerified() => $_clearField(12); +} + +/// +/// Unique local debugging info for this node +/// Note: we don't include position or the user info, because that will come in the +/// Sent to the phone in response to WantNodes. +class MyNodeInfo extends $pb.GeneratedMessage { + factory MyNodeInfo({ + $core.int? myNodeNum, + $core.int? rebootCount, + $core.int? minAppVersion, + $core.List<$core.int>? deviceId, + $core.String? pioEnv, + FirmwareEdition? firmwareEdition, + $core.int? nodedbCount, + }) { + final result = create(); + if (myNodeNum != null) result.myNodeNum = myNodeNum; + if (rebootCount != null) result.rebootCount = rebootCount; + if (minAppVersion != null) result.minAppVersion = minAppVersion; + if (deviceId != null) result.deviceId = deviceId; + if (pioEnv != null) result.pioEnv = pioEnv; + if (firmwareEdition != null) result.firmwareEdition = firmwareEdition; + if (nodedbCount != null) result.nodedbCount = nodedbCount; + return result; + } + + MyNodeInfo._(); + + factory MyNodeInfo.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory MyNodeInfo.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'MyNodeInfo', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'myNodeNum', $pb.PbFieldType.OU3) + ..a<$core.int>(8, _omitFieldNames ? '' : 'rebootCount', $pb.PbFieldType.OU3) + ..a<$core.int>( + 11, _omitFieldNames ? '' : 'minAppVersion', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 12, _omitFieldNames ? '' : 'deviceId', $pb.PbFieldType.OY) + ..aOS(13, _omitFieldNames ? '' : 'pioEnv') + ..e( + 14, _omitFieldNames ? '' : 'firmwareEdition', $pb.PbFieldType.OE, + defaultOrMaker: FirmwareEdition.VANILLA, + valueOf: FirmwareEdition.valueOf, + enumValues: FirmwareEdition.values) + ..a<$core.int>( + 15, _omitFieldNames ? '' : 'nodedbCount', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MyNodeInfo clone() => MyNodeInfo()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MyNodeInfo copyWith(void Function(MyNodeInfo) updates) => + super.copyWith((message) => updates(message as MyNodeInfo)) as MyNodeInfo; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static MyNodeInfo create() => MyNodeInfo._(); + @$core.override + MyNodeInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static MyNodeInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static MyNodeInfo? _defaultInstance; + + /// + /// Tells the phone what our node number is, default starting value is + /// lowbyte of macaddr, but it will be fixed if that is already in use + @$pb.TagNumber(1) + $core.int get myNodeNum => $_getIZ(0); + @$pb.TagNumber(1) + set myNodeNum($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasMyNodeNum() => $_has(0); + @$pb.TagNumber(1) + void clearMyNodeNum() => $_clearField(1); + + /// + /// The total number of reboots this node has ever encountered + /// (well - since the last time we discarded preferences) + @$pb.TagNumber(8) + $core.int get rebootCount => $_getIZ(1); + @$pb.TagNumber(8) + set rebootCount($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(8) + $core.bool hasRebootCount() => $_has(1); + @$pb.TagNumber(8) + void clearRebootCount() => $_clearField(8); + + /// + /// The minimum app version that can talk to this device. + /// Phone/PC apps should compare this to their build number and if too low tell the user they must update their app + @$pb.TagNumber(11) + $core.int get minAppVersion => $_getIZ(2); + @$pb.TagNumber(11) + set minAppVersion($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(11) + $core.bool hasMinAppVersion() => $_has(2); + @$pb.TagNumber(11) + void clearMinAppVersion() => $_clearField(11); + + /// + /// Unique hardware identifier for this device + @$pb.TagNumber(12) + $core.List<$core.int> get deviceId => $_getN(3); + @$pb.TagNumber(12) + set deviceId($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(12) + $core.bool hasDeviceId() => $_has(3); + @$pb.TagNumber(12) + void clearDeviceId() => $_clearField(12); + + /// + /// The PlatformIO environment used to build this firmware + @$pb.TagNumber(13) + $core.String get pioEnv => $_getSZ(4); + @$pb.TagNumber(13) + set pioEnv($core.String value) => $_setString(4, value); + @$pb.TagNumber(13) + $core.bool hasPioEnv() => $_has(4); + @$pb.TagNumber(13) + void clearPioEnv() => $_clearField(13); + + /// + /// The indicator for whether this device is running event firmware and which + @$pb.TagNumber(14) + FirmwareEdition get firmwareEdition => $_getN(5); + @$pb.TagNumber(14) + set firmwareEdition(FirmwareEdition value) => $_setField(14, value); + @$pb.TagNumber(14) + $core.bool hasFirmwareEdition() => $_has(5); + @$pb.TagNumber(14) + void clearFirmwareEdition() => $_clearField(14); + + /// + /// The number of nodes in the nodedb. + /// This is used by the phone to know how many NodeInfo packets to expect on want_config + @$pb.TagNumber(15) + $core.int get nodedbCount => $_getIZ(6); + @$pb.TagNumber(15) + set nodedbCount($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(15) + $core.bool hasNodedbCount() => $_has(6); + @$pb.TagNumber(15) + void clearNodedbCount() => $_clearField(15); +} + +/// +/// Debug output from the device. +/// To minimize the size of records inside the device code, if a time/source/level is not set +/// on the message it is assumed to be a continuation of the previously sent message. +/// This allows the device code to use fixed maxlen 64 byte strings for messages, +/// and then extend as needed by emitting multiple records. +class LogRecord extends $pb.GeneratedMessage { + factory LogRecord({ + $core.String? message, + $core.int? time, + $core.String? source, + LogRecord_Level? level, + }) { + final result = create(); + if (message != null) result.message = message; + if (time != null) result.time = time; + if (source != null) result.source = source; + if (level != null) result.level = level; + return result; + } + + LogRecord._(); + + factory LogRecord.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory LogRecord.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'LogRecord', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'message') + ..a<$core.int>(2, _omitFieldNames ? '' : 'time', $pb.PbFieldType.OF3) + ..aOS(3, _omitFieldNames ? '' : 'source') + ..e(4, _omitFieldNames ? '' : 'level', $pb.PbFieldType.OE, + defaultOrMaker: LogRecord_Level.UNSET, + valueOf: LogRecord_Level.valueOf, + enumValues: LogRecord_Level.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LogRecord clone() => LogRecord()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LogRecord copyWith(void Function(LogRecord) updates) => + super.copyWith((message) => updates(message as LogRecord)) as LogRecord; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LogRecord create() => LogRecord._(); + @$core.override + LogRecord createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LogRecord getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static LogRecord? _defaultInstance; + + /// + /// Log levels, chosen to match python logging conventions. + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => $_clearField(1); + + /// + /// Seconds since 1970 - or 0 for unknown/unset + @$pb.TagNumber(2) + $core.int get time => $_getIZ(1); + @$pb.TagNumber(2) + set time($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasTime() => $_has(1); + @$pb.TagNumber(2) + void clearTime() => $_clearField(2); + + /// + /// Usually based on thread name - if known + @$pb.TagNumber(3) + $core.String get source => $_getSZ(2); + @$pb.TagNumber(3) + set source($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasSource() => $_has(2); + @$pb.TagNumber(3) + void clearSource() => $_clearField(3); + + /// + /// Not yet set + @$pb.TagNumber(4) + LogRecord_Level get level => $_getN(3); + @$pb.TagNumber(4) + set level(LogRecord_Level value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasLevel() => $_has(3); + @$pb.TagNumber(4) + void clearLevel() => $_clearField(4); +} + +class QueueStatus extends $pb.GeneratedMessage { + factory QueueStatus({ + $core.int? res, + $core.int? free, + $core.int? maxlen, + $core.int? meshPacketId, + }) { + final result = create(); + if (res != null) result.res = res; + if (free != null) result.free = free; + if (maxlen != null) result.maxlen = maxlen; + if (meshPacketId != null) result.meshPacketId = meshPacketId; + return result; + } + + QueueStatus._(); + + factory QueueStatus.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory QueueStatus.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'QueueStatus', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'res', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'free', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'maxlen', $pb.PbFieldType.OU3) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'meshPacketId', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + QueueStatus clone() => QueueStatus()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + QueueStatus copyWith(void Function(QueueStatus) updates) => + super.copyWith((message) => updates(message as QueueStatus)) + as QueueStatus; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static QueueStatus create() => QueueStatus._(); + @$core.override + QueueStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static QueueStatus getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static QueueStatus? _defaultInstance; + + /// Last attempt to queue status, ErrorCode + @$pb.TagNumber(1) + $core.int get res => $_getIZ(0); + @$pb.TagNumber(1) + set res($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasRes() => $_has(0); + @$pb.TagNumber(1) + void clearRes() => $_clearField(1); + + /// Free entries in the outgoing queue + @$pb.TagNumber(2) + $core.int get free => $_getIZ(1); + @$pb.TagNumber(2) + set free($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasFree() => $_has(1); + @$pb.TagNumber(2) + void clearFree() => $_clearField(2); + + /// Maximum entries in the outgoing queue + @$pb.TagNumber(3) + $core.int get maxlen => $_getIZ(2); + @$pb.TagNumber(3) + set maxlen($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasMaxlen() => $_has(2); + @$pb.TagNumber(3) + void clearMaxlen() => $_clearField(3); + + /// What was mesh packet id that generated this response? + @$pb.TagNumber(4) + $core.int get meshPacketId => $_getIZ(3); + @$pb.TagNumber(4) + set meshPacketId($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasMeshPacketId() => $_has(3); + @$pb.TagNumber(4) + void clearMeshPacketId() => $_clearField(4); +} + +enum FromRadio_PayloadVariant { + packet, + myInfo, + nodeInfo, + config, + logRecord, + configCompleteId, + rebooted, + moduleConfig, + channel, + queueStatus, + xmodemPacket, + metadata, + mqttClientProxyMessage, + fileInfo, + clientNotification, + deviceuiConfig, + notSet +} + +/// +/// Packets from the radio to the phone will appear on the fromRadio characteristic. +/// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? +/// It will sit in that descriptor until consumed by the phone, +/// at which point the next item in the FIFO will be populated. +class FromRadio extends $pb.GeneratedMessage { + factory FromRadio({ + $core.int? id, + MeshPacket? packet, + MyNodeInfo? myInfo, + NodeInfo? nodeInfo, + $1.Config? config, + LogRecord? logRecord, + $core.int? configCompleteId, + $core.bool? rebooted, + $2.ModuleConfig? moduleConfig, + $3.Channel? channel, + QueueStatus? queueStatus, + $4.XModem? xmodemPacket, + DeviceMetadata? metadata, + MqttClientProxyMessage? mqttClientProxyMessage, + FileInfo? fileInfo, + ClientNotification? clientNotification, + $5.DeviceUIConfig? deviceuiConfig, + }) { + final result = create(); + if (id != null) result.id = id; + if (packet != null) result.packet = packet; + if (myInfo != null) result.myInfo = myInfo; + if (nodeInfo != null) result.nodeInfo = nodeInfo; + if (config != null) result.config = config; + if (logRecord != null) result.logRecord = logRecord; + if (configCompleteId != null) result.configCompleteId = configCompleteId; + if (rebooted != null) result.rebooted = rebooted; + if (moduleConfig != null) result.moduleConfig = moduleConfig; + if (channel != null) result.channel = channel; + if (queueStatus != null) result.queueStatus = queueStatus; + if (xmodemPacket != null) result.xmodemPacket = xmodemPacket; + if (metadata != null) result.metadata = metadata; + if (mqttClientProxyMessage != null) + result.mqttClientProxyMessage = mqttClientProxyMessage; + if (fileInfo != null) result.fileInfo = fileInfo; + if (clientNotification != null) + result.clientNotification = clientNotification; + if (deviceuiConfig != null) result.deviceuiConfig = deviceuiConfig; + return result; + } + + FromRadio._(); + + factory FromRadio.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory FromRadio.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, FromRadio_PayloadVariant> + _FromRadio_PayloadVariantByTag = { + 2: FromRadio_PayloadVariant.packet, + 3: FromRadio_PayloadVariant.myInfo, + 4: FromRadio_PayloadVariant.nodeInfo, + 5: FromRadio_PayloadVariant.config, + 6: FromRadio_PayloadVariant.logRecord, + 7: FromRadio_PayloadVariant.configCompleteId, + 8: FromRadio_PayloadVariant.rebooted, + 9: FromRadio_PayloadVariant.moduleConfig, + 10: FromRadio_PayloadVariant.channel, + 11: FromRadio_PayloadVariant.queueStatus, + 12: FromRadio_PayloadVariant.xmodemPacket, + 13: FromRadio_PayloadVariant.metadata, + 14: FromRadio_PayloadVariant.mqttClientProxyMessage, + 15: FromRadio_PayloadVariant.fileInfo, + 16: FromRadio_PayloadVariant.clientNotification, + 17: FromRadio_PayloadVariant.deviceuiConfig, + 0: FromRadio_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'FromRadio', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'id', $pb.PbFieldType.OU3) + ..aOM(2, _omitFieldNames ? '' : 'packet', + subBuilder: MeshPacket.create) + ..aOM(3, _omitFieldNames ? '' : 'myInfo', + subBuilder: MyNodeInfo.create) + ..aOM(4, _omitFieldNames ? '' : 'nodeInfo', + subBuilder: NodeInfo.create) + ..aOM<$1.Config>(5, _omitFieldNames ? '' : 'config', + subBuilder: $1.Config.create) + ..aOM(6, _omitFieldNames ? '' : 'logRecord', + subBuilder: LogRecord.create) + ..a<$core.int>( + 7, _omitFieldNames ? '' : 'configCompleteId', $pb.PbFieldType.OU3) + ..aOB(8, _omitFieldNames ? '' : 'rebooted') + ..aOM<$2.ModuleConfig>(9, _omitFieldNames ? '' : 'moduleConfig', + protoName: 'moduleConfig', subBuilder: $2.ModuleConfig.create) + ..aOM<$3.Channel>(10, _omitFieldNames ? '' : 'channel', + subBuilder: $3.Channel.create) + ..aOM(11, _omitFieldNames ? '' : 'queueStatus', + protoName: 'queueStatus', subBuilder: QueueStatus.create) + ..aOM<$4.XModem>(12, _omitFieldNames ? '' : 'xmodemPacket', + protoName: 'xmodemPacket', subBuilder: $4.XModem.create) + ..aOM(13, _omitFieldNames ? '' : 'metadata', + subBuilder: DeviceMetadata.create) + ..aOM( + 14, _omitFieldNames ? '' : 'mqttClientProxyMessage', + protoName: 'mqttClientProxyMessage', + subBuilder: MqttClientProxyMessage.create) + ..aOM(15, _omitFieldNames ? '' : 'fileInfo', + protoName: 'fileInfo', subBuilder: FileInfo.create) + ..aOM(16, _omitFieldNames ? '' : 'clientNotification', + protoName: 'clientNotification', subBuilder: ClientNotification.create) + ..aOM<$5.DeviceUIConfig>(17, _omitFieldNames ? '' : 'deviceuiConfig', + protoName: 'deviceuiConfig', subBuilder: $5.DeviceUIConfig.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + FromRadio clone() => FromRadio()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + FromRadio copyWith(void Function(FromRadio) updates) => + super.copyWith((message) => updates(message as FromRadio)) as FromRadio; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static FromRadio create() => FromRadio._(); + @$core.override + FromRadio createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static FromRadio getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static FromRadio? _defaultInstance; + + FromRadio_PayloadVariant whichPayloadVariant() => + _FromRadio_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// The packet id, used to allow the phone to request missing read packets from the FIFO, + /// see our bluetooth docs + @$pb.TagNumber(1) + $core.int get id => $_getIZ(0); + @$pb.TagNumber(1) + set id($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => $_clearField(1); + + /// + /// Log levels, chosen to match python logging conventions. + @$pb.TagNumber(2) + MeshPacket get packet => $_getN(1); + @$pb.TagNumber(2) + set packet(MeshPacket value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasPacket() => $_has(1); + @$pb.TagNumber(2) + void clearPacket() => $_clearField(2); + @$pb.TagNumber(2) + MeshPacket ensurePacket() => $_ensure(1); + + /// + /// Tells the phone what our node number is, can be -1 if we've not yet joined a mesh. + /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. + @$pb.TagNumber(3) + MyNodeInfo get myInfo => $_getN(2); + @$pb.TagNumber(3) + set myInfo(MyNodeInfo value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasMyInfo() => $_has(2); + @$pb.TagNumber(3) + void clearMyInfo() => $_clearField(3); + @$pb.TagNumber(3) + MyNodeInfo ensureMyInfo() => $_ensure(2); + + /// + /// One packet is sent for each node in the on radio DB + /// starts over with the first node in our DB + @$pb.TagNumber(4) + NodeInfo get nodeInfo => $_getN(3); + @$pb.TagNumber(4) + set nodeInfo(NodeInfo value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasNodeInfo() => $_has(3); + @$pb.TagNumber(4) + void clearNodeInfo() => $_clearField(4); + @$pb.TagNumber(4) + NodeInfo ensureNodeInfo() => $_ensure(3); + + /// + /// Include a part of the config (was: RadioConfig radio) + @$pb.TagNumber(5) + $1.Config get config => $_getN(4); + @$pb.TagNumber(5) + set config($1.Config value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasConfig() => $_has(4); + @$pb.TagNumber(5) + void clearConfig() => $_clearField(5); + @$pb.TagNumber(5) + $1.Config ensureConfig() => $_ensure(4); + + /// + /// Set to send debug console output over our protobuf stream + @$pb.TagNumber(6) + LogRecord get logRecord => $_getN(5); + @$pb.TagNumber(6) + set logRecord(LogRecord value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasLogRecord() => $_has(5); + @$pb.TagNumber(6) + void clearLogRecord() => $_clearField(6); + @$pb.TagNumber(6) + LogRecord ensureLogRecord() => $_ensure(5); + + /// + /// Sent as true once the device has finished sending all of the responses to want_config + /// recipient should check if this ID matches our original request nonce, if + /// not, it means your config responses haven't started yet. + /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. + @$pb.TagNumber(7) + $core.int get configCompleteId => $_getIZ(6); + @$pb.TagNumber(7) + set configCompleteId($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasConfigCompleteId() => $_has(6); + @$pb.TagNumber(7) + void clearConfigCompleteId() => $_clearField(7); + + /// + /// Sent to tell clients the radio has just rebooted. + /// Set to true if present. + /// Not used on all transports, currently just used for the serial console. + /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. + @$pb.TagNumber(8) + $core.bool get rebooted => $_getBF(7); + @$pb.TagNumber(8) + set rebooted($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasRebooted() => $_has(7); + @$pb.TagNumber(8) + void clearRebooted() => $_clearField(8); + + /// + /// Include module config + @$pb.TagNumber(9) + $2.ModuleConfig get moduleConfig => $_getN(8); + @$pb.TagNumber(9) + set moduleConfig($2.ModuleConfig value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasModuleConfig() => $_has(8); + @$pb.TagNumber(9) + void clearModuleConfig() => $_clearField(9); + @$pb.TagNumber(9) + $2.ModuleConfig ensureModuleConfig() => $_ensure(8); + + /// + /// One packet is sent for each channel + @$pb.TagNumber(10) + $3.Channel get channel => $_getN(9); + @$pb.TagNumber(10) + set channel($3.Channel value) => $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasChannel() => $_has(9); + @$pb.TagNumber(10) + void clearChannel() => $_clearField(10); + @$pb.TagNumber(10) + $3.Channel ensureChannel() => $_ensure(9); + + /// + /// Queue status info + @$pb.TagNumber(11) + QueueStatus get queueStatus => $_getN(10); + @$pb.TagNumber(11) + set queueStatus(QueueStatus value) => $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasQueueStatus() => $_has(10); + @$pb.TagNumber(11) + void clearQueueStatus() => $_clearField(11); + @$pb.TagNumber(11) + QueueStatus ensureQueueStatus() => $_ensure(10); + + /// + /// File Transfer Chunk + @$pb.TagNumber(12) + $4.XModem get xmodemPacket => $_getN(11); + @$pb.TagNumber(12) + set xmodemPacket($4.XModem value) => $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasXmodemPacket() => $_has(11); + @$pb.TagNumber(12) + void clearXmodemPacket() => $_clearField(12); + @$pb.TagNumber(12) + $4.XModem ensureXmodemPacket() => $_ensure(11); + + /// + /// Device metadata message + @$pb.TagNumber(13) + DeviceMetadata get metadata => $_getN(12); + @$pb.TagNumber(13) + set metadata(DeviceMetadata value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasMetadata() => $_has(12); + @$pb.TagNumber(13) + void clearMetadata() => $_clearField(13); + @$pb.TagNumber(13) + DeviceMetadata ensureMetadata() => $_ensure(12); + + /// + /// MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) + @$pb.TagNumber(14) + MqttClientProxyMessage get mqttClientProxyMessage => $_getN(13); + @$pb.TagNumber(14) + set mqttClientProxyMessage(MqttClientProxyMessage value) => + $_setField(14, value); + @$pb.TagNumber(14) + $core.bool hasMqttClientProxyMessage() => $_has(13); + @$pb.TagNumber(14) + void clearMqttClientProxyMessage() => $_clearField(14); + @$pb.TagNumber(14) + MqttClientProxyMessage ensureMqttClientProxyMessage() => $_ensure(13); + + /// + /// File system manifest messages + @$pb.TagNumber(15) + FileInfo get fileInfo => $_getN(14); + @$pb.TagNumber(15) + set fileInfo(FileInfo value) => $_setField(15, value); + @$pb.TagNumber(15) + $core.bool hasFileInfo() => $_has(14); + @$pb.TagNumber(15) + void clearFileInfo() => $_clearField(15); + @$pb.TagNumber(15) + FileInfo ensureFileInfo() => $_ensure(14); + + /// + /// Notification message to the client + @$pb.TagNumber(16) + ClientNotification get clientNotification => $_getN(15); + @$pb.TagNumber(16) + set clientNotification(ClientNotification value) => $_setField(16, value); + @$pb.TagNumber(16) + $core.bool hasClientNotification() => $_has(15); + @$pb.TagNumber(16) + void clearClientNotification() => $_clearField(16); + @$pb.TagNumber(16) + ClientNotification ensureClientNotification() => $_ensure(15); + + /// + /// Persistent data for device-ui + @$pb.TagNumber(17) + $5.DeviceUIConfig get deviceuiConfig => $_getN(16); + @$pb.TagNumber(17) + set deviceuiConfig($5.DeviceUIConfig value) => $_setField(17, value); + @$pb.TagNumber(17) + $core.bool hasDeviceuiConfig() => $_has(16); + @$pb.TagNumber(17) + void clearDeviceuiConfig() => $_clearField(17); + @$pb.TagNumber(17) + $5.DeviceUIConfig ensureDeviceuiConfig() => $_ensure(16); +} + +enum ClientNotification_PayloadVariant { + keyVerificationNumberInform, + keyVerificationNumberRequest, + keyVerificationFinal, + duplicatedPublicKey, + lowEntropyKey, + notSet +} + +/// +/// A notification message from the device to the client +/// To be used for important messages that should to be displayed to the user +/// in the form of push notifications or validation messages when saving +/// invalid configuration. +class ClientNotification extends $pb.GeneratedMessage { + factory ClientNotification({ + $core.int? replyId, + $core.int? time, + LogRecord_Level? level, + $core.String? message, + KeyVerificationNumberInform? keyVerificationNumberInform, + KeyVerificationNumberRequest? keyVerificationNumberRequest, + KeyVerificationFinal? keyVerificationFinal, + DuplicatedPublicKey? duplicatedPublicKey, + LowEntropyKey? lowEntropyKey, + }) { + final result = create(); + if (replyId != null) result.replyId = replyId; + if (time != null) result.time = time; + if (level != null) result.level = level; + if (message != null) result.message = message; + if (keyVerificationNumberInform != null) + result.keyVerificationNumberInform = keyVerificationNumberInform; + if (keyVerificationNumberRequest != null) + result.keyVerificationNumberRequest = keyVerificationNumberRequest; + if (keyVerificationFinal != null) + result.keyVerificationFinal = keyVerificationFinal; + if (duplicatedPublicKey != null) + result.duplicatedPublicKey = duplicatedPublicKey; + if (lowEntropyKey != null) result.lowEntropyKey = lowEntropyKey; + return result; + } + + ClientNotification._(); + + factory ClientNotification.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ClientNotification.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, ClientNotification_PayloadVariant> + _ClientNotification_PayloadVariantByTag = { + 11: ClientNotification_PayloadVariant.keyVerificationNumberInform, + 12: ClientNotification_PayloadVariant.keyVerificationNumberRequest, + 13: ClientNotification_PayloadVariant.keyVerificationFinal, + 14: ClientNotification_PayloadVariant.duplicatedPublicKey, + 15: ClientNotification_PayloadVariant.lowEntropyKey, + 0: ClientNotification_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ClientNotification', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [11, 12, 13, 14, 15]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'replyId', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'time', $pb.PbFieldType.OF3) + ..e(3, _omitFieldNames ? '' : 'level', $pb.PbFieldType.OE, + defaultOrMaker: LogRecord_Level.UNSET, + valueOf: LogRecord_Level.valueOf, + enumValues: LogRecord_Level.values) + ..aOS(4, _omitFieldNames ? '' : 'message') + ..aOM( + 11, _omitFieldNames ? '' : 'keyVerificationNumberInform', + subBuilder: KeyVerificationNumberInform.create) + ..aOM( + 12, _omitFieldNames ? '' : 'keyVerificationNumberRequest', + subBuilder: KeyVerificationNumberRequest.create) + ..aOM( + 13, _omitFieldNames ? '' : 'keyVerificationFinal', + subBuilder: KeyVerificationFinal.create) + ..aOM(14, _omitFieldNames ? '' : 'duplicatedPublicKey', + subBuilder: DuplicatedPublicKey.create) + ..aOM(15, _omitFieldNames ? '' : 'lowEntropyKey', + subBuilder: LowEntropyKey.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ClientNotification clone() => ClientNotification()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ClientNotification copyWith(void Function(ClientNotification) updates) => + super.copyWith((message) => updates(message as ClientNotification)) + as ClientNotification; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ClientNotification create() => ClientNotification._(); + @$core.override + ClientNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ClientNotification getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ClientNotification? _defaultInstance; + + ClientNotification_PayloadVariant whichPayloadVariant() => + _ClientNotification_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// The id of the packet we're notifying in response to + @$pb.TagNumber(1) + $core.int get replyId => $_getIZ(0); + @$pb.TagNumber(1) + set replyId($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasReplyId() => $_has(0); + @$pb.TagNumber(1) + void clearReplyId() => $_clearField(1); + + /// + /// Seconds since 1970 - or 0 for unknown/unset + @$pb.TagNumber(2) + $core.int get time => $_getIZ(1); + @$pb.TagNumber(2) + set time($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasTime() => $_has(1); + @$pb.TagNumber(2) + void clearTime() => $_clearField(2); + + /// + /// The level type of notification + @$pb.TagNumber(3) + LogRecord_Level get level => $_getN(2); + @$pb.TagNumber(3) + set level(LogRecord_Level value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasLevel() => $_has(2); + @$pb.TagNumber(3) + void clearLevel() => $_clearField(3); + + /// + /// The message body of the notification + @$pb.TagNumber(4) + $core.String get message => $_getSZ(3); + @$pb.TagNumber(4) + set message($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasMessage() => $_has(3); + @$pb.TagNumber(4) + void clearMessage() => $_clearField(4); + + @$pb.TagNumber(11) + KeyVerificationNumberInform get keyVerificationNumberInform => $_getN(4); + @$pb.TagNumber(11) + set keyVerificationNumberInform(KeyVerificationNumberInform value) => + $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasKeyVerificationNumberInform() => $_has(4); + @$pb.TagNumber(11) + void clearKeyVerificationNumberInform() => $_clearField(11); + @$pb.TagNumber(11) + KeyVerificationNumberInform ensureKeyVerificationNumberInform() => + $_ensure(4); + + @$pb.TagNumber(12) + KeyVerificationNumberRequest get keyVerificationNumberRequest => $_getN(5); + @$pb.TagNumber(12) + set keyVerificationNumberRequest(KeyVerificationNumberRequest value) => + $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasKeyVerificationNumberRequest() => $_has(5); + @$pb.TagNumber(12) + void clearKeyVerificationNumberRequest() => $_clearField(12); + @$pb.TagNumber(12) + KeyVerificationNumberRequest ensureKeyVerificationNumberRequest() => + $_ensure(5); + + @$pb.TagNumber(13) + KeyVerificationFinal get keyVerificationFinal => $_getN(6); + @$pb.TagNumber(13) + set keyVerificationFinal(KeyVerificationFinal value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasKeyVerificationFinal() => $_has(6); + @$pb.TagNumber(13) + void clearKeyVerificationFinal() => $_clearField(13); + @$pb.TagNumber(13) + KeyVerificationFinal ensureKeyVerificationFinal() => $_ensure(6); + + @$pb.TagNumber(14) + DuplicatedPublicKey get duplicatedPublicKey => $_getN(7); + @$pb.TagNumber(14) + set duplicatedPublicKey(DuplicatedPublicKey value) => $_setField(14, value); + @$pb.TagNumber(14) + $core.bool hasDuplicatedPublicKey() => $_has(7); + @$pb.TagNumber(14) + void clearDuplicatedPublicKey() => $_clearField(14); + @$pb.TagNumber(14) + DuplicatedPublicKey ensureDuplicatedPublicKey() => $_ensure(7); + + @$pb.TagNumber(15) + LowEntropyKey get lowEntropyKey => $_getN(8); + @$pb.TagNumber(15) + set lowEntropyKey(LowEntropyKey value) => $_setField(15, value); + @$pb.TagNumber(15) + $core.bool hasLowEntropyKey() => $_has(8); + @$pb.TagNumber(15) + void clearLowEntropyKey() => $_clearField(15); + @$pb.TagNumber(15) + LowEntropyKey ensureLowEntropyKey() => $_ensure(8); +} + +class KeyVerificationNumberInform extends $pb.GeneratedMessage { + factory KeyVerificationNumberInform({ + $fixnum.Int64? nonce, + $core.String? remoteLongname, + $core.int? securityNumber, + }) { + final result = create(); + if (nonce != null) result.nonce = nonce; + if (remoteLongname != null) result.remoteLongname = remoteLongname; + if (securityNumber != null) result.securityNumber = securityNumber; + return result; + } + + KeyVerificationNumberInform._(); + + factory KeyVerificationNumberInform.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory KeyVerificationNumberInform.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'KeyVerificationNumberInform', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(2, _omitFieldNames ? '' : 'remoteLongname') + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'securityNumber', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationNumberInform clone() => + KeyVerificationNumberInform()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationNumberInform copyWith( + void Function(KeyVerificationNumberInform) updates) => + super.copyWith( + (message) => updates(message as KeyVerificationNumberInform)) + as KeyVerificationNumberInform; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static KeyVerificationNumberInform create() => + KeyVerificationNumberInform._(); + @$core.override + KeyVerificationNumberInform createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static KeyVerificationNumberInform getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static KeyVerificationNumberInform? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get nonce => $_getI64(0); + @$pb.TagNumber(1) + set nonce($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get remoteLongname => $_getSZ(1); + @$pb.TagNumber(2) + set remoteLongname($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasRemoteLongname() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteLongname() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get securityNumber => $_getIZ(2); + @$pb.TagNumber(3) + set securityNumber($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasSecurityNumber() => $_has(2); + @$pb.TagNumber(3) + void clearSecurityNumber() => $_clearField(3); +} + +class KeyVerificationNumberRequest extends $pb.GeneratedMessage { + factory KeyVerificationNumberRequest({ + $fixnum.Int64? nonce, + $core.String? remoteLongname, + }) { + final result = create(); + if (nonce != null) result.nonce = nonce; + if (remoteLongname != null) result.remoteLongname = remoteLongname; + return result; + } + + KeyVerificationNumberRequest._(); + + factory KeyVerificationNumberRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory KeyVerificationNumberRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'KeyVerificationNumberRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(2, _omitFieldNames ? '' : 'remoteLongname') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationNumberRequest clone() => + KeyVerificationNumberRequest()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationNumberRequest copyWith( + void Function(KeyVerificationNumberRequest) updates) => + super.copyWith( + (message) => updates(message as KeyVerificationNumberRequest)) + as KeyVerificationNumberRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static KeyVerificationNumberRequest create() => + KeyVerificationNumberRequest._(); + @$core.override + KeyVerificationNumberRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static KeyVerificationNumberRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static KeyVerificationNumberRequest? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get nonce => $_getI64(0); + @$pb.TagNumber(1) + set nonce($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get remoteLongname => $_getSZ(1); + @$pb.TagNumber(2) + set remoteLongname($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasRemoteLongname() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteLongname() => $_clearField(2); +} + +class KeyVerificationFinal extends $pb.GeneratedMessage { + factory KeyVerificationFinal({ + $fixnum.Int64? nonce, + $core.String? remoteLongname, + $core.bool? isSender, + $core.String? verificationCharacters, + }) { + final result = create(); + if (nonce != null) result.nonce = nonce; + if (remoteLongname != null) result.remoteLongname = remoteLongname; + if (isSender != null) result.isSender = isSender; + if (verificationCharacters != null) + result.verificationCharacters = verificationCharacters; + return result; + } + + KeyVerificationFinal._(); + + factory KeyVerificationFinal.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory KeyVerificationFinal.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'KeyVerificationFinal', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..aOS(2, _omitFieldNames ? '' : 'remoteLongname') + ..aOB(3, _omitFieldNames ? '' : 'isSender', protoName: 'isSender') + ..aOS(4, _omitFieldNames ? '' : 'verificationCharacters') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationFinal clone() => + KeyVerificationFinal()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + KeyVerificationFinal copyWith(void Function(KeyVerificationFinal) updates) => + super.copyWith((message) => updates(message as KeyVerificationFinal)) + as KeyVerificationFinal; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static KeyVerificationFinal create() => KeyVerificationFinal._(); + @$core.override + KeyVerificationFinal createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static KeyVerificationFinal getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static KeyVerificationFinal? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get nonce => $_getI64(0); + @$pb.TagNumber(1) + set nonce($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get remoteLongname => $_getSZ(1); + @$pb.TagNumber(2) + set remoteLongname($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasRemoteLongname() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteLongname() => $_clearField(2); + + @$pb.TagNumber(3) + $core.bool get isSender => $_getBF(2); + @$pb.TagNumber(3) + set isSender($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasIsSender() => $_has(2); + @$pb.TagNumber(3) + void clearIsSender() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get verificationCharacters => $_getSZ(3); + @$pb.TagNumber(4) + set verificationCharacters($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasVerificationCharacters() => $_has(3); + @$pb.TagNumber(4) + void clearVerificationCharacters() => $_clearField(4); +} + +class DuplicatedPublicKey extends $pb.GeneratedMessage { + factory DuplicatedPublicKey() => create(); + + DuplicatedPublicKey._(); + + factory DuplicatedPublicKey.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DuplicatedPublicKey.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DuplicatedPublicKey', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DuplicatedPublicKey clone() => DuplicatedPublicKey()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DuplicatedPublicKey copyWith(void Function(DuplicatedPublicKey) updates) => + super.copyWith((message) => updates(message as DuplicatedPublicKey)) + as DuplicatedPublicKey; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DuplicatedPublicKey create() => DuplicatedPublicKey._(); + @$core.override + DuplicatedPublicKey createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DuplicatedPublicKey getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DuplicatedPublicKey? _defaultInstance; +} + +class LowEntropyKey extends $pb.GeneratedMessage { + factory LowEntropyKey() => create(); + + LowEntropyKey._(); + + factory LowEntropyKey.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory LowEntropyKey.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'LowEntropyKey', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LowEntropyKey clone() => LowEntropyKey()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LowEntropyKey copyWith(void Function(LowEntropyKey) updates) => + super.copyWith((message) => updates(message as LowEntropyKey)) + as LowEntropyKey; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LowEntropyKey create() => LowEntropyKey._(); + @$core.override + LowEntropyKey createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LowEntropyKey getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static LowEntropyKey? _defaultInstance; +} + +/// +/// Individual File info for the device +class FileInfo extends $pb.GeneratedMessage { + factory FileInfo({ + $core.String? fileName, + $core.int? sizeBytes, + }) { + final result = create(); + if (fileName != null) result.fileName = fileName; + if (sizeBytes != null) result.sizeBytes = sizeBytes; + return result; + } + + FileInfo._(); + + factory FileInfo.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory FileInfo.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'FileInfo', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'fileName') + ..a<$core.int>(2, _omitFieldNames ? '' : 'sizeBytes', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + FileInfo clone() => FileInfo()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + FileInfo copyWith(void Function(FileInfo) updates) => + super.copyWith((message) => updates(message as FileInfo)) as FileInfo; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static FileInfo create() => FileInfo._(); + @$core.override + FileInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static FileInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static FileInfo? _defaultInstance; + + /// + /// The fully qualified path of the file + @$pb.TagNumber(1) + $core.String get fileName => $_getSZ(0); + @$pb.TagNumber(1) + set fileName($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasFileName() => $_has(0); + @$pb.TagNumber(1) + void clearFileName() => $_clearField(1); + + /// + /// The size of the file in bytes + @$pb.TagNumber(2) + $core.int get sizeBytes => $_getIZ(1); + @$pb.TagNumber(2) + set sizeBytes($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasSizeBytes() => $_has(1); + @$pb.TagNumber(2) + void clearSizeBytes() => $_clearField(2); +} + +enum ToRadio_PayloadVariant { + packet, + wantConfigId, + disconnect, + xmodemPacket, + mqttClientProxyMessage, + heartbeat, + notSet +} + +/// +/// Packets/commands to the radio will be written (reliably) to the toRadio characteristic. +/// Once the write completes the phone can assume it is handled. +class ToRadio extends $pb.GeneratedMessage { + factory ToRadio({ + MeshPacket? packet, + $core.int? wantConfigId, + $core.bool? disconnect, + $4.XModem? xmodemPacket, + MqttClientProxyMessage? mqttClientProxyMessage, + Heartbeat? heartbeat, + }) { + final result = create(); + if (packet != null) result.packet = packet; + if (wantConfigId != null) result.wantConfigId = wantConfigId; + if (disconnect != null) result.disconnect = disconnect; + if (xmodemPacket != null) result.xmodemPacket = xmodemPacket; + if (mqttClientProxyMessage != null) + result.mqttClientProxyMessage = mqttClientProxyMessage; + if (heartbeat != null) result.heartbeat = heartbeat; + return result; + } + + ToRadio._(); + + factory ToRadio.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ToRadio.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, ToRadio_PayloadVariant> + _ToRadio_PayloadVariantByTag = { + 1: ToRadio_PayloadVariant.packet, + 3: ToRadio_PayloadVariant.wantConfigId, + 4: ToRadio_PayloadVariant.disconnect, + 5: ToRadio_PayloadVariant.xmodemPacket, + 6: ToRadio_PayloadVariant.mqttClientProxyMessage, + 7: ToRadio_PayloadVariant.heartbeat, + 0: ToRadio_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ToRadio', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [1, 3, 4, 5, 6, 7]) + ..aOM(1, _omitFieldNames ? '' : 'packet', + subBuilder: MeshPacket.create) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'wantConfigId', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'disconnect') + ..aOM<$4.XModem>(5, _omitFieldNames ? '' : 'xmodemPacket', + protoName: 'xmodemPacket', subBuilder: $4.XModem.create) + ..aOM( + 6, _omitFieldNames ? '' : 'mqttClientProxyMessage', + protoName: 'mqttClientProxyMessage', + subBuilder: MqttClientProxyMessage.create) + ..aOM(7, _omitFieldNames ? '' : 'heartbeat', + subBuilder: Heartbeat.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ToRadio clone() => ToRadio()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ToRadio copyWith(void Function(ToRadio) updates) => + super.copyWith((message) => updates(message as ToRadio)) as ToRadio; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ToRadio create() => ToRadio._(); + @$core.override + ToRadio createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ToRadio getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ToRadio? _defaultInstance; + + ToRadio_PayloadVariant whichPayloadVariant() => + _ToRadio_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// Send this packet on the mesh + @$pb.TagNumber(1) + MeshPacket get packet => $_getN(0); + @$pb.TagNumber(1) + set packet(MeshPacket value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasPacket() => $_has(0); + @$pb.TagNumber(1) + void clearPacket() => $_clearField(1); + @$pb.TagNumber(1) + MeshPacket ensurePacket() => $_ensure(0); + + /// + /// Phone wants radio to send full node db to the phone, This is + /// typically the first packet sent to the radio when the phone gets a + /// bluetooth connection. The radio will respond by sending back a + /// MyNodeInfo, a owner, a radio config and a series of + /// FromRadio.node_infos, and config_complete + /// the integer you write into this field will be reported back in the + /// config_complete_id response this allows clients to never be confused by + /// a stale old partially sent config. + @$pb.TagNumber(3) + $core.int get wantConfigId => $_getIZ(1); + @$pb.TagNumber(3) + set wantConfigId($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(3) + $core.bool hasWantConfigId() => $_has(1); + @$pb.TagNumber(3) + void clearWantConfigId() => $_clearField(3); + + /// + /// Tell API server we are disconnecting now. + /// This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link. + /// (Sending this message is optional for clients) + @$pb.TagNumber(4) + $core.bool get disconnect => $_getBF(2); + @$pb.TagNumber(4) + set disconnect($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(4) + $core.bool hasDisconnect() => $_has(2); + @$pb.TagNumber(4) + void clearDisconnect() => $_clearField(4); + + @$pb.TagNumber(5) + $4.XModem get xmodemPacket => $_getN(3); + @$pb.TagNumber(5) + set xmodemPacket($4.XModem value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasXmodemPacket() => $_has(3); + @$pb.TagNumber(5) + void clearXmodemPacket() => $_clearField(5); + @$pb.TagNumber(5) + $4.XModem ensureXmodemPacket() => $_ensure(3); + + /// + /// MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device) + @$pb.TagNumber(6) + MqttClientProxyMessage get mqttClientProxyMessage => $_getN(4); + @$pb.TagNumber(6) + set mqttClientProxyMessage(MqttClientProxyMessage value) => + $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasMqttClientProxyMessage() => $_has(4); + @$pb.TagNumber(6) + void clearMqttClientProxyMessage() => $_clearField(6); + @$pb.TagNumber(6) + MqttClientProxyMessage ensureMqttClientProxyMessage() => $_ensure(4); + + /// + /// Heartbeat message (used to keep the device connection awake on serial) + @$pb.TagNumber(7) + Heartbeat get heartbeat => $_getN(5); + @$pb.TagNumber(7) + set heartbeat(Heartbeat value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasHeartbeat() => $_has(5); + @$pb.TagNumber(7) + void clearHeartbeat() => $_clearField(7); + @$pb.TagNumber(7) + Heartbeat ensureHeartbeat() => $_ensure(5); +} + +/// +/// Compressed message payload +class Compressed extends $pb.GeneratedMessage { + factory Compressed({ + $6.PortNum? portnum, + $core.List<$core.int>? data, + }) { + final result = create(); + if (portnum != null) result.portnum = portnum; + if (data != null) result.data = data; + return result; + } + + Compressed._(); + + factory Compressed.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Compressed.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Compressed', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e<$6.PortNum>(1, _omitFieldNames ? '' : 'portnum', $pb.PbFieldType.OE, + defaultOrMaker: $6.PortNum.UNKNOWN_APP, + valueOf: $6.PortNum.valueOf, + enumValues: $6.PortNum.values) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Compressed clone() => Compressed()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Compressed copyWith(void Function(Compressed) updates) => + super.copyWith((message) => updates(message as Compressed)) as Compressed; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Compressed create() => Compressed._(); + @$core.override + Compressed createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Compressed getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Compressed? _defaultInstance; + + /// + /// PortNum to determine the how to handle the compressed payload. + @$pb.TagNumber(1) + $6.PortNum get portnum => $_getN(0); + @$pb.TagNumber(1) + set portnum($6.PortNum value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasPortnum() => $_has(0); + @$pb.TagNumber(1) + void clearPortnum() => $_clearField(1); + + /// + /// Compressed data. + @$pb.TagNumber(2) + $core.List<$core.int> get data => $_getN(1); + @$pb.TagNumber(2) + set data($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasData() => $_has(1); + @$pb.TagNumber(2) + void clearData() => $_clearField(2); +} + +/// +/// Full info on edges for a single node +class NeighborInfo extends $pb.GeneratedMessage { + factory NeighborInfo({ + $core.int? nodeId, + $core.int? lastSentById, + $core.int? nodeBroadcastIntervalSecs, + $core.Iterable? neighbors, + }) { + final result = create(); + if (nodeId != null) result.nodeId = nodeId; + if (lastSentById != null) result.lastSentById = lastSentById; + if (nodeBroadcastIntervalSecs != null) + result.nodeBroadcastIntervalSecs = nodeBroadcastIntervalSecs; + if (neighbors != null) result.neighbors.addAll(neighbors); + return result; + } + + NeighborInfo._(); + + factory NeighborInfo.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NeighborInfo.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NeighborInfo', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'nodeId', $pb.PbFieldType.OU3) + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'lastSentById', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'nodeBroadcastIntervalSecs', + $pb.PbFieldType.OU3) + ..pc(4, _omitFieldNames ? '' : 'neighbors', $pb.PbFieldType.PM, + subBuilder: Neighbor.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NeighborInfo clone() => NeighborInfo()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NeighborInfo copyWith(void Function(NeighborInfo) updates) => + super.copyWith((message) => updates(message as NeighborInfo)) + as NeighborInfo; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NeighborInfo create() => NeighborInfo._(); + @$core.override + NeighborInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NeighborInfo getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NeighborInfo? _defaultInstance; + + /// + /// The node ID of the node sending info on its neighbors + @$pb.TagNumber(1) + $core.int get nodeId => $_getIZ(0); + @$pb.TagNumber(1) + set nodeId($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNodeId() => $_has(0); + @$pb.TagNumber(1) + void clearNodeId() => $_clearField(1); + + /// + /// Field to pass neighbor info for the next sending cycle + @$pb.TagNumber(2) + $core.int get lastSentById => $_getIZ(1); + @$pb.TagNumber(2) + set lastSentById($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasLastSentById() => $_has(1); + @$pb.TagNumber(2) + void clearLastSentById() => $_clearField(2); + + /// + /// Broadcast interval of the represented node (in seconds) + @$pb.TagNumber(3) + $core.int get nodeBroadcastIntervalSecs => $_getIZ(2); + @$pb.TagNumber(3) + set nodeBroadcastIntervalSecs($core.int value) => + $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasNodeBroadcastIntervalSecs() => $_has(2); + @$pb.TagNumber(3) + void clearNodeBroadcastIntervalSecs() => $_clearField(3); + + /// + /// The list of out edges from this node + @$pb.TagNumber(4) + $pb.PbList get neighbors => $_getList(3); +} + +/// +/// A single edge in the mesh +class Neighbor extends $pb.GeneratedMessage { + factory Neighbor({ + $core.int? nodeId, + $core.double? snr, + $core.int? lastRxTime, + $core.int? nodeBroadcastIntervalSecs, + }) { + final result = create(); + if (nodeId != null) result.nodeId = nodeId; + if (snr != null) result.snr = snr; + if (lastRxTime != null) result.lastRxTime = lastRxTime; + if (nodeBroadcastIntervalSecs != null) + result.nodeBroadcastIntervalSecs = nodeBroadcastIntervalSecs; + return result; + } + + Neighbor._(); + + factory Neighbor.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Neighbor.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Neighbor', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'nodeId', $pb.PbFieldType.OU3) + ..a<$core.double>(2, _omitFieldNames ? '' : 'snr', $pb.PbFieldType.OF) + ..a<$core.int>(3, _omitFieldNames ? '' : 'lastRxTime', $pb.PbFieldType.OF3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'nodeBroadcastIntervalSecs', + $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Neighbor clone() => Neighbor()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Neighbor copyWith(void Function(Neighbor) updates) => + super.copyWith((message) => updates(message as Neighbor)) as Neighbor; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Neighbor create() => Neighbor._(); + @$core.override + Neighbor createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Neighbor getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Neighbor? _defaultInstance; + + /// + /// Node ID of neighbor + @$pb.TagNumber(1) + $core.int get nodeId => $_getIZ(0); + @$pb.TagNumber(1) + set nodeId($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNodeId() => $_has(0); + @$pb.TagNumber(1) + void clearNodeId() => $_clearField(1); + + /// + /// SNR of last heard message + @$pb.TagNumber(2) + $core.double get snr => $_getN(1); + @$pb.TagNumber(2) + set snr($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasSnr() => $_has(1); + @$pb.TagNumber(2) + void clearSnr() => $_clearField(2); + + /// + /// Reception time (in secs since 1970) of last message that was last sent by this ID. + /// Note: this is for local storage only and will not be sent out over the mesh. + @$pb.TagNumber(3) + $core.int get lastRxTime => $_getIZ(2); + @$pb.TagNumber(3) + set lastRxTime($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasLastRxTime() => $_has(2); + @$pb.TagNumber(3) + void clearLastRxTime() => $_clearField(3); + + /// + /// Broadcast interval of this neighbor (in seconds). + /// Note: this is for local storage only and will not be sent out over the mesh. + @$pb.TagNumber(4) + $core.int get nodeBroadcastIntervalSecs => $_getIZ(3); + @$pb.TagNumber(4) + set nodeBroadcastIntervalSecs($core.int value) => + $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasNodeBroadcastIntervalSecs() => $_has(3); + @$pb.TagNumber(4) + void clearNodeBroadcastIntervalSecs() => $_clearField(4); +} + +/// +/// Device metadata response +class DeviceMetadata extends $pb.GeneratedMessage { + factory DeviceMetadata({ + $core.String? firmwareVersion, + $core.int? deviceStateVersion, + $core.bool? canShutdown, + $core.bool? hasWifi, + $core.bool? hasBluetooth, + $core.bool? hasEthernet, + $1.Config_DeviceConfig_Role? role, + $core.int? positionFlags, + HardwareModel? hwModel, + $core.bool? hasRemoteHardware, + $core.bool? hasPKC, + $core.int? excludedModules, + }) { + final result = create(); + if (firmwareVersion != null) result.firmwareVersion = firmwareVersion; + if (deviceStateVersion != null) + result.deviceStateVersion = deviceStateVersion; + if (canShutdown != null) result.canShutdown = canShutdown; + if (hasWifi != null) result.hasWifi = hasWifi; + if (hasBluetooth != null) result.hasBluetooth = hasBluetooth; + if (hasEthernet != null) result.hasEthernet = hasEthernet; + if (role != null) result.role = role; + if (positionFlags != null) result.positionFlags = positionFlags; + if (hwModel != null) result.hwModel = hwModel; + if (hasRemoteHardware != null) result.hasRemoteHardware = hasRemoteHardware; + if (hasPKC != null) result.hasPKC = hasPKC; + if (excludedModules != null) result.excludedModules = excludedModules; + return result; + } + + DeviceMetadata._(); + + factory DeviceMetadata.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeviceMetadata.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeviceMetadata', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'firmwareVersion') + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'deviceStateVersion', $pb.PbFieldType.OU3) + ..aOB(3, _omitFieldNames ? '' : 'canShutdown', protoName: 'canShutdown') + ..aOB(4, _omitFieldNames ? '' : 'hasWifi', protoName: 'hasWifi') + ..aOB(5, _omitFieldNames ? '' : 'hasBluetooth', protoName: 'hasBluetooth') + ..aOB(6, _omitFieldNames ? '' : 'hasEthernet', protoName: 'hasEthernet') + ..e<$1.Config_DeviceConfig_Role>( + 7, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: $1.Config_DeviceConfig_Role.CLIENT, + valueOf: $1.Config_DeviceConfig_Role.valueOf, + enumValues: $1.Config_DeviceConfig_Role.values) + ..a<$core.int>( + 8, _omitFieldNames ? '' : 'positionFlags', $pb.PbFieldType.OU3) + ..e(9, _omitFieldNames ? '' : 'hwModel', $pb.PbFieldType.OE, + defaultOrMaker: HardwareModel.UNSET, + valueOf: HardwareModel.valueOf, + enumValues: HardwareModel.values) + ..aOB(10, _omitFieldNames ? '' : 'hasRemoteHardware', + protoName: 'hasRemoteHardware') + ..aOB(11, _omitFieldNames ? '' : 'hasPKC', protoName: 'hasPKC') + ..a<$core.int>( + 12, _omitFieldNames ? '' : 'excludedModules', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceMetadata clone() => DeviceMetadata()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceMetadata copyWith(void Function(DeviceMetadata) updates) => + super.copyWith((message) => updates(message as DeviceMetadata)) + as DeviceMetadata; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceMetadata create() => DeviceMetadata._(); + @$core.override + DeviceMetadata createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceMetadata getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeviceMetadata? _defaultInstance; + + /// + /// Device firmware version string + @$pb.TagNumber(1) + $core.String get firmwareVersion => $_getSZ(0); + @$pb.TagNumber(1) + set firmwareVersion($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasFirmwareVersion() => $_has(0); + @$pb.TagNumber(1) + void clearFirmwareVersion() => $_clearField(1); + + /// + /// Device state version + @$pb.TagNumber(2) + $core.int get deviceStateVersion => $_getIZ(1); + @$pb.TagNumber(2) + set deviceStateVersion($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasDeviceStateVersion() => $_has(1); + @$pb.TagNumber(2) + void clearDeviceStateVersion() => $_clearField(2); + + /// + /// Indicates whether the device can shutdown CPU natively or via power management chip + @$pb.TagNumber(3) + $core.bool get canShutdown => $_getBF(2); + @$pb.TagNumber(3) + set canShutdown($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasCanShutdown() => $_has(2); + @$pb.TagNumber(3) + void clearCanShutdown() => $_clearField(3); + + /// + /// Indicates that the device has native wifi capability + @$pb.TagNumber(4) + $core.bool get hasWifi => $_getBF(3); + @$pb.TagNumber(4) + set hasWifi($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasHasWifi() => $_has(3); + @$pb.TagNumber(4) + void clearHasWifi() => $_clearField(4); + + /// + /// Indicates that the device has native bluetooth capability + @$pb.TagNumber(5) + $core.bool get hasBluetooth => $_getBF(4); + @$pb.TagNumber(5) + set hasBluetooth($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasHasBluetooth() => $_has(4); + @$pb.TagNumber(5) + void clearHasBluetooth() => $_clearField(5); + + /// + /// Indicates that the device has an ethernet peripheral + @$pb.TagNumber(6) + $core.bool get hasEthernet => $_getBF(5); + @$pb.TagNumber(6) + set hasEthernet($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasHasEthernet() => $_has(5); + @$pb.TagNumber(6) + void clearHasEthernet() => $_clearField(6); + + /// + /// Indicates that the device's role in the mesh + @$pb.TagNumber(7) + $1.Config_DeviceConfig_Role get role => $_getN(6); + @$pb.TagNumber(7) + set role($1.Config_DeviceConfig_Role value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasRole() => $_has(6); + @$pb.TagNumber(7) + void clearRole() => $_clearField(7); + + /// + /// Indicates the device's current enabled position flags + @$pb.TagNumber(8) + $core.int get positionFlags => $_getIZ(7); + @$pb.TagNumber(8) + set positionFlags($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasPositionFlags() => $_has(7); + @$pb.TagNumber(8) + void clearPositionFlags() => $_clearField(8); + + /// + /// Device hardware model + @$pb.TagNumber(9) + HardwareModel get hwModel => $_getN(8); + @$pb.TagNumber(9) + set hwModel(HardwareModel value) => $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasHwModel() => $_has(8); + @$pb.TagNumber(9) + void clearHwModel() => $_clearField(9); + + /// + /// Has Remote Hardware enabled + @$pb.TagNumber(10) + $core.bool get hasRemoteHardware => $_getBF(9); + @$pb.TagNumber(10) + set hasRemoteHardware($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasHasRemoteHardware() => $_has(9); + @$pb.TagNumber(10) + void clearHasRemoteHardware() => $_clearField(10); + + /// + /// Has PKC capabilities + @$pb.TagNumber(11) + $core.bool get hasPKC => $_getBF(10); + @$pb.TagNumber(11) + set hasPKC($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(11) + $core.bool hasHasPKC() => $_has(10); + @$pb.TagNumber(11) + void clearHasPKC() => $_clearField(11); + + /// + /// Bit field of boolean for excluded modules + /// (bitwise OR of ExcludedModules) + @$pb.TagNumber(12) + $core.int get excludedModules => $_getIZ(11); + @$pb.TagNumber(12) + set excludedModules($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasExcludedModules() => $_has(11); + @$pb.TagNumber(12) + void clearExcludedModules() => $_clearField(12); +} + +/// +/// A heartbeat message is sent to the node from the client to keep the connection alive. +/// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. +class Heartbeat extends $pb.GeneratedMessage { + factory Heartbeat({ + $core.int? nonce, + }) { + final result = create(); + if (nonce != null) result.nonce = nonce; + return result; + } + + Heartbeat._(); + + factory Heartbeat.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Heartbeat.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Heartbeat', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Heartbeat clone() => Heartbeat()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Heartbeat copyWith(void Function(Heartbeat) updates) => + super.copyWith((message) => updates(message as Heartbeat)) as Heartbeat; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Heartbeat create() => Heartbeat._(); + @$core.override + Heartbeat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Heartbeat getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Heartbeat? _defaultInstance; + + /// + /// The nonce of the heartbeat message + @$pb.TagNumber(1) + $core.int get nonce => $_getIZ(0); + @$pb.TagNumber(1) + set nonce($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => $_clearField(1); +} + +/// +/// RemoteHardwarePins associated with a node +class NodeRemoteHardwarePin extends $pb.GeneratedMessage { + factory NodeRemoteHardwarePin({ + $core.int? nodeNum, + $2.RemoteHardwarePin? pin, + }) { + final result = create(); + if (nodeNum != null) result.nodeNum = nodeNum; + if (pin != null) result.pin = pin; + return result; + } + + NodeRemoteHardwarePin._(); + + factory NodeRemoteHardwarePin.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeRemoteHardwarePin.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeRemoteHardwarePin', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'nodeNum', $pb.PbFieldType.OU3) + ..aOM<$2.RemoteHardwarePin>(2, _omitFieldNames ? '' : 'pin', + subBuilder: $2.RemoteHardwarePin.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeRemoteHardwarePin clone() => + NodeRemoteHardwarePin()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeRemoteHardwarePin copyWith( + void Function(NodeRemoteHardwarePin) updates) => + super.copyWith((message) => updates(message as NodeRemoteHardwarePin)) + as NodeRemoteHardwarePin; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeRemoteHardwarePin create() => NodeRemoteHardwarePin._(); + @$core.override + NodeRemoteHardwarePin createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NodeRemoteHardwarePin getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeRemoteHardwarePin? _defaultInstance; + + /// + /// The node_num exposing the available gpio pin + @$pb.TagNumber(1) + $core.int get nodeNum => $_getIZ(0); + @$pb.TagNumber(1) + set nodeNum($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasNodeNum() => $_has(0); + @$pb.TagNumber(1) + void clearNodeNum() => $_clearField(1); + + /// + /// The the available gpio pin for usage with RemoteHardware module + @$pb.TagNumber(2) + $2.RemoteHardwarePin get pin => $_getN(1); + @$pb.TagNumber(2) + set pin($2.RemoteHardwarePin value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasPin() => $_has(1); + @$pb.TagNumber(2) + void clearPin() => $_clearField(2); + @$pb.TagNumber(2) + $2.RemoteHardwarePin ensurePin() => $_ensure(1); +} + +class ChunkedPayload extends $pb.GeneratedMessage { + factory ChunkedPayload({ + $core.int? payloadId, + $core.int? chunkCount, + $core.int? chunkIndex, + $core.List<$core.int>? payloadChunk, + }) { + final result = create(); + if (payloadId != null) result.payloadId = payloadId; + if (chunkCount != null) result.chunkCount = chunkCount; + if (chunkIndex != null) result.chunkIndex = chunkIndex; + if (payloadChunk != null) result.payloadChunk = payloadChunk; + return result; + } + + ChunkedPayload._(); + + factory ChunkedPayload.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ChunkedPayload.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ChunkedPayload', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'payloadId', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'chunkCount', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'chunkIndex', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'payloadChunk', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChunkedPayload clone() => ChunkedPayload()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChunkedPayload copyWith(void Function(ChunkedPayload) updates) => + super.copyWith((message) => updates(message as ChunkedPayload)) + as ChunkedPayload; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChunkedPayload create() => ChunkedPayload._(); + @$core.override + ChunkedPayload createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChunkedPayload getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ChunkedPayload? _defaultInstance; + + /// + /// The ID of the entire payload + @$pb.TagNumber(1) + $core.int get payloadId => $_getIZ(0); + @$pb.TagNumber(1) + set payloadId($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPayloadId() => $_has(0); + @$pb.TagNumber(1) + void clearPayloadId() => $_clearField(1); + + /// + /// The total number of chunks in the payload + @$pb.TagNumber(2) + $core.int get chunkCount => $_getIZ(1); + @$pb.TagNumber(2) + set chunkCount($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasChunkCount() => $_has(1); + @$pb.TagNumber(2) + void clearChunkCount() => $_clearField(2); + + /// + /// The current chunk index in the total + @$pb.TagNumber(3) + $core.int get chunkIndex => $_getIZ(2); + @$pb.TagNumber(3) + set chunkIndex($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasChunkIndex() => $_has(2); + @$pb.TagNumber(3) + void clearChunkIndex() => $_clearField(3); + + /// + /// The binary data of the current chunk + @$pb.TagNumber(4) + $core.List<$core.int> get payloadChunk => $_getN(3); + @$pb.TagNumber(4) + set payloadChunk($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasPayloadChunk() => $_has(3); + @$pb.TagNumber(4) + void clearPayloadChunk() => $_clearField(4); +} + +/// +/// Wrapper message for broken repeated oneof support +class resend_chunks extends $pb.GeneratedMessage { + factory resend_chunks({ + $core.Iterable<$core.int>? chunks, + }) { + final result = create(); + if (chunks != null) result.chunks.addAll(chunks); + return result; + } + + resend_chunks._(); + + factory resend_chunks.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory resend_chunks.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'resend_chunks', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..p<$core.int>(1, _omitFieldNames ? '' : 'chunks', $pb.PbFieldType.KU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + resend_chunks clone() => resend_chunks()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + resend_chunks copyWith(void Function(resend_chunks) updates) => + super.copyWith((message) => updates(message as resend_chunks)) + as resend_chunks; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static resend_chunks create() => resend_chunks._(); + @$core.override + resend_chunks createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static resend_chunks getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static resend_chunks? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList<$core.int> get chunks => $_getList(0); +} + +enum ChunkedPayloadResponse_PayloadVariant { + requestTransfer, + acceptTransfer, + resendChunks, + notSet +} + +/// +/// Responses to a ChunkedPayload request +class ChunkedPayloadResponse extends $pb.GeneratedMessage { + factory ChunkedPayloadResponse({ + $core.int? payloadId, + $core.bool? requestTransfer, + $core.bool? acceptTransfer, + resend_chunks? resendChunks, + }) { + final result = create(); + if (payloadId != null) result.payloadId = payloadId; + if (requestTransfer != null) result.requestTransfer = requestTransfer; + if (acceptTransfer != null) result.acceptTransfer = acceptTransfer; + if (resendChunks != null) result.resendChunks = resendChunks; + return result; + } + + ChunkedPayloadResponse._(); + + factory ChunkedPayloadResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ChunkedPayloadResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, ChunkedPayloadResponse_PayloadVariant> + _ChunkedPayloadResponse_PayloadVariantByTag = { + 2: ChunkedPayloadResponse_PayloadVariant.requestTransfer, + 3: ChunkedPayloadResponse_PayloadVariant.acceptTransfer, + 4: ChunkedPayloadResponse_PayloadVariant.resendChunks, + 0: ChunkedPayloadResponse_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ChunkedPayloadResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [2, 3, 4]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'payloadId', $pb.PbFieldType.OU3) + ..aOB(2, _omitFieldNames ? '' : 'requestTransfer') + ..aOB(3, _omitFieldNames ? '' : 'acceptTransfer') + ..aOM(4, _omitFieldNames ? '' : 'resendChunks', + subBuilder: resend_chunks.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChunkedPayloadResponse clone() => + ChunkedPayloadResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ChunkedPayloadResponse copyWith( + void Function(ChunkedPayloadResponse) updates) => + super.copyWith((message) => updates(message as ChunkedPayloadResponse)) + as ChunkedPayloadResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ChunkedPayloadResponse create() => ChunkedPayloadResponse._(); + @$core.override + ChunkedPayloadResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ChunkedPayloadResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ChunkedPayloadResponse? _defaultInstance; + + ChunkedPayloadResponse_PayloadVariant whichPayloadVariant() => + _ChunkedPayloadResponse_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// The ID of the entire payload + @$pb.TagNumber(1) + $core.int get payloadId => $_getIZ(0); + @$pb.TagNumber(1) + set payloadId($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPayloadId() => $_has(0); + @$pb.TagNumber(1) + void clearPayloadId() => $_clearField(1); + + /// + /// Request to transfer chunked payload + @$pb.TagNumber(2) + $core.bool get requestTransfer => $_getBF(1); + @$pb.TagNumber(2) + set requestTransfer($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasRequestTransfer() => $_has(1); + @$pb.TagNumber(2) + void clearRequestTransfer() => $_clearField(2); + + /// + /// Accept the transfer chunked payload + @$pb.TagNumber(3) + $core.bool get acceptTransfer => $_getBF(2); + @$pb.TagNumber(3) + set acceptTransfer($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasAcceptTransfer() => $_has(2); + @$pb.TagNumber(3) + void clearAcceptTransfer() => $_clearField(3); + + /// + /// Request missing indexes in the chunked payload + @$pb.TagNumber(4) + resend_chunks get resendChunks => $_getN(3); + @$pb.TagNumber(4) + set resendChunks(resend_chunks value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasResendChunks() => $_has(3); + @$pb.TagNumber(4) + void clearResendChunks() => $_clearField(4); + @$pb.TagNumber(4) + resend_chunks ensureResendChunks() => $_ensure(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/mesh.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/mesh.pbenum.dart new file mode 100644 index 000000000..3d1bbc1ca --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/mesh.pbenum.dart @@ -0,0 +1,1479 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/mesh.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// Note: these enum names must EXACTLY match the string used in the device +/// bin/build-all.sh script. +/// Because they will be used to find firmware filenames in the android app for OTA updates. +/// To match the old style filenames, _ is converted to -, p is converted to . +class HardwareModel extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const HardwareModel UNSET = + HardwareModel._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// TODO: REPLACE + static const HardwareModel TLORA_V2 = + HardwareModel._(1, _omitEnumNames ? '' : 'TLORA_V2'); + + /// + /// TODO: REPLACE + static const HardwareModel TLORA_V1 = + HardwareModel._(2, _omitEnumNames ? '' : 'TLORA_V1'); + + /// + /// TODO: REPLACE + static const HardwareModel TLORA_V2_1_1P6 = + HardwareModel._(3, _omitEnumNames ? '' : 'TLORA_V2_1_1P6'); + + /// + /// TODO: REPLACE + static const HardwareModel TBEAM = + HardwareModel._(4, _omitEnumNames ? '' : 'TBEAM'); + + /// + /// The original heltec WiFi_Lora_32_V2, which had battery voltage sensing hooked to GPIO 13 + /// (see HELTEC_V2 for the new version). + static const HardwareModel HELTEC_V2_0 = + HardwareModel._(5, _omitEnumNames ? '' : 'HELTEC_V2_0'); + + /// + /// TODO: REPLACE + static const HardwareModel TBEAM_V0P7 = + HardwareModel._(6, _omitEnumNames ? '' : 'TBEAM_V0P7'); + + /// + /// TODO: REPLACE + static const HardwareModel T_ECHO = + HardwareModel._(7, _omitEnumNames ? '' : 'T_ECHO'); + + /// + /// TODO: REPLACE + static const HardwareModel TLORA_V1_1P3 = + HardwareModel._(8, _omitEnumNames ? '' : 'TLORA_V1_1P3'); + + /// + /// TODO: REPLACE + static const HardwareModel RAK4631 = + HardwareModel._(9, _omitEnumNames ? '' : 'RAK4631'); + + /// + /// The new version of the heltec WiFi_Lora_32_V2 board that has battery sensing hooked to GPIO 37. + /// Sadly they did not update anything on the silkscreen to identify this board + static const HardwareModel HELTEC_V2_1 = + HardwareModel._(10, _omitEnumNames ? '' : 'HELTEC_V2_1'); + + /// + /// Ancient heltec WiFi_Lora_32 board + static const HardwareModel HELTEC_V1 = + HardwareModel._(11, _omitEnumNames ? '' : 'HELTEC_V1'); + + /// + /// New T-BEAM with ESP32-S3 CPU + static const HardwareModel LILYGO_TBEAM_S3_CORE = + HardwareModel._(12, _omitEnumNames ? '' : 'LILYGO_TBEAM_S3_CORE'); + + /// + /// RAK WisBlock ESP32 core: https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200/Overview/ + static const HardwareModel RAK11200 = + HardwareModel._(13, _omitEnumNames ? '' : 'RAK11200'); + + /// + /// B&Q Consulting Nano Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:nano + static const HardwareModel NANO_G1 = + HardwareModel._(14, _omitEnumNames ? '' : 'NANO_G1'); + + /// + /// TODO: REPLACE + static const HardwareModel TLORA_V2_1_1P8 = + HardwareModel._(15, _omitEnumNames ? '' : 'TLORA_V2_1_1P8'); + + /// + /// TODO: REPLACE + static const HardwareModel TLORA_T3_S3 = + HardwareModel._(16, _omitEnumNames ? '' : 'TLORA_T3_S3'); + + /// + /// B&Q Consulting Nano G1 Explorer: https://wiki.uniteng.com/en/meshtastic/nano-g1-explorer + static const HardwareModel NANO_G1_EXPLORER = + HardwareModel._(17, _omitEnumNames ? '' : 'NANO_G1_EXPLORER'); + + /// + /// B&Q Consulting Nano G2 Ultra: https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra + static const HardwareModel NANO_G2_ULTRA = + HardwareModel._(18, _omitEnumNames ? '' : 'NANO_G2_ULTRA'); + + /// + /// LoRAType device: https://loratype.org/ + static const HardwareModel LORA_TYPE = + HardwareModel._(19, _omitEnumNames ? '' : 'LORA_TYPE'); + + /// + /// wiphone https://www.wiphone.io/ + static const HardwareModel WIPHONE = + HardwareModel._(20, _omitEnumNames ? '' : 'WIPHONE'); + + /// + /// WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk + static const HardwareModel WIO_WM1110 = + HardwareModel._(21, _omitEnumNames ? '' : 'WIO_WM1110'); + + /// + /// RAK2560 Solar base station based on RAK4630 + static const HardwareModel RAK2560 = + HardwareModel._(22, _omitEnumNames ? '' : 'RAK2560'); + + /// + /// Heltec HRU-3601: https://heltec.org/project/hru-3601/ + static const HardwareModel HELTEC_HRU_3601 = + HardwareModel._(23, _omitEnumNames ? '' : 'HELTEC_HRU_3601'); + + /// + /// Heltec Wireless Bridge + static const HardwareModel HELTEC_WIRELESS_BRIDGE = + HardwareModel._(24, _omitEnumNames ? '' : 'HELTEC_WIRELESS_BRIDGE'); + + /// + /// B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station + static const HardwareModel STATION_G1 = + HardwareModel._(25, _omitEnumNames ? '' : 'STATION_G1'); + + /// + /// RAK11310 (RP2040 + SX1262) + static const HardwareModel RAK11310 = + HardwareModel._(26, _omitEnumNames ? '' : 'RAK11310'); + + /// + /// Makerfabs SenseLoRA Receiver (RP2040 + RFM96) + static const HardwareModel SENSELORA_RP2040 = + HardwareModel._(27, _omitEnumNames ? '' : 'SENSELORA_RP2040'); + + /// + /// Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96) + static const HardwareModel SENSELORA_S3 = + HardwareModel._(28, _omitEnumNames ? '' : 'SENSELORA_S3'); + + /// + /// Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone + static const HardwareModel CANARYONE = + HardwareModel._(29, _omitEnumNames ? '' : 'CANARYONE'); + + /// + /// Waveshare RP2040 LoRa - https://www.waveshare.com/rp2040-lora.htm + static const HardwareModel RP2040_LORA = + HardwareModel._(30, _omitEnumNames ? '' : 'RP2040_LORA'); + + /// + /// B&Q Consulting Station G2: https://wiki.uniteng.com/en/meshtastic/station-g2 + static const HardwareModel STATION_G2 = + HardwareModel._(31, _omitEnumNames ? '' : 'STATION_G2'); + + /// + /// --------------------------------------------------------------------------- + /// Less common/prototype boards listed here (needs one more byte over the air) + /// --------------------------------------------------------------------------- + static const HardwareModel LORA_RELAY_V1 = + HardwareModel._(32, _omitEnumNames ? '' : 'LORA_RELAY_V1'); + + /// + /// TODO: REPLACE + static const HardwareModel NRF52840DK = + HardwareModel._(33, _omitEnumNames ? '' : 'NRF52840DK'); + + /// + /// TODO: REPLACE + static const HardwareModel PPR = + HardwareModel._(34, _omitEnumNames ? '' : 'PPR'); + + /// + /// TODO: REPLACE + static const HardwareModel GENIEBLOCKS = + HardwareModel._(35, _omitEnumNames ? '' : 'GENIEBLOCKS'); + + /// + /// TODO: REPLACE + static const HardwareModel NRF52_UNKNOWN = + HardwareModel._(36, _omitEnumNames ? '' : 'NRF52_UNKNOWN'); + + /// + /// TODO: REPLACE + static const HardwareModel PORTDUINO = + HardwareModel._(37, _omitEnumNames ? '' : 'PORTDUINO'); + + /// + /// The simulator built into the android app + static const HardwareModel ANDROID_SIM = + HardwareModel._(38, _omitEnumNames ? '' : 'ANDROID_SIM'); + + /// + /// Custom DIY device based on @NanoVHF schematics: https://github.com/NanoVHF/Meshtastic-DIY/tree/main/Schematics + static const HardwareModel DIY_V1 = + HardwareModel._(39, _omitEnumNames ? '' : 'DIY_V1'); + + /// + /// nRF52840 Dongle : https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle/ + static const HardwareModel NRF52840_PCA10059 = + HardwareModel._(40, _omitEnumNames ? '' : 'NRF52840_PCA10059'); + + /// + /// Custom Disaster Radio esp32 v3 device https://github.com/sudomesh/disaster-radio/tree/master/hardware/board_esp32_v3 + static const HardwareModel DR_DEV = + HardwareModel._(41, _omitEnumNames ? '' : 'DR_DEV'); + + /// + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ + static const HardwareModel M5STACK = + HardwareModel._(42, _omitEnumNames ? '' : 'M5STACK'); + + /// + /// New Heltec LoRA32 with ESP32-S3 CPU + static const HardwareModel HELTEC_V3 = + HardwareModel._(43, _omitEnumNames ? '' : 'HELTEC_V3'); + + /// + /// New Heltec Wireless Stick Lite with ESP32-S3 CPU + static const HardwareModel HELTEC_WSL_V3 = + HardwareModel._(44, _omitEnumNames ? '' : 'HELTEC_WSL_V3'); + + /// + /// New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU + static const HardwareModel BETAFPV_2400_TX = + HardwareModel._(45, _omitEnumNames ? '' : 'BETAFPV_2400_TX'); + + /// + /// BetaFPV ExpressLRS "Nano" TX Module 900MHz with ESP32 CPU + static const HardwareModel BETAFPV_900_NANO_TX = + HardwareModel._(46, _omitEnumNames ? '' : 'BETAFPV_900_NANO_TX'); + + /// + /// Raspberry Pi Pico (W) with Waveshare SX1262 LoRa Node Module + static const HardwareModel RPI_PICO = + HardwareModel._(47, _omitEnumNames ? '' : 'RPI_PICO'); + + /// + /// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT + /// Newer V1.1, version is written on the PCB near the display. + static const HardwareModel HELTEC_WIRELESS_TRACKER = + HardwareModel._(48, _omitEnumNames ? '' : 'HELTEC_WIRELESS_TRACKER'); + + /// + /// Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display + static const HardwareModel HELTEC_WIRELESS_PAPER = + HardwareModel._(49, _omitEnumNames ? '' : 'HELTEC_WIRELESS_PAPER'); + + /// + /// LilyGo T-Deck with ESP32-S3 CPU, Keyboard and IPS display + static const HardwareModel T_DECK = + HardwareModel._(50, _omitEnumNames ? '' : 'T_DECK'); + + /// + /// LilyGo T-Watch S3 with ESP32-S3 CPU and IPS display + static const HardwareModel T_WATCH_S3 = + HardwareModel._(51, _omitEnumNames ? '' : 'T_WATCH_S3'); + + /// + /// Bobricius Picomputer with ESP32-S3 CPU, Keyboard and IPS display + static const HardwareModel PICOMPUTER_S3 = + HardwareModel._(52, _omitEnumNames ? '' : 'PICOMPUTER_S3'); + + /// + /// Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa + static const HardwareModel HELTEC_HT62 = + HardwareModel._(53, _omitEnumNames ? '' : 'HELTEC_HT62'); + + /// + /// EBYTE SPI LoRa module and ESP32-S3 + static const HardwareModel EBYTE_ESP32_S3 = + HardwareModel._(54, _omitEnumNames ? '' : 'EBYTE_ESP32_S3'); + + /// + /// Waveshare ESP32-S3-PICO with PICO LoRa HAT and 2.9inch e-Ink + static const HardwareModel ESP32_S3_PICO = + HardwareModel._(55, _omitEnumNames ? '' : 'ESP32_S3_PICO'); + + /// + /// CircuitMess Chatter 2 LLCC68 Lora Module and ESP32 Wroom + /// Lora module can be swapped out for a Heltec RA-62 which is "almost" pin compatible + /// with one cut and one jumper Meshtastic works + static const HardwareModel CHATTER_2 = + HardwareModel._(56, _omitEnumNames ? '' : 'CHATTER_2'); + + /// + /// Heltec Wireless Paper, With ESP32-S3 CPU and E-Ink display + /// Older "V1.0" Variant, has no "version sticker" + /// E-Ink model is DEPG0213BNS800 + /// Tab on the screen protector is RED + /// Flex connector marking is FPC-7528B + static const HardwareModel HELTEC_WIRELESS_PAPER_V1_0 = + HardwareModel._(57, _omitEnumNames ? '' : 'HELTEC_WIRELESS_PAPER_V1_0'); + + /// + /// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT + /// Older "V1.0" Variant + static const HardwareModel HELTEC_WIRELESS_TRACKER_V1_0 = + HardwareModel._(58, _omitEnumNames ? '' : 'HELTEC_WIRELESS_TRACKER_V1_0'); + + /// + /// unPhone with ESP32-S3, TFT touchscreen, LSM6DS3TR-C accelerometer and gyroscope + static const HardwareModel UNPHONE = + HardwareModel._(59, _omitEnumNames ? '' : 'UNPHONE'); + + /// + /// Teledatics TD-LORAC NRF52840 based M.2 LoRA module + /// Compatible with the TD-WRLS development board + static const HardwareModel TD_LORAC = + HardwareModel._(60, _omitEnumNames ? '' : 'TD_LORAC'); + + /// + /// CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3 + static const HardwareModel CDEBYTE_EORA_S3 = + HardwareModel._(61, _omitEnumNames ? '' : 'CDEBYTE_EORA_S3'); + + /// + /// TWC_MESH_V4 + /// Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS + static const HardwareModel TWC_MESH_V4 = + HardwareModel._(62, _omitEnumNames ? '' : 'TWC_MESH_V4'); + + /// + /// NRF52_PROMICRO_DIY + /// Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS + static const HardwareModel NRF52_PROMICRO_DIY = + HardwareModel._(63, _omitEnumNames ? '' : 'NRF52_PROMICRO_DIY'); + + /// + /// RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module + /// ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS + static const HardwareModel RADIOMASTER_900_BANDIT_NANO = + HardwareModel._(64, _omitEnumNames ? '' : 'RADIOMASTER_900_BANDIT_NANO'); + + /// + /// Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors + static const HardwareModel HELTEC_CAPSULE_SENSOR_V3 = + HardwareModel._(65, _omitEnumNames ? '' : 'HELTEC_CAPSULE_SENSOR_V3'); + + /// + /// Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display + static const HardwareModel HELTEC_VISION_MASTER_T190 = + HardwareModel._(66, _omitEnumNames ? '' : 'HELTEC_VISION_MASTER_T190'); + + /// + /// Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display + static const HardwareModel HELTEC_VISION_MASTER_E213 = + HardwareModel._(67, _omitEnumNames ? '' : 'HELTEC_VISION_MASTER_E213'); + + /// + /// Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display + static const HardwareModel HELTEC_VISION_MASTER_E290 = + HardwareModel._(68, _omitEnumNames ? '' : 'HELTEC_VISION_MASTER_E290'); + + /// + /// Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design, + /// specifically adapted for the Meshtatic project + static const HardwareModel HELTEC_MESH_NODE_T114 = + HardwareModel._(69, _omitEnumNames ? '' : 'HELTEC_MESH_NODE_T114'); + + /// + /// Sensecap Indicator from Seeed Studio. ESP32-S3 device with TFT and RP2040 coprocessor + static const HardwareModel SENSECAP_INDICATOR = + HardwareModel._(70, _omitEnumNames ? '' : 'SENSECAP_INDICATOR'); + + /// + /// Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors. + static const HardwareModel TRACKER_T1000_E = + HardwareModel._(71, _omitEnumNames ? '' : 'TRACKER_T1000_E'); + + /// + /// RAK3172 STM32WLE5 Module (https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172) + static const HardwareModel RAK3172 = + HardwareModel._(72, _omitEnumNames ? '' : 'RAK3172'); + + /// + /// Seeed Studio Wio-E5 (either mini or Dev kit) using STM32WL chip. + static const HardwareModel WIO_E5 = + HardwareModel._(73, _omitEnumNames ? '' : 'WIO_E5'); + + /// + /// RadioMaster 900 Bandit, https://www.radiomasterrc.com/products/bandit-expresslrs-rf-module + /// SSD1306 OLED and No GPS + static const HardwareModel RADIOMASTER_900_BANDIT = + HardwareModel._(74, _omitEnumNames ? '' : 'RADIOMASTER_900_BANDIT'); + + /// + /// Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. + static const HardwareModel ME25LS01_4Y10TD = + HardwareModel._(75, _omitEnumNames ? '' : 'ME25LS01_4Y10TD'); + + /// + /// RP2040_FEATHER_RFM95 + /// Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED + /// https://www.adafruit.com/product/5714 + /// https://www.adafruit.com/product/326 + /// https://www.adafruit.com/product/938 + /// ^^^ short A0 to switch to I2C address 0x3C + static const HardwareModel RP2040_FEATHER_RFM95 = + HardwareModel._(76, _omitEnumNames ? '' : 'RP2040_FEATHER_RFM95'); + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ + static const HardwareModel M5STACK_COREBASIC = + HardwareModel._(77, _omitEnumNames ? '' : 'M5STACK_COREBASIC'); + static const HardwareModel M5STACK_CORE2 = + HardwareModel._(78, _omitEnumNames ? '' : 'M5STACK_CORE2'); + + /// Pico2 with Waveshare Hat, same as Pico + static const HardwareModel RPI_PICO2 = + HardwareModel._(79, _omitEnumNames ? '' : 'RPI_PICO2'); + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ + static const HardwareModel M5STACK_CORES3 = + HardwareModel._(80, _omitEnumNames ? '' : 'M5STACK_CORES3'); + + /// Seeed XIAO S3 DK + static const HardwareModel SEEED_XIAO_S3 = + HardwareModel._(81, _omitEnumNames ? '' : 'SEEED_XIAO_S3'); + + /// + /// Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1 + static const HardwareModel MS24SF1 = + HardwareModel._(82, _omitEnumNames ? '' : 'MS24SF1'); + + /// + /// Lilygo TLora-C6 with the new ESP32-C6 MCU + static const HardwareModel TLORA_C6 = + HardwareModel._(83, _omitEnumNames ? '' : 'TLORA_C6'); + + /// + /// WisMesh Tap + /// RAK-4631 w/ TFT in injection modled case + static const HardwareModel WISMESH_TAP = + HardwareModel._(84, _omitEnumNames ? '' : 'WISMESH_TAP'); + + /// + /// Similar to PORTDUINO but used by Routastic devices, this is not any + /// particular device and does not run Meshtastic's code but supports + /// the same frame format. + /// Runs on linux, see https://github.com/Jorropo/routastic + static const HardwareModel ROUTASTIC = + HardwareModel._(85, _omitEnumNames ? '' : 'ROUTASTIC'); + + /// + /// Mesh-Tab, esp32 based + /// https://github.com/valzzu/Mesh-Tab + static const HardwareModel MESH_TAB = + HardwareModel._(86, _omitEnumNames ? '' : 'MESH_TAB'); + + /// + /// MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog + /// https://www.loraitalia.it + static const HardwareModel MESHLINK = + HardwareModel._(87, _omitEnumNames ? '' : 'MESHLINK'); + + /// + /// Seeed XIAO nRF52840 + Wio SX1262 kit + static const HardwareModel XIAO_NRF52_KIT = + HardwareModel._(88, _omitEnumNames ? '' : 'XIAO_NRF52_KIT'); + + /// + /// Elecrow ThinkNode M1 & M2 + /// https://www.elecrow.com/wiki/ThinkNode-M1_Transceiver_Device(Meshtastic)_Power_By_nRF52840.html + /// https://www.elecrow.com/wiki/ThinkNode-M2_Transceiver_Device(Meshtastic)_Power_By_NRF52840.html (this actually uses ESP32-S3) + static const HardwareModel THINKNODE_M1 = + HardwareModel._(89, _omitEnumNames ? '' : 'THINKNODE_M1'); + static const HardwareModel THINKNODE_M2 = + HardwareModel._(90, _omitEnumNames ? '' : 'THINKNODE_M2'); + + /// + /// Lilygo T-ETH-Elite + static const HardwareModel T_ETH_ELITE = + HardwareModel._(91, _omitEnumNames ? '' : 'T_ETH_ELITE'); + + /// + /// Heltec HRI-3621 industrial probe + static const HardwareModel HELTEC_SENSOR_HUB = + HardwareModel._(92, _omitEnumNames ? '' : 'HELTEC_SENSOR_HUB'); + + /// + /// Reserved Fried Chicken ID for future use + static const HardwareModel RESERVED_FRIED_CHICKEN = + HardwareModel._(93, _omitEnumNames ? '' : 'RESERVED_FRIED_CHICKEN'); + + /// + /// Heltec Magnetic Power Bank with Meshtastic compatible + static const HardwareModel HELTEC_MESH_POCKET = + HardwareModel._(94, _omitEnumNames ? '' : 'HELTEC_MESH_POCKET'); + + /// + /// Seeed Solar Node + static const HardwareModel SEEED_SOLAR_NODE = + HardwareModel._(95, _omitEnumNames ? '' : 'SEEED_SOLAR_NODE'); + + /// + /// NomadStar Meteor Pro https://nomadstar.ch/ + static const HardwareModel NOMADSTAR_METEOR_PRO = + HardwareModel._(96, _omitEnumNames ? '' : 'NOMADSTAR_METEOR_PRO'); + + /// + /// Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin + static const HardwareModel CROWPANEL = + HardwareModel._(97, _omitEnumNames ? '' : 'CROWPANEL'); + + /// + /// Lilygo LINK32 board with sensors + static const HardwareModel LINK_32 = + HardwareModel._(98, _omitEnumNames ? '' : 'LINK_32'); + + /// + /// Seeed Tracker L1 + static const HardwareModel SEEED_WIO_TRACKER_L1 = + HardwareModel._(99, _omitEnumNames ? '' : 'SEEED_WIO_TRACKER_L1'); + + /// + /// Seeed Tracker L1 EINK driver + static const HardwareModel SEEED_WIO_TRACKER_L1_EINK = + HardwareModel._(100, _omitEnumNames ? '' : 'SEEED_WIO_TRACKER_L1_EINK'); + + /// + /// Reserved ID for future and past use + static const HardwareModel QWANTZ_TINY_ARMS = + HardwareModel._(101, _omitEnumNames ? '' : 'QWANTZ_TINY_ARMS'); + + /// + /// Lilygo T-Deck Pro + static const HardwareModel T_DECK_PRO = + HardwareModel._(102, _omitEnumNames ? '' : 'T_DECK_PRO'); + + /// + /// Lilygo TLora Pager + static const HardwareModel T_LORA_PAGER = + HardwareModel._(103, _omitEnumNames ? '' : 'T_LORA_PAGER'); + + /// + /// GAT562 Mesh Trial Tracker + static const HardwareModel GAT562_MESH_TRIAL_TRACKER = + HardwareModel._(104, _omitEnumNames ? '' : 'GAT562_MESH_TRIAL_TRACKER'); + + /// + /// RAKwireless WisMesh Tag + static const HardwareModel WISMESH_TAG = + HardwareModel._(105, _omitEnumNames ? '' : 'WISMESH_TAG'); + + /// + /// RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ + static const HardwareModel RAK3312 = + HardwareModel._(106, _omitEnumNames ? '' : 'RAK3312'); + + /// + /// Elecrow ThinkNode M5 https://www.elecrow.com/wiki/ThinkNode_M5_Meshtastic_LoRa_Signal_Transceiver_ESP32-S3.html + static const HardwareModel THINKNODE_M5 = + HardwareModel._(107, _omitEnumNames ? '' : 'THINKNODE_M5'); + + /// + /// MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices. + /// https://heltec.org/project/meshsolar/ + static const HardwareModel HELTEC_MESH_SOLAR = + HardwareModel._(108, _omitEnumNames ? '' : 'HELTEC_MESH_SOLAR'); + + /// + /// Lilygo T-Echo Lite + static const HardwareModel T_ECHO_LITE = + HardwareModel._(109, _omitEnumNames ? '' : 'T_ECHO_LITE'); + + /// + /// ------------------------------------------------------------------------------------------------------------------------------------------ + /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. + /// ------------------------------------------------------------------------------------------------------------------------------------------ + static const HardwareModel PRIVATE_HW = + HardwareModel._(255, _omitEnumNames ? '' : 'PRIVATE_HW'); + + static const $core.List values = [ + UNSET, + TLORA_V2, + TLORA_V1, + TLORA_V2_1_1P6, + TBEAM, + HELTEC_V2_0, + TBEAM_V0P7, + T_ECHO, + TLORA_V1_1P3, + RAK4631, + HELTEC_V2_1, + HELTEC_V1, + LILYGO_TBEAM_S3_CORE, + RAK11200, + NANO_G1, + TLORA_V2_1_1P8, + TLORA_T3_S3, + NANO_G1_EXPLORER, + NANO_G2_ULTRA, + LORA_TYPE, + WIPHONE, + WIO_WM1110, + RAK2560, + HELTEC_HRU_3601, + HELTEC_WIRELESS_BRIDGE, + STATION_G1, + RAK11310, + SENSELORA_RP2040, + SENSELORA_S3, + CANARYONE, + RP2040_LORA, + STATION_G2, + LORA_RELAY_V1, + NRF52840DK, + PPR, + GENIEBLOCKS, + NRF52_UNKNOWN, + PORTDUINO, + ANDROID_SIM, + DIY_V1, + NRF52840_PCA10059, + DR_DEV, + M5STACK, + HELTEC_V3, + HELTEC_WSL_V3, + BETAFPV_2400_TX, + BETAFPV_900_NANO_TX, + RPI_PICO, + HELTEC_WIRELESS_TRACKER, + HELTEC_WIRELESS_PAPER, + T_DECK, + T_WATCH_S3, + PICOMPUTER_S3, + HELTEC_HT62, + EBYTE_ESP32_S3, + ESP32_S3_PICO, + CHATTER_2, + HELTEC_WIRELESS_PAPER_V1_0, + HELTEC_WIRELESS_TRACKER_V1_0, + UNPHONE, + TD_LORAC, + CDEBYTE_EORA_S3, + TWC_MESH_V4, + NRF52_PROMICRO_DIY, + RADIOMASTER_900_BANDIT_NANO, + HELTEC_CAPSULE_SENSOR_V3, + HELTEC_VISION_MASTER_T190, + HELTEC_VISION_MASTER_E213, + HELTEC_VISION_MASTER_E290, + HELTEC_MESH_NODE_T114, + SENSECAP_INDICATOR, + TRACKER_T1000_E, + RAK3172, + WIO_E5, + RADIOMASTER_900_BANDIT, + ME25LS01_4Y10TD, + RP2040_FEATHER_RFM95, + M5STACK_COREBASIC, + M5STACK_CORE2, + RPI_PICO2, + M5STACK_CORES3, + SEEED_XIAO_S3, + MS24SF1, + TLORA_C6, + WISMESH_TAP, + ROUTASTIC, + MESH_TAB, + MESHLINK, + XIAO_NRF52_KIT, + THINKNODE_M1, + THINKNODE_M2, + T_ETH_ELITE, + HELTEC_SENSOR_HUB, + RESERVED_FRIED_CHICKEN, + HELTEC_MESH_POCKET, + SEEED_SOLAR_NODE, + NOMADSTAR_METEOR_PRO, + CROWPANEL, + LINK_32, + SEEED_WIO_TRACKER_L1, + SEEED_WIO_TRACKER_L1_EINK, + QWANTZ_TINY_ARMS, + T_DECK_PRO, + T_LORA_PAGER, + GAT562_MESH_TRIAL_TRACKER, + WISMESH_TAG, + RAK3312, + THINKNODE_M5, + HELTEC_MESH_SOLAR, + T_ECHO_LITE, + PRIVATE_HW, + ]; + + static final $core.Map<$core.int, HardwareModel> _byValue = + $pb.ProtobufEnum.initByValue(values); + static HardwareModel? valueOf($core.int value) => _byValue[value]; + + const HardwareModel._(super.value, super.name); +} + +/// +/// Shared constants between device and phone +class Constants extends $pb.ProtobufEnum { + /// + /// First enum must be zero, and we are just using this enum to + /// pass int constants between two very different environments + static const Constants ZERO = Constants._(0, _omitEnumNames ? '' : 'ZERO'); + + /// + /// From mesh.options + /// note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is + /// outside of this envelope + static const Constants DATA_PAYLOAD_LEN = + Constants._(233, _omitEnumNames ? '' : 'DATA_PAYLOAD_LEN'); + + static const $core.List values = [ + ZERO, + DATA_PAYLOAD_LEN, + ]; + + static final $core.Map<$core.int, Constants> _byValue = + $pb.ProtobufEnum.initByValue(values); + static Constants? valueOf($core.int value) => _byValue[value]; + + const Constants._(super.value, super.name); +} + +/// +/// Error codes for critical errors +/// The device might report these fault codes on the screen. +/// If you encounter a fault code, please post on the meshtastic.discourse.group +/// and we'll try to help. +class CriticalErrorCode extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const CriticalErrorCode NONE = + CriticalErrorCode._(0, _omitEnumNames ? '' : 'NONE'); + + /// + /// A software bug was detected while trying to send lora + static const CriticalErrorCode TX_WATCHDOG = + CriticalErrorCode._(1, _omitEnumNames ? '' : 'TX_WATCHDOG'); + + /// + /// A software bug was detected on entry to sleep + static const CriticalErrorCode SLEEP_ENTER_WAIT = + CriticalErrorCode._(2, _omitEnumNames ? '' : 'SLEEP_ENTER_WAIT'); + + /// + /// No Lora radio hardware could be found + static const CriticalErrorCode NO_RADIO = + CriticalErrorCode._(3, _omitEnumNames ? '' : 'NO_RADIO'); + + /// + /// Not normally used + static const CriticalErrorCode UNSPECIFIED = + CriticalErrorCode._(4, _omitEnumNames ? '' : 'UNSPECIFIED'); + + /// + /// We failed while configuring a UBlox GPS + static const CriticalErrorCode UBLOX_UNIT_FAILED = + CriticalErrorCode._(5, _omitEnumNames ? '' : 'UBLOX_UNIT_FAILED'); + + /// + /// This board was expected to have a power management chip and it is missing or broken + static const CriticalErrorCode NO_AXP192 = + CriticalErrorCode._(6, _omitEnumNames ? '' : 'NO_AXP192'); + + /// + /// The channel tried to set a radio setting which is not supported by this chipset, + /// radio comms settings are now undefined. + static const CriticalErrorCode INVALID_RADIO_SETTING = + CriticalErrorCode._(7, _omitEnumNames ? '' : 'INVALID_RADIO_SETTING'); + + /// + /// Radio transmit hardware failure. We sent data to the radio chip, but it didn't + /// reply with an interrupt. + static const CriticalErrorCode TRANSMIT_FAILED = + CriticalErrorCode._(8, _omitEnumNames ? '' : 'TRANSMIT_FAILED'); + + /// + /// We detected that the main CPU voltage dropped below the minimum acceptable value + static const CriticalErrorCode BROWNOUT = + CriticalErrorCode._(9, _omitEnumNames ? '' : 'BROWNOUT'); + + /// Selftest of SX1262 radio chip failed + static const CriticalErrorCode SX1262_FAILURE = + CriticalErrorCode._(10, _omitEnumNames ? '' : 'SX1262_FAILURE'); + + /// + /// A (likely software but possibly hardware) failure was detected while trying to send packets. + /// If this occurs on your board, please post in the forum so that we can ask you to collect some information to allow fixing this bug + static const CriticalErrorCode RADIO_SPI_BUG = + CriticalErrorCode._(11, _omitEnumNames ? '' : 'RADIO_SPI_BUG'); + + /// + /// Corruption was detected on the flash filesystem but we were able to repair things. + /// If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. + static const CriticalErrorCode FLASH_CORRUPTION_RECOVERABLE = + CriticalErrorCode._( + 12, _omitEnumNames ? '' : 'FLASH_CORRUPTION_RECOVERABLE'); + + /// + /// Corruption was detected on the flash filesystem but we were unable to repair things. + /// NOTE: Your node will probably need to be reconfigured the next time it reboots (it will lose the region code etc...) + /// If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. + static const CriticalErrorCode FLASH_CORRUPTION_UNRECOVERABLE = + CriticalErrorCode._( + 13, _omitEnumNames ? '' : 'FLASH_CORRUPTION_UNRECOVERABLE'); + + static const $core.List values = [ + NONE, + TX_WATCHDOG, + SLEEP_ENTER_WAIT, + NO_RADIO, + UNSPECIFIED, + UBLOX_UNIT_FAILED, + NO_AXP192, + INVALID_RADIO_SETTING, + TRANSMIT_FAILED, + BROWNOUT, + SX1262_FAILURE, + RADIO_SPI_BUG, + FLASH_CORRUPTION_RECOVERABLE, + FLASH_CORRUPTION_UNRECOVERABLE, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 13); + static CriticalErrorCode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const CriticalErrorCode._(super.value, super.name); +} + +/// +/// Enum to indicate to clients whether this firmware is a special firmware build, like an event. +/// The first 16 values are reserved for non-event special firmwares, like the Smart Citizen use case. +class FirmwareEdition extends $pb.ProtobufEnum { + /// + /// Vanilla firmware + static const FirmwareEdition VANILLA = + FirmwareEdition._(0, _omitEnumNames ? '' : 'VANILLA'); + + /// + /// Firmware for use in the Smart Citizen environmental monitoring network + static const FirmwareEdition SMART_CITIZEN = + FirmwareEdition._(1, _omitEnumNames ? '' : 'SMART_CITIZEN'); + + /// + /// Open Sauce, the maker conference held yearly in CA + static const FirmwareEdition OPEN_SAUCE = + FirmwareEdition._(16, _omitEnumNames ? '' : 'OPEN_SAUCE'); + + /// + /// DEFCON, the yearly hacker conference + static const FirmwareEdition DEFCON = + FirmwareEdition._(17, _omitEnumNames ? '' : 'DEFCON'); + + /// + /// Burning Man, the yearly hippie gathering in the desert + static const FirmwareEdition BURNING_MAN = + FirmwareEdition._(18, _omitEnumNames ? '' : 'BURNING_MAN'); + + /// + /// Hamvention, the Dayton amateur radio convention + static const FirmwareEdition HAMVENTION = + FirmwareEdition._(19, _omitEnumNames ? '' : 'HAMVENTION'); + + /// + /// Placeholder for DIY and unofficial events + static const FirmwareEdition DIY_EDITION = + FirmwareEdition._(127, _omitEnumNames ? '' : 'DIY_EDITION'); + + static const $core.List values = [ + VANILLA, + SMART_CITIZEN, + OPEN_SAUCE, + DEFCON, + BURNING_MAN, + HAMVENTION, + DIY_EDITION, + ]; + + static final $core.Map<$core.int, FirmwareEdition> _byValue = + $pb.ProtobufEnum.initByValue(values); + static FirmwareEdition? valueOf($core.int value) => _byValue[value]; + + const FirmwareEdition._(super.value, super.name); +} + +/// +/// Enum for modules excluded from a device's configuration. +/// Each value represents a ModuleConfigType that can be toggled as excluded +/// by setting its corresponding bit in the `excluded_modules` bitmask field. +class ExcludedModules extends $pb.ProtobufEnum { + /// + /// Default value of 0 indicates no modules are excluded. + static const ExcludedModules EXCLUDED_NONE = + ExcludedModules._(0, _omitEnumNames ? '' : 'EXCLUDED_NONE'); + + /// + /// MQTT module + static const ExcludedModules MQTT_CONFIG = + ExcludedModules._(1, _omitEnumNames ? '' : 'MQTT_CONFIG'); + + /// + /// Serial module + static const ExcludedModules SERIAL_CONFIG = + ExcludedModules._(2, _omitEnumNames ? '' : 'SERIAL_CONFIG'); + + /// + /// External Notification module + static const ExcludedModules EXTNOTIF_CONFIG = + ExcludedModules._(4, _omitEnumNames ? '' : 'EXTNOTIF_CONFIG'); + + /// + /// Store and Forward module + static const ExcludedModules STOREFORWARD_CONFIG = + ExcludedModules._(8, _omitEnumNames ? '' : 'STOREFORWARD_CONFIG'); + + /// + /// Range Test module + static const ExcludedModules RANGETEST_CONFIG = + ExcludedModules._(16, _omitEnumNames ? '' : 'RANGETEST_CONFIG'); + + /// + /// Telemetry module + static const ExcludedModules TELEMETRY_CONFIG = + ExcludedModules._(32, _omitEnumNames ? '' : 'TELEMETRY_CONFIG'); + + /// + /// Canned Message module + static const ExcludedModules CANNEDMSG_CONFIG = + ExcludedModules._(64, _omitEnumNames ? '' : 'CANNEDMSG_CONFIG'); + + /// + /// Audio module + static const ExcludedModules AUDIO_CONFIG = + ExcludedModules._(128, _omitEnumNames ? '' : 'AUDIO_CONFIG'); + + /// + /// Remote Hardware module + static const ExcludedModules REMOTEHARDWARE_CONFIG = + ExcludedModules._(256, _omitEnumNames ? '' : 'REMOTEHARDWARE_CONFIG'); + + /// + /// Neighbor Info module + static const ExcludedModules NEIGHBORINFO_CONFIG = + ExcludedModules._(512, _omitEnumNames ? '' : 'NEIGHBORINFO_CONFIG'); + + /// + /// Ambient Lighting module + static const ExcludedModules AMBIENTLIGHTING_CONFIG = + ExcludedModules._(1024, _omitEnumNames ? '' : 'AMBIENTLIGHTING_CONFIG'); + + /// + /// Detection Sensor module + static const ExcludedModules DETECTIONSENSOR_CONFIG = + ExcludedModules._(2048, _omitEnumNames ? '' : 'DETECTIONSENSOR_CONFIG'); + + /// + /// Paxcounter module + static const ExcludedModules PAXCOUNTER_CONFIG = + ExcludedModules._(4096, _omitEnumNames ? '' : 'PAXCOUNTER_CONFIG'); + + /// + /// Bluetooth config (not technically a module, but used to indicate bluetooth capabilities) + static const ExcludedModules BLUETOOTH_CONFIG = + ExcludedModules._(8192, _omitEnumNames ? '' : 'BLUETOOTH_CONFIG'); + + /// + /// Network config (not technically a module, but used to indicate network capabilities) + static const ExcludedModules NETWORK_CONFIG = + ExcludedModules._(16384, _omitEnumNames ? '' : 'NETWORK_CONFIG'); + + static const $core.List values = [ + EXCLUDED_NONE, + MQTT_CONFIG, + SERIAL_CONFIG, + EXTNOTIF_CONFIG, + STOREFORWARD_CONFIG, + RANGETEST_CONFIG, + TELEMETRY_CONFIG, + CANNEDMSG_CONFIG, + AUDIO_CONFIG, + REMOTEHARDWARE_CONFIG, + NEIGHBORINFO_CONFIG, + AMBIENTLIGHTING_CONFIG, + DETECTIONSENSOR_CONFIG, + PAXCOUNTER_CONFIG, + BLUETOOTH_CONFIG, + NETWORK_CONFIG, + ]; + + static final $core.Map<$core.int, ExcludedModules> _byValue = + $pb.ProtobufEnum.initByValue(values); + static ExcludedModules? valueOf($core.int value) => _byValue[value]; + + const ExcludedModules._(super.value, super.name); +} + +/// +/// How the location was acquired: manual, onboard GPS, external (EUD) GPS +class Position_LocSource extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const Position_LocSource LOC_UNSET = + Position_LocSource._(0, _omitEnumNames ? '' : 'LOC_UNSET'); + + /// + /// TODO: REPLACE + static const Position_LocSource LOC_MANUAL = + Position_LocSource._(1, _omitEnumNames ? '' : 'LOC_MANUAL'); + + /// + /// TODO: REPLACE + static const Position_LocSource LOC_INTERNAL = + Position_LocSource._(2, _omitEnumNames ? '' : 'LOC_INTERNAL'); + + /// + /// TODO: REPLACE + static const Position_LocSource LOC_EXTERNAL = + Position_LocSource._(3, _omitEnumNames ? '' : 'LOC_EXTERNAL'); + + static const $core.List values = [ + LOC_UNSET, + LOC_MANUAL, + LOC_INTERNAL, + LOC_EXTERNAL, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 3); + static Position_LocSource? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Position_LocSource._(super.value, super.name); +} + +/// +/// How the altitude was acquired: manual, GPS int/ext, etc +/// Default: same as location_source if present +class Position_AltSource extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const Position_AltSource ALT_UNSET = + Position_AltSource._(0, _omitEnumNames ? '' : 'ALT_UNSET'); + + /// + /// TODO: REPLACE + static const Position_AltSource ALT_MANUAL = + Position_AltSource._(1, _omitEnumNames ? '' : 'ALT_MANUAL'); + + /// + /// TODO: REPLACE + static const Position_AltSource ALT_INTERNAL = + Position_AltSource._(2, _omitEnumNames ? '' : 'ALT_INTERNAL'); + + /// + /// TODO: REPLACE + static const Position_AltSource ALT_EXTERNAL = + Position_AltSource._(3, _omitEnumNames ? '' : 'ALT_EXTERNAL'); + + /// + /// TODO: REPLACE + static const Position_AltSource ALT_BAROMETRIC = + Position_AltSource._(4, _omitEnumNames ? '' : 'ALT_BAROMETRIC'); + + static const $core.List values = [ + ALT_UNSET, + ALT_MANUAL, + ALT_INTERNAL, + ALT_EXTERNAL, + ALT_BAROMETRIC, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 4); + static Position_AltSource? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const Position_AltSource._(super.value, super.name); +} + +/// +/// A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide +/// details on the type of failure). +class Routing_Error extends $pb.ProtobufEnum { + /// + /// This message is not a failure + static const Routing_Error NONE = + Routing_Error._(0, _omitEnumNames ? '' : 'NONE'); + + /// + /// Our node doesn't have a route to the requested destination anymore. + static const Routing_Error NO_ROUTE = + Routing_Error._(1, _omitEnumNames ? '' : 'NO_ROUTE'); + + /// + /// We received a nak while trying to forward on your behalf + static const Routing_Error GOT_NAK = + Routing_Error._(2, _omitEnumNames ? '' : 'GOT_NAK'); + + /// + /// TODO: REPLACE + static const Routing_Error TIMEOUT = + Routing_Error._(3, _omitEnumNames ? '' : 'TIMEOUT'); + + /// + /// No suitable interface could be found for delivering this packet + static const Routing_Error NO_INTERFACE = + Routing_Error._(4, _omitEnumNames ? '' : 'NO_INTERFACE'); + + /// + /// We reached the max retransmission count (typically for naive flood routing) + static const Routing_Error MAX_RETRANSMIT = + Routing_Error._(5, _omitEnumNames ? '' : 'MAX_RETRANSMIT'); + + /// + /// No suitable channel was found for sending this packet (i.e. was requested channel index disabled?) + static const Routing_Error NO_CHANNEL = + Routing_Error._(6, _omitEnumNames ? '' : 'NO_CHANNEL'); + + /// + /// The packet was too big for sending (exceeds interface MTU after encoding) + static const Routing_Error TOO_LARGE = + Routing_Error._(7, _omitEnumNames ? '' : 'TOO_LARGE'); + + /// + /// The request had want_response set, the request reached the destination node, but no service on that node wants to send a response + /// (possibly due to bad channel permissions) + static const Routing_Error NO_RESPONSE = + Routing_Error._(8, _omitEnumNames ? '' : 'NO_RESPONSE'); + + /// + /// Cannot send currently because duty cycle regulations will be violated. + static const Routing_Error DUTY_CYCLE_LIMIT = + Routing_Error._(9, _omitEnumNames ? '' : 'DUTY_CYCLE_LIMIT'); + + /// + /// The application layer service on the remote node received your request, but considered your request somehow invalid + static const Routing_Error BAD_REQUEST = + Routing_Error._(32, _omitEnumNames ? '' : 'BAD_REQUEST'); + + /// + /// The application layer service on the remote node received your request, but considered your request not authorized + /// (i.e you did not send the request on the required bound channel) + static const Routing_Error NOT_AUTHORIZED = + Routing_Error._(33, _omitEnumNames ? '' : 'NOT_AUTHORIZED'); + + /// + /// The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all) + static const Routing_Error PKI_FAILED = + Routing_Error._(34, _omitEnumNames ? '' : 'PKI_FAILED'); + + /// + /// The receiving node does not have a Public Key to decode with + static const Routing_Error PKI_UNKNOWN_PUBKEY = + Routing_Error._(35, _omitEnumNames ? '' : 'PKI_UNKNOWN_PUBKEY'); + + /// + /// Admin packet otherwise checks out, but uses a bogus or expired session key + static const Routing_Error ADMIN_BAD_SESSION_KEY = + Routing_Error._(36, _omitEnumNames ? '' : 'ADMIN_BAD_SESSION_KEY'); + + /// + /// Admin packet sent using PKC, but not from a public key on the admin key list + static const Routing_Error ADMIN_PUBLIC_KEY_UNAUTHORIZED = Routing_Error._( + 37, _omitEnumNames ? '' : 'ADMIN_PUBLIC_KEY_UNAUTHORIZED'); + + /// + /// Airtime fairness rate limit exceeded for a packet + /// This typically enforced per portnum and is used to prevent a single node from monopolizing airtime + static const Routing_Error RATE_LIMIT_EXCEEDED = + Routing_Error._(38, _omitEnumNames ? '' : 'RATE_LIMIT_EXCEEDED'); + + static const $core.List values = [ + NONE, + NO_ROUTE, + GOT_NAK, + TIMEOUT, + NO_INTERFACE, + MAX_RETRANSMIT, + NO_CHANNEL, + TOO_LARGE, + NO_RESPONSE, + DUTY_CYCLE_LIMIT, + BAD_REQUEST, + NOT_AUTHORIZED, + PKI_FAILED, + PKI_UNKNOWN_PUBKEY, + ADMIN_BAD_SESSION_KEY, + ADMIN_PUBLIC_KEY_UNAUTHORIZED, + RATE_LIMIT_EXCEEDED, + ]; + + static final $core.Map<$core.int, Routing_Error> _byValue = + $pb.ProtobufEnum.initByValue(values); + static Routing_Error? valueOf($core.int value) => _byValue[value]; + + const Routing_Error._(super.value, super.name); +} + +/// +/// The priority of this message for sending. +/// Higher priorities are sent first (when managing the transmit queue). +/// This field is never sent over the air, it is only used internally inside of a local device node. +/// API clients (either on the local node or connected directly to the node) +/// can set this parameter if necessary. +/// (values must be <= 127 to keep protobuf field to one byte in size. +/// Detailed background on this field: +/// I noticed a funny side effect of lora being so slow: Usually when making +/// a protocol there isn’t much need to use message priority to change the order +/// of transmission (because interfaces are fairly fast). +/// But for lora where packets can take a few seconds each, it is very important +/// to make sure that critical packets are sent ASAP. +/// In the case of meshtastic that means we want to send protocol acks as soon as possible +/// (to prevent unneeded retransmissions), we want routing messages to be sent next, +/// then messages marked as reliable and finally 'background' packets like periodic position updates. +/// So I bit the bullet and implemented a new (internal - not sent over the air) +/// field in MeshPacket called 'priority'. +/// And the transmission queue in the router object is now a priority queue. +class MeshPacket_Priority extends $pb.ProtobufEnum { + /// + /// Treated as Priority.DEFAULT + static const MeshPacket_Priority UNSET = + MeshPacket_Priority._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// TODO: REPLACE + static const MeshPacket_Priority MIN = + MeshPacket_Priority._(1, _omitEnumNames ? '' : 'MIN'); + + /// + /// Background position updates are sent with very low priority - + /// if the link is super congested they might not go out at all + static const MeshPacket_Priority BACKGROUND = + MeshPacket_Priority._(10, _omitEnumNames ? '' : 'BACKGROUND'); + + /// + /// This priority is used for most messages that don't have a priority set + static const MeshPacket_Priority DEFAULT = + MeshPacket_Priority._(64, _omitEnumNames ? '' : 'DEFAULT'); + + /// + /// If priority is unset but the message is marked as want_ack, + /// assume it is important and use a slightly higher priority + static const MeshPacket_Priority RELIABLE = + MeshPacket_Priority._(70, _omitEnumNames ? '' : 'RELIABLE'); + + /// + /// If priority is unset but the packet is a response to a request, we want it to get there relatively quickly. + /// Furthermore, responses stop relaying packets directed to a node early. + static const MeshPacket_Priority RESPONSE = + MeshPacket_Priority._(80, _omitEnumNames ? '' : 'RESPONSE'); + + /// + /// Higher priority for specific message types (portnums) to distinguish between other reliable packets. + static const MeshPacket_Priority HIGH = + MeshPacket_Priority._(100, _omitEnumNames ? '' : 'HIGH'); + + /// + /// Higher priority alert message used for critical alerts which take priority over other reliable packets. + static const MeshPacket_Priority ALERT = + MeshPacket_Priority._(110, _omitEnumNames ? '' : 'ALERT'); + + /// + /// Ack/naks are sent with very high priority to ensure that retransmission + /// stops as soon as possible + static const MeshPacket_Priority ACK = + MeshPacket_Priority._(120, _omitEnumNames ? '' : 'ACK'); + + /// + /// TODO: REPLACE + static const MeshPacket_Priority MAX = + MeshPacket_Priority._(127, _omitEnumNames ? '' : 'MAX'); + + static const $core.List values = [ + UNSET, + MIN, + BACKGROUND, + DEFAULT, + RELIABLE, + RESPONSE, + HIGH, + ALERT, + ACK, + MAX, + ]; + + static final $core.Map<$core.int, MeshPacket_Priority> _byValue = + $pb.ProtobufEnum.initByValue(values); + static MeshPacket_Priority? valueOf($core.int value) => _byValue[value]; + + const MeshPacket_Priority._(super.value, super.name); +} + +/// +/// Identify if this is a delayed packet +class MeshPacket_Delayed extends $pb.ProtobufEnum { + /// + /// If unset, the message is being sent in real time. + static const MeshPacket_Delayed NO_DELAY = + MeshPacket_Delayed._(0, _omitEnumNames ? '' : 'NO_DELAY'); + + /// + /// The message is delayed and was originally a broadcast + static const MeshPacket_Delayed DELAYED_BROADCAST = + MeshPacket_Delayed._(1, _omitEnumNames ? '' : 'DELAYED_BROADCAST'); + + /// + /// The message is delayed and was originally a direct message + static const MeshPacket_Delayed DELAYED_DIRECT = + MeshPacket_Delayed._(2, _omitEnumNames ? '' : 'DELAYED_DIRECT'); + + static const $core.List values = [ + NO_DELAY, + DELAYED_BROADCAST, + DELAYED_DIRECT, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static MeshPacket_Delayed? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const MeshPacket_Delayed._(super.value, super.name); +} + +/// +/// Enum to identify which transport mechanism this packet arrived over +class MeshPacket_TransportMechanism extends $pb.ProtobufEnum { + /// + /// The default case is that the node generated a packet itself + static const MeshPacket_TransportMechanism TRANSPORT_INTERNAL = + MeshPacket_TransportMechanism._( + 0, _omitEnumNames ? '' : 'TRANSPORT_INTERNAL'); + + /// + /// Arrived via the primary LoRa radio + static const MeshPacket_TransportMechanism TRANSPORT_LORA = + MeshPacket_TransportMechanism._( + 1, _omitEnumNames ? '' : 'TRANSPORT_LORA'); + + /// + /// Arrived via a secondary LoRa radio + static const MeshPacket_TransportMechanism TRANSPORT_LORA_ALT1 = + MeshPacket_TransportMechanism._( + 2, _omitEnumNames ? '' : 'TRANSPORT_LORA_ALT1'); + + /// + /// Arrived via a tertiary LoRa radio + static const MeshPacket_TransportMechanism TRANSPORT_LORA_ALT2 = + MeshPacket_TransportMechanism._( + 3, _omitEnumNames ? '' : 'TRANSPORT_LORA_ALT2'); + + /// + /// Arrived via a quaternary LoRa radio + static const MeshPacket_TransportMechanism TRANSPORT_LORA_ALT3 = + MeshPacket_TransportMechanism._( + 4, _omitEnumNames ? '' : 'TRANSPORT_LORA_ALT3'); + + /// + /// Arrived via an MQTT connection + static const MeshPacket_TransportMechanism TRANSPORT_MQTT = + MeshPacket_TransportMechanism._( + 5, _omitEnumNames ? '' : 'TRANSPORT_MQTT'); + + /// + /// Arrived via Multicast UDP + static const MeshPacket_TransportMechanism TRANSPORT_MULTICAST_UDP = + MeshPacket_TransportMechanism._( + 6, _omitEnumNames ? '' : 'TRANSPORT_MULTICAST_UDP'); + + /// + /// Arrived via API connection + static const MeshPacket_TransportMechanism TRANSPORT_API = + MeshPacket_TransportMechanism._(7, _omitEnumNames ? '' : 'TRANSPORT_API'); + + static const $core.List values = + [ + TRANSPORT_INTERNAL, + TRANSPORT_LORA, + TRANSPORT_LORA_ALT1, + TRANSPORT_LORA_ALT2, + TRANSPORT_LORA_ALT3, + TRANSPORT_MQTT, + TRANSPORT_MULTICAST_UDP, + TRANSPORT_API, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 7); + static MeshPacket_TransportMechanism? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const MeshPacket_TransportMechanism._(super.value, super.name); +} + +/// +/// Log levels, chosen to match python logging conventions. +class LogRecord_Level extends $pb.ProtobufEnum { + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level UNSET = + LogRecord_Level._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level CRITICAL = + LogRecord_Level._(50, _omitEnumNames ? '' : 'CRITICAL'); + + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level ERROR = + LogRecord_Level._(40, _omitEnumNames ? '' : 'ERROR'); + + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level WARNING = + LogRecord_Level._(30, _omitEnumNames ? '' : 'WARNING'); + + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level INFO = + LogRecord_Level._(20, _omitEnumNames ? '' : 'INFO'); + + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level DEBUG = + LogRecord_Level._(10, _omitEnumNames ? '' : 'DEBUG'); + + /// + /// Log levels, chosen to match python logging conventions. + static const LogRecord_Level TRACE = + LogRecord_Level._(5, _omitEnumNames ? '' : 'TRACE'); + + static const $core.List values = [ + UNSET, + CRITICAL, + ERROR, + WARNING, + INFO, + DEBUG, + TRACE, + ]; + + static final $core.Map<$core.int, LogRecord_Level> _byValue = + $pb.ProtobufEnum.initByValue(values); + static LogRecord_Level? valueOf($core.int value) => _byValue[value]; + + const LogRecord_Level._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/mesh.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/mesh.pbjson.dart new file mode 100644 index 000000000..6e813c5e5 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/mesh.pbjson.dart @@ -0,0 +1,1649 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/mesh.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use hardwareModelDescriptor instead') +const HardwareModel$json = { + '1': 'HardwareModel', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'TLORA_V2', '2': 1}, + {'1': 'TLORA_V1', '2': 2}, + {'1': 'TLORA_V2_1_1P6', '2': 3}, + {'1': 'TBEAM', '2': 4}, + {'1': 'HELTEC_V2_0', '2': 5}, + {'1': 'TBEAM_V0P7', '2': 6}, + {'1': 'T_ECHO', '2': 7}, + {'1': 'TLORA_V1_1P3', '2': 8}, + {'1': 'RAK4631', '2': 9}, + {'1': 'HELTEC_V2_1', '2': 10}, + {'1': 'HELTEC_V1', '2': 11}, + {'1': 'LILYGO_TBEAM_S3_CORE', '2': 12}, + {'1': 'RAK11200', '2': 13}, + {'1': 'NANO_G1', '2': 14}, + {'1': 'TLORA_V2_1_1P8', '2': 15}, + {'1': 'TLORA_T3_S3', '2': 16}, + {'1': 'NANO_G1_EXPLORER', '2': 17}, + {'1': 'NANO_G2_ULTRA', '2': 18}, + {'1': 'LORA_TYPE', '2': 19}, + {'1': 'WIPHONE', '2': 20}, + {'1': 'WIO_WM1110', '2': 21}, + {'1': 'RAK2560', '2': 22}, + {'1': 'HELTEC_HRU_3601', '2': 23}, + {'1': 'HELTEC_WIRELESS_BRIDGE', '2': 24}, + {'1': 'STATION_G1', '2': 25}, + {'1': 'RAK11310', '2': 26}, + {'1': 'SENSELORA_RP2040', '2': 27}, + {'1': 'SENSELORA_S3', '2': 28}, + {'1': 'CANARYONE', '2': 29}, + {'1': 'RP2040_LORA', '2': 30}, + {'1': 'STATION_G2', '2': 31}, + {'1': 'LORA_RELAY_V1', '2': 32}, + {'1': 'NRF52840DK', '2': 33}, + {'1': 'PPR', '2': 34}, + {'1': 'GENIEBLOCKS', '2': 35}, + {'1': 'NRF52_UNKNOWN', '2': 36}, + {'1': 'PORTDUINO', '2': 37}, + {'1': 'ANDROID_SIM', '2': 38}, + {'1': 'DIY_V1', '2': 39}, + {'1': 'NRF52840_PCA10059', '2': 40}, + {'1': 'DR_DEV', '2': 41}, + {'1': 'M5STACK', '2': 42}, + {'1': 'HELTEC_V3', '2': 43}, + {'1': 'HELTEC_WSL_V3', '2': 44}, + {'1': 'BETAFPV_2400_TX', '2': 45}, + {'1': 'BETAFPV_900_NANO_TX', '2': 46}, + {'1': 'RPI_PICO', '2': 47}, + {'1': 'HELTEC_WIRELESS_TRACKER', '2': 48}, + {'1': 'HELTEC_WIRELESS_PAPER', '2': 49}, + {'1': 'T_DECK', '2': 50}, + {'1': 'T_WATCH_S3', '2': 51}, + {'1': 'PICOMPUTER_S3', '2': 52}, + {'1': 'HELTEC_HT62', '2': 53}, + {'1': 'EBYTE_ESP32_S3', '2': 54}, + {'1': 'ESP32_S3_PICO', '2': 55}, + {'1': 'CHATTER_2', '2': 56}, + {'1': 'HELTEC_WIRELESS_PAPER_V1_0', '2': 57}, + {'1': 'HELTEC_WIRELESS_TRACKER_V1_0', '2': 58}, + {'1': 'UNPHONE', '2': 59}, + {'1': 'TD_LORAC', '2': 60}, + {'1': 'CDEBYTE_EORA_S3', '2': 61}, + {'1': 'TWC_MESH_V4', '2': 62}, + {'1': 'NRF52_PROMICRO_DIY', '2': 63}, + {'1': 'RADIOMASTER_900_BANDIT_NANO', '2': 64}, + {'1': 'HELTEC_CAPSULE_SENSOR_V3', '2': 65}, + {'1': 'HELTEC_VISION_MASTER_T190', '2': 66}, + {'1': 'HELTEC_VISION_MASTER_E213', '2': 67}, + {'1': 'HELTEC_VISION_MASTER_E290', '2': 68}, + {'1': 'HELTEC_MESH_NODE_T114', '2': 69}, + {'1': 'SENSECAP_INDICATOR', '2': 70}, + {'1': 'TRACKER_T1000_E', '2': 71}, + {'1': 'RAK3172', '2': 72}, + {'1': 'WIO_E5', '2': 73}, + {'1': 'RADIOMASTER_900_BANDIT', '2': 74}, + {'1': 'ME25LS01_4Y10TD', '2': 75}, + {'1': 'RP2040_FEATHER_RFM95', '2': 76}, + {'1': 'M5STACK_COREBASIC', '2': 77}, + {'1': 'M5STACK_CORE2', '2': 78}, + {'1': 'RPI_PICO2', '2': 79}, + {'1': 'M5STACK_CORES3', '2': 80}, + {'1': 'SEEED_XIAO_S3', '2': 81}, + {'1': 'MS24SF1', '2': 82}, + {'1': 'TLORA_C6', '2': 83}, + {'1': 'WISMESH_TAP', '2': 84}, + {'1': 'ROUTASTIC', '2': 85}, + {'1': 'MESH_TAB', '2': 86}, + {'1': 'MESHLINK', '2': 87}, + {'1': 'XIAO_NRF52_KIT', '2': 88}, + {'1': 'THINKNODE_M1', '2': 89}, + {'1': 'THINKNODE_M2', '2': 90}, + {'1': 'T_ETH_ELITE', '2': 91}, + {'1': 'HELTEC_SENSOR_HUB', '2': 92}, + {'1': 'RESERVED_FRIED_CHICKEN', '2': 93}, + {'1': 'HELTEC_MESH_POCKET', '2': 94}, + {'1': 'SEEED_SOLAR_NODE', '2': 95}, + {'1': 'NOMADSTAR_METEOR_PRO', '2': 96}, + {'1': 'CROWPANEL', '2': 97}, + {'1': 'LINK_32', '2': 98}, + {'1': 'SEEED_WIO_TRACKER_L1', '2': 99}, + {'1': 'SEEED_WIO_TRACKER_L1_EINK', '2': 100}, + {'1': 'QWANTZ_TINY_ARMS', '2': 101}, + {'1': 'T_DECK_PRO', '2': 102}, + {'1': 'T_LORA_PAGER', '2': 103}, + {'1': 'GAT562_MESH_TRIAL_TRACKER', '2': 104}, + {'1': 'WISMESH_TAG', '2': 105}, + {'1': 'RAK3312', '2': 106}, + {'1': 'THINKNODE_M5', '2': 107}, + {'1': 'HELTEC_MESH_SOLAR', '2': 108}, + {'1': 'T_ECHO_LITE', '2': 109}, + {'1': 'PRIVATE_HW', '2': 255}, + ], +}; + +/// Descriptor for `HardwareModel`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List hardwareModelDescriptor = $convert.base64Decode( + 'Cg1IYXJkd2FyZU1vZGVsEgkKBVVOU0VUEAASDAoIVExPUkFfVjIQARIMCghUTE9SQV9WMRACEh' + 'IKDlRMT1JBX1YyXzFfMVA2EAMSCQoFVEJFQU0QBBIPCgtIRUxURUNfVjJfMBAFEg4KClRCRUFN' + 'X1YwUDcQBhIKCgZUX0VDSE8QBxIQCgxUTE9SQV9WMV8xUDMQCBILCgdSQUs0NjMxEAkSDwoLSE' + 'VMVEVDX1YyXzEQChINCglIRUxURUNfVjEQCxIYChRMSUxZR09fVEJFQU1fUzNfQ09SRRAMEgwK' + 'CFJBSzExMjAwEA0SCwoHTkFOT19HMRAOEhIKDlRMT1JBX1YyXzFfMVA4EA8SDwoLVExPUkFfVD' + 'NfUzMQEBIUChBOQU5PX0cxX0VYUExPUkVSEBESEQoNTkFOT19HMl9VTFRSQRASEg0KCUxPUkFf' + 'VFlQRRATEgsKB1dJUEhPTkUQFBIOCgpXSU9fV00xMTEwEBUSCwoHUkFLMjU2MBAWEhMKD0hFTF' + 'RFQ19IUlVfMzYwMRAXEhoKFkhFTFRFQ19XSVJFTEVTU19CUklER0UQGBIOCgpTVEFUSU9OX0cx' + 'EBkSDAoIUkFLMTEzMTAQGhIUChBTRU5TRUxPUkFfUlAyMDQwEBsSEAoMU0VOU0VMT1JBX1MzEB' + 'wSDQoJQ0FOQVJZT05FEB0SDwoLUlAyMDQwX0xPUkEQHhIOCgpTVEFUSU9OX0cyEB8SEQoNTE9S' + 'QV9SRUxBWV9WMRAgEg4KCk5SRjUyODQwREsQIRIHCgNQUFIQIhIPCgtHRU5JRUJMT0NLUxAjEh' + 'EKDU5SRjUyX1VOS05PV04QJBINCglQT1JURFVJTk8QJRIPCgtBTkRST0lEX1NJTRAmEgoKBkRJ' + 'WV9WMRAnEhUKEU5SRjUyODQwX1BDQTEwMDU5ECgSCgoGRFJfREVWECkSCwoHTTVTVEFDSxAqEg' + '0KCUhFTFRFQ19WMxArEhEKDUhFTFRFQ19XU0xfVjMQLBITCg9CRVRBRlBWXzI0MDBfVFgQLRIX' + 'ChNCRVRBRlBWXzkwMF9OQU5PX1RYEC4SDAoIUlBJX1BJQ08QLxIbChdIRUxURUNfV0lSRUxFU1' + 'NfVFJBQ0tFUhAwEhkKFUhFTFRFQ19XSVJFTEVTU19QQVBFUhAxEgoKBlRfREVDSxAyEg4KClRf' + 'V0FUQ0hfUzMQMxIRCg1QSUNPTVBVVEVSX1MzEDQSDwoLSEVMVEVDX0hUNjIQNRISCg5FQllURV' + '9FU1AzMl9TMxA2EhEKDUVTUDMyX1MzX1BJQ08QNxINCglDSEFUVEVSXzIQOBIeChpIRUxURUNf' + 'V0lSRUxFU1NfUEFQRVJfVjFfMBA5EiAKHEhFTFRFQ19XSVJFTEVTU19UUkFDS0VSX1YxXzAQOh' + 'ILCgdVTlBIT05FEDsSDAoIVERfTE9SQUMQPBITCg9DREVCWVRFX0VPUkFfUzMQPRIPCgtUV0Nf' + 'TUVTSF9WNBA+EhYKEk5SRjUyX1BST01JQ1JPX0RJWRA/Eh8KG1JBRElPTUFTVEVSXzkwMF9CQU' + '5ESVRfTkFOTxBAEhwKGEhFTFRFQ19DQVBTVUxFX1NFTlNPUl9WMxBBEh0KGUhFTFRFQ19WSVNJ' + 'T05fTUFTVEVSX1QxOTAQQhIdChlIRUxURUNfVklTSU9OX01BU1RFUl9FMjEzEEMSHQoZSEVMVE' + 'VDX1ZJU0lPTl9NQVNURVJfRTI5MBBEEhkKFUhFTFRFQ19NRVNIX05PREVfVDExNBBFEhYKElNF' + 'TlNFQ0FQX0lORElDQVRPUhBGEhMKD1RSQUNLRVJfVDEwMDBfRRBHEgsKB1JBSzMxNzIQSBIKCg' + 'ZXSU9fRTUQSRIaChZSQURJT01BU1RFUl85MDBfQkFORElUEEoSEwoPTUUyNUxTMDFfNFkxMFRE' + 'EEsSGAoUUlAyMDQwX0ZFQVRIRVJfUkZNOTUQTBIVChFNNVNUQUNLX0NPUkVCQVNJQxBNEhEKDU' + '01U1RBQ0tfQ09SRTIQThINCglSUElfUElDTzIQTxISCg5NNVNUQUNLX0NPUkVTMxBQEhEKDVNF' + 'RUVEX1hJQU9fUzMQURILCgdNUzI0U0YxEFISDAoIVExPUkFfQzYQUxIPCgtXSVNNRVNIX1RBUB' + 'BUEg0KCVJPVVRBU1RJQxBVEgwKCE1FU0hfVEFCEFYSDAoITUVTSExJTksQVxISCg5YSUFPX05S' + 'RjUyX0tJVBBYEhAKDFRISU5LTk9ERV9NMRBZEhAKDFRISU5LTk9ERV9NMhBaEg8KC1RfRVRIX0' + 'VMSVRFEFsSFQoRSEVMVEVDX1NFTlNPUl9IVUIQXBIaChZSRVNFUlZFRF9GUklFRF9DSElDS0VO' + 'EF0SFgoSSEVMVEVDX01FU0hfUE9DS0VUEF4SFAoQU0VFRURfU09MQVJfTk9ERRBfEhgKFE5PTU' + 'FEU1RBUl9NRVRFT1JfUFJPEGASDQoJQ1JPV1BBTkVMEGESCwoHTElOS18zMhBiEhgKFFNFRUVE' + 'X1dJT19UUkFDS0VSX0wxEGMSHQoZU0VFRURfV0lPX1RSQUNLRVJfTDFfRUlOSxBkEhQKEFFXQU' + '5UWl9USU5ZX0FSTVMQZRIOCgpUX0RFQ0tfUFJPEGYSEAoMVF9MT1JBX1BBR0VSEGcSHQoZR0FU' + 'NTYyX01FU0hfVFJJQUxfVFJBQ0tFUhBoEg8KC1dJU01FU0hfVEFHEGkSCwoHUkFLMzMxMhBqEh' + 'AKDFRISU5LTk9ERV9NNRBrEhUKEUhFTFRFQ19NRVNIX1NPTEFSEGwSDwoLVF9FQ0hPX0xJVEUQ' + 'bRIPCgpQUklWQVRFX0hXEP8B'); + +@$core.Deprecated('Use constantsDescriptor instead') +const Constants$json = { + '1': 'Constants', + '2': [ + {'1': 'ZERO', '2': 0}, + {'1': 'DATA_PAYLOAD_LEN', '2': 233}, + ], +}; + +/// Descriptor for `Constants`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List constantsDescriptor = $convert.base64Decode( + 'CglDb25zdGFudHMSCAoEWkVSTxAAEhUKEERBVEFfUEFZTE9BRF9MRU4Q6QE='); + +@$core.Deprecated('Use criticalErrorCodeDescriptor instead') +const CriticalErrorCode$json = { + '1': 'CriticalErrorCode', + '2': [ + {'1': 'NONE', '2': 0}, + {'1': 'TX_WATCHDOG', '2': 1}, + {'1': 'SLEEP_ENTER_WAIT', '2': 2}, + {'1': 'NO_RADIO', '2': 3}, + {'1': 'UNSPECIFIED', '2': 4}, + {'1': 'UBLOX_UNIT_FAILED', '2': 5}, + {'1': 'NO_AXP192', '2': 6}, + {'1': 'INVALID_RADIO_SETTING', '2': 7}, + {'1': 'TRANSMIT_FAILED', '2': 8}, + {'1': 'BROWNOUT', '2': 9}, + {'1': 'SX1262_FAILURE', '2': 10}, + {'1': 'RADIO_SPI_BUG', '2': 11}, + {'1': 'FLASH_CORRUPTION_RECOVERABLE', '2': 12}, + {'1': 'FLASH_CORRUPTION_UNRECOVERABLE', '2': 13}, + ], +}; + +/// Descriptor for `CriticalErrorCode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List criticalErrorCodeDescriptor = $convert.base64Decode( + 'ChFDcml0aWNhbEVycm9yQ29kZRIICgROT05FEAASDwoLVFhfV0FUQ0hET0cQARIUChBTTEVFUF' + '9FTlRFUl9XQUlUEAISDAoITk9fUkFESU8QAxIPCgtVTlNQRUNJRklFRBAEEhUKEVVCTE9YX1VO' + 'SVRfRkFJTEVEEAUSDQoJTk9fQVhQMTkyEAYSGQoVSU5WQUxJRF9SQURJT19TRVRUSU5HEAcSEw' + 'oPVFJBTlNNSVRfRkFJTEVEEAgSDAoIQlJPV05PVVQQCRISCg5TWDEyNjJfRkFJTFVSRRAKEhEK' + 'DVJBRElPX1NQSV9CVUcQCxIgChxGTEFTSF9DT1JSVVBUSU9OX1JFQ09WRVJBQkxFEAwSIgoeRk' + 'xBU0hfQ09SUlVQVElPTl9VTlJFQ09WRVJBQkxFEA0='); + +@$core.Deprecated('Use firmwareEditionDescriptor instead') +const FirmwareEdition$json = { + '1': 'FirmwareEdition', + '2': [ + {'1': 'VANILLA', '2': 0}, + {'1': 'SMART_CITIZEN', '2': 1}, + {'1': 'OPEN_SAUCE', '2': 16}, + {'1': 'DEFCON', '2': 17}, + {'1': 'BURNING_MAN', '2': 18}, + {'1': 'HAMVENTION', '2': 19}, + {'1': 'DIY_EDITION', '2': 127}, + ], +}; + +/// Descriptor for `FirmwareEdition`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List firmwareEditionDescriptor = $convert.base64Decode( + 'Cg9GaXJtd2FyZUVkaXRpb24SCwoHVkFOSUxMQRAAEhEKDVNNQVJUX0NJVElaRU4QARIOCgpPUE' + 'VOX1NBVUNFEBASCgoGREVGQ09OEBESDwoLQlVSTklOR19NQU4QEhIOCgpIQU1WRU5USU9OEBMS' + 'DwoLRElZX0VESVRJT04Qfw=='); + +@$core.Deprecated('Use excludedModulesDescriptor instead') +const ExcludedModules$json = { + '1': 'ExcludedModules', + '2': [ + {'1': 'EXCLUDED_NONE', '2': 0}, + {'1': 'MQTT_CONFIG', '2': 1}, + {'1': 'SERIAL_CONFIG', '2': 2}, + {'1': 'EXTNOTIF_CONFIG', '2': 4}, + {'1': 'STOREFORWARD_CONFIG', '2': 8}, + {'1': 'RANGETEST_CONFIG', '2': 16}, + {'1': 'TELEMETRY_CONFIG', '2': 32}, + {'1': 'CANNEDMSG_CONFIG', '2': 64}, + {'1': 'AUDIO_CONFIG', '2': 128}, + {'1': 'REMOTEHARDWARE_CONFIG', '2': 256}, + {'1': 'NEIGHBORINFO_CONFIG', '2': 512}, + {'1': 'AMBIENTLIGHTING_CONFIG', '2': 1024}, + {'1': 'DETECTIONSENSOR_CONFIG', '2': 2048}, + {'1': 'PAXCOUNTER_CONFIG', '2': 4096}, + {'1': 'BLUETOOTH_CONFIG', '2': 8192}, + {'1': 'NETWORK_CONFIG', '2': 16384}, + ], +}; + +/// Descriptor for `ExcludedModules`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List excludedModulesDescriptor = $convert.base64Decode( + 'Cg9FeGNsdWRlZE1vZHVsZXMSEQoNRVhDTFVERURfTk9ORRAAEg8KC01RVFRfQ09ORklHEAESEQ' + 'oNU0VSSUFMX0NPTkZJRxACEhMKD0VYVE5PVElGX0NPTkZJRxAEEhcKE1NUT1JFRk9SV0FSRF9D' + 'T05GSUcQCBIUChBSQU5HRVRFU1RfQ09ORklHEBASFAoQVEVMRU1FVFJZX0NPTkZJRxAgEhQKEE' + 'NBTk5FRE1TR19DT05GSUcQQBIRCgxBVURJT19DT05GSUcQgAESGgoVUkVNT1RFSEFSRFdBUkVf' + 'Q09ORklHEIACEhgKE05FSUdIQk9SSU5GT19DT05GSUcQgAQSGwoWQU1CSUVOVExJR0hUSU5HX0' + 'NPTkZJRxCACBIbChZERVRFQ1RJT05TRU5TT1JfQ09ORklHEIAQEhYKEVBBWENPVU5URVJfQ09O' + 'RklHEIAgEhUKEEJMVUVUT09USF9DT05GSUcQgEASFAoOTkVUV09SS19DT05GSUcQgIAB'); + +@$core.Deprecated('Use positionDescriptor instead') +const Position$json = { + '1': 'Position', + '2': [ + { + '1': 'latitude_i', + '3': 1, + '4': 1, + '5': 15, + '9': 0, + '10': 'latitudeI', + '17': true + }, + { + '1': 'longitude_i', + '3': 2, + '4': 1, + '5': 15, + '9': 1, + '10': 'longitudeI', + '17': true + }, + { + '1': 'altitude', + '3': 3, + '4': 1, + '5': 5, + '9': 2, + '10': 'altitude', + '17': true + }, + {'1': 'time', '3': 4, '4': 1, '5': 7, '10': 'time'}, + { + '1': 'location_source', + '3': 5, + '4': 1, + '5': 14, + '6': '.meshtastic.Position.LocSource', + '10': 'locationSource' + }, + { + '1': 'altitude_source', + '3': 6, + '4': 1, + '5': 14, + '6': '.meshtastic.Position.AltSource', + '10': 'altitudeSource' + }, + {'1': 'timestamp', '3': 7, '4': 1, '5': 7, '10': 'timestamp'}, + { + '1': 'timestamp_millis_adjust', + '3': 8, + '4': 1, + '5': 5, + '10': 'timestampMillisAdjust' + }, + { + '1': 'altitude_hae', + '3': 9, + '4': 1, + '5': 17, + '9': 3, + '10': 'altitudeHae', + '17': true + }, + { + '1': 'altitude_geoidal_separation', + '3': 10, + '4': 1, + '5': 17, + '9': 4, + '10': 'altitudeGeoidalSeparation', + '17': true + }, + {'1': 'PDOP', '3': 11, '4': 1, '5': 13, '10': 'PDOP'}, + {'1': 'HDOP', '3': 12, '4': 1, '5': 13, '10': 'HDOP'}, + {'1': 'VDOP', '3': 13, '4': 1, '5': 13, '10': 'VDOP'}, + {'1': 'gps_accuracy', '3': 14, '4': 1, '5': 13, '10': 'gpsAccuracy'}, + { + '1': 'ground_speed', + '3': 15, + '4': 1, + '5': 13, + '9': 5, + '10': 'groundSpeed', + '17': true + }, + { + '1': 'ground_track', + '3': 16, + '4': 1, + '5': 13, + '9': 6, + '10': 'groundTrack', + '17': true + }, + {'1': 'fix_quality', '3': 17, '4': 1, '5': 13, '10': 'fixQuality'}, + {'1': 'fix_type', '3': 18, '4': 1, '5': 13, '10': 'fixType'}, + {'1': 'sats_in_view', '3': 19, '4': 1, '5': 13, '10': 'satsInView'}, + {'1': 'sensor_id', '3': 20, '4': 1, '5': 13, '10': 'sensorId'}, + {'1': 'next_update', '3': 21, '4': 1, '5': 13, '10': 'nextUpdate'}, + {'1': 'seq_number', '3': 22, '4': 1, '5': 13, '10': 'seqNumber'}, + {'1': 'precision_bits', '3': 23, '4': 1, '5': 13, '10': 'precisionBits'}, + ], + '4': [Position_LocSource$json, Position_AltSource$json], + '8': [ + {'1': '_latitude_i'}, + {'1': '_longitude_i'}, + {'1': '_altitude'}, + {'1': '_altitude_hae'}, + {'1': '_altitude_geoidal_separation'}, + {'1': '_ground_speed'}, + {'1': '_ground_track'}, + ], +}; + +@$core.Deprecated('Use positionDescriptor instead') +const Position_LocSource$json = { + '1': 'LocSource', + '2': [ + {'1': 'LOC_UNSET', '2': 0}, + {'1': 'LOC_MANUAL', '2': 1}, + {'1': 'LOC_INTERNAL', '2': 2}, + {'1': 'LOC_EXTERNAL', '2': 3}, + ], +}; + +@$core.Deprecated('Use positionDescriptor instead') +const Position_AltSource$json = { + '1': 'AltSource', + '2': [ + {'1': 'ALT_UNSET', '2': 0}, + {'1': 'ALT_MANUAL', '2': 1}, + {'1': 'ALT_INTERNAL', '2': 2}, + {'1': 'ALT_EXTERNAL', '2': 3}, + {'1': 'ALT_BAROMETRIC', '2': 4}, + ], +}; + +/// Descriptor for `Position`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List positionDescriptor = $convert.base64Decode( + 'CghQb3NpdGlvbhIiCgpsYXRpdHVkZV9pGAEgASgPSABSCWxhdGl0dWRlSYgBARIkCgtsb25naX' + 'R1ZGVfaRgCIAEoD0gBUgpsb25naXR1ZGVJiAEBEh8KCGFsdGl0dWRlGAMgASgFSAJSCGFsdGl0' + 'dWRliAEBEhIKBHRpbWUYBCABKAdSBHRpbWUSRwoPbG9jYXRpb25fc291cmNlGAUgASgOMh4ubW' + 'VzaHRhc3RpYy5Qb3NpdGlvbi5Mb2NTb3VyY2VSDmxvY2F0aW9uU291cmNlEkcKD2FsdGl0dWRl' + 'X3NvdXJjZRgGIAEoDjIeLm1lc2h0YXN0aWMuUG9zaXRpb24uQWx0U291cmNlUg5hbHRpdHVkZV' + 'NvdXJjZRIcCgl0aW1lc3RhbXAYByABKAdSCXRpbWVzdGFtcBI2Chd0aW1lc3RhbXBfbWlsbGlz' + 'X2FkanVzdBgIIAEoBVIVdGltZXN0YW1wTWlsbGlzQWRqdXN0EiYKDGFsdGl0dWRlX2hhZRgJIA' + 'EoEUgDUgthbHRpdHVkZUhhZYgBARJDChthbHRpdHVkZV9nZW9pZGFsX3NlcGFyYXRpb24YCiAB' + 'KBFIBFIZYWx0aXR1ZGVHZW9pZGFsU2VwYXJhdGlvbogBARISCgRQRE9QGAsgASgNUgRQRE9QEh' + 'IKBEhET1AYDCABKA1SBEhET1ASEgoEVkRPUBgNIAEoDVIEVkRPUBIhCgxncHNfYWNjdXJhY3kY' + 'DiABKA1SC2dwc0FjY3VyYWN5EiYKDGdyb3VuZF9zcGVlZBgPIAEoDUgFUgtncm91bmRTcGVlZI' + 'gBARImCgxncm91bmRfdHJhY2sYECABKA1IBlILZ3JvdW5kVHJhY2uIAQESHwoLZml4X3F1YWxp' + 'dHkYESABKA1SCmZpeFF1YWxpdHkSGQoIZml4X3R5cGUYEiABKA1SB2ZpeFR5cGUSIAoMc2F0c1' + '9pbl92aWV3GBMgASgNUgpzYXRzSW5WaWV3EhsKCXNlbnNvcl9pZBgUIAEoDVIIc2Vuc29ySWQS' + 'HwoLbmV4dF91cGRhdGUYFSABKA1SCm5leHRVcGRhdGUSHQoKc2VxX251bWJlchgWIAEoDVIJc2' + 'VxTnVtYmVyEiUKDnByZWNpc2lvbl9iaXRzGBcgASgNUg1wcmVjaXNpb25CaXRzIk4KCUxvY1Nv' + 'dXJjZRINCglMT0NfVU5TRVQQABIOCgpMT0NfTUFOVUFMEAESEAoMTE9DX0lOVEVSTkFMEAISEA' + 'oMTE9DX0VYVEVSTkFMEAMiYgoJQWx0U291cmNlEg0KCUFMVF9VTlNFVBAAEg4KCkFMVF9NQU5V' + 'QUwQARIQCgxBTFRfSU5URVJOQUwQAhIQCgxBTFRfRVhURVJOQUwQAxISCg5BTFRfQkFST01FVF' + 'JJQxAEQg0KC19sYXRpdHVkZV9pQg4KDF9sb25naXR1ZGVfaUILCglfYWx0aXR1ZGVCDwoNX2Fs' + 'dGl0dWRlX2hhZUIeChxfYWx0aXR1ZGVfZ2VvaWRhbF9zZXBhcmF0aW9uQg8KDV9ncm91bmRfc3' + 'BlZWRCDwoNX2dyb3VuZF90cmFjaw=='); + +@$core.Deprecated('Use userDescriptor instead') +const User$json = { + '1': 'User', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'}, + {'1': 'long_name', '3': 2, '4': 1, '5': 9, '10': 'longName'}, + {'1': 'short_name', '3': 3, '4': 1, '5': 9, '10': 'shortName'}, + { + '1': 'macaddr', + '3': 4, + '4': 1, + '5': 12, + '8': {'3': true}, + '10': 'macaddr', + }, + { + '1': 'hw_model', + '3': 5, + '4': 1, + '5': 14, + '6': '.meshtastic.HardwareModel', + '10': 'hwModel' + }, + {'1': 'is_licensed', '3': 6, '4': 1, '5': 8, '10': 'isLicensed'}, + { + '1': 'role', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.Role', + '10': 'role' + }, + {'1': 'public_key', '3': 8, '4': 1, '5': 12, '10': 'publicKey'}, + { + '1': 'is_unmessagable', + '3': 9, + '4': 1, + '5': 8, + '9': 0, + '10': 'isUnmessagable', + '17': true + }, + ], + '8': [ + {'1': '_is_unmessagable'}, + ], +}; + +/// Descriptor for `User`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List userDescriptor = $convert.base64Decode( + 'CgRVc2VyEg4KAmlkGAEgASgJUgJpZBIbCglsb25nX25hbWUYAiABKAlSCGxvbmdOYW1lEh0KCn' + 'Nob3J0X25hbWUYAyABKAlSCXNob3J0TmFtZRIcCgdtYWNhZGRyGAQgASgMQgIYAVIHbWFjYWRk' + 'chI0Cghod19tb2RlbBgFIAEoDjIZLm1lc2h0YXN0aWMuSGFyZHdhcmVNb2RlbFIHaHdNb2RlbB' + 'IfCgtpc19saWNlbnNlZBgGIAEoCFIKaXNMaWNlbnNlZBI4CgRyb2xlGAcgASgOMiQubWVzaHRh' + 'c3RpYy5Db25maWcuRGV2aWNlQ29uZmlnLlJvbGVSBHJvbGUSHQoKcHVibGljX2tleRgIIAEoDF' + 'IJcHVibGljS2V5EiwKD2lzX3VubWVzc2FnYWJsZRgJIAEoCEgAUg5pc1VubWVzc2FnYWJsZYgB' + 'AUISChBfaXNfdW5tZXNzYWdhYmxl'); + +@$core.Deprecated('Use routeDiscoveryDescriptor instead') +const RouteDiscovery$json = { + '1': 'RouteDiscovery', + '2': [ + {'1': 'route', '3': 1, '4': 3, '5': 7, '10': 'route'}, + {'1': 'snr_towards', '3': 2, '4': 3, '5': 5, '10': 'snrTowards'}, + {'1': 'route_back', '3': 3, '4': 3, '5': 7, '10': 'routeBack'}, + {'1': 'snr_back', '3': 4, '4': 3, '5': 5, '10': 'snrBack'}, + ], +}; + +/// Descriptor for `RouteDiscovery`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List routeDiscoveryDescriptor = $convert.base64Decode( + 'Cg5Sb3V0ZURpc2NvdmVyeRIUCgVyb3V0ZRgBIAMoB1IFcm91dGUSHwoLc25yX3Rvd2FyZHMYAi' + 'ADKAVSCnNuclRvd2FyZHMSHQoKcm91dGVfYmFjaxgDIAMoB1IJcm91dGVCYWNrEhkKCHNucl9i' + 'YWNrGAQgAygFUgdzbnJCYWNr'); + +@$core.Deprecated('Use routingDescriptor instead') +const Routing$json = { + '1': 'Routing', + '2': [ + { + '1': 'route_request', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.RouteDiscovery', + '9': 0, + '10': 'routeRequest' + }, + { + '1': 'route_reply', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.RouteDiscovery', + '9': 0, + '10': 'routeReply' + }, + { + '1': 'error_reason', + '3': 3, + '4': 1, + '5': 14, + '6': '.meshtastic.Routing.Error', + '9': 0, + '10': 'errorReason' + }, + ], + '4': [Routing_Error$json], + '8': [ + {'1': 'variant'}, + ], +}; + +@$core.Deprecated('Use routingDescriptor instead') +const Routing_Error$json = { + '1': 'Error', + '2': [ + {'1': 'NONE', '2': 0}, + {'1': 'NO_ROUTE', '2': 1}, + {'1': 'GOT_NAK', '2': 2}, + {'1': 'TIMEOUT', '2': 3}, + {'1': 'NO_INTERFACE', '2': 4}, + {'1': 'MAX_RETRANSMIT', '2': 5}, + {'1': 'NO_CHANNEL', '2': 6}, + {'1': 'TOO_LARGE', '2': 7}, + {'1': 'NO_RESPONSE', '2': 8}, + {'1': 'DUTY_CYCLE_LIMIT', '2': 9}, + {'1': 'BAD_REQUEST', '2': 32}, + {'1': 'NOT_AUTHORIZED', '2': 33}, + {'1': 'PKI_FAILED', '2': 34}, + {'1': 'PKI_UNKNOWN_PUBKEY', '2': 35}, + {'1': 'ADMIN_BAD_SESSION_KEY', '2': 36}, + {'1': 'ADMIN_PUBLIC_KEY_UNAUTHORIZED', '2': 37}, + {'1': 'RATE_LIMIT_EXCEEDED', '2': 38}, + ], +}; + +/// Descriptor for `Routing`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List routingDescriptor = $convert.base64Decode( + 'CgdSb3V0aW5nEkEKDXJvdXRlX3JlcXVlc3QYASABKAsyGi5tZXNodGFzdGljLlJvdXRlRGlzY2' + '92ZXJ5SABSDHJvdXRlUmVxdWVzdBI9Cgtyb3V0ZV9yZXBseRgCIAEoCzIaLm1lc2h0YXN0aWMu' + 'Um91dGVEaXNjb3ZlcnlIAFIKcm91dGVSZXBseRI+CgxlcnJvcl9yZWFzb24YAyABKA4yGS5tZX' + 'NodGFzdGljLlJvdXRpbmcuRXJyb3JIAFILZXJyb3JSZWFzb24iyQIKBUVycm9yEggKBE5PTkUQ' + 'ABIMCghOT19ST1VURRABEgsKB0dPVF9OQUsQAhILCgdUSU1FT1VUEAMSEAoMTk9fSU5URVJGQU' + 'NFEAQSEgoOTUFYX1JFVFJBTlNNSVQQBRIOCgpOT19DSEFOTkVMEAYSDQoJVE9PX0xBUkdFEAcS' + 'DwoLTk9fUkVTUE9OU0UQCBIUChBEVVRZX0NZQ0xFX0xJTUlUEAkSDwoLQkFEX1JFUVVFU1QQIB' + 'ISCg5OT1RfQVVUSE9SSVpFRBAhEg4KClBLSV9GQUlMRUQQIhIWChJQS0lfVU5LTk9XTl9QVUJL' + 'RVkQIxIZChVBRE1JTl9CQURfU0VTU0lPTl9LRVkQJBIhCh1BRE1JTl9QVUJMSUNfS0VZX1VOQV' + 'VUSE9SSVpFRBAlEhcKE1JBVEVfTElNSVRfRVhDRUVERUQQJkIJCgd2YXJpYW50'); + +@$core.Deprecated('Use dataDescriptor instead') +const Data$json = { + '1': 'Data', + '2': [ + { + '1': 'portnum', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.PortNum', + '10': 'portnum' + }, + {'1': 'payload', '3': 2, '4': 1, '5': 12, '10': 'payload'}, + {'1': 'want_response', '3': 3, '4': 1, '5': 8, '10': 'wantResponse'}, + {'1': 'dest', '3': 4, '4': 1, '5': 7, '10': 'dest'}, + {'1': 'source', '3': 5, '4': 1, '5': 7, '10': 'source'}, + {'1': 'request_id', '3': 6, '4': 1, '5': 7, '10': 'requestId'}, + {'1': 'reply_id', '3': 7, '4': 1, '5': 7, '10': 'replyId'}, + {'1': 'emoji', '3': 8, '4': 1, '5': 7, '10': 'emoji'}, + { + '1': 'bitfield', + '3': 9, + '4': 1, + '5': 13, + '9': 0, + '10': 'bitfield', + '17': true + }, + ], + '8': [ + {'1': '_bitfield'}, + ], +}; + +/// Descriptor for `Data`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dataDescriptor = $convert.base64Decode( + 'CgREYXRhEi0KB3BvcnRudW0YASABKA4yEy5tZXNodGFzdGljLlBvcnROdW1SB3BvcnRudW0SGA' + 'oHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIjCg13YW50X3Jlc3BvbnNlGAMgASgIUgx3YW50UmVz' + 'cG9uc2USEgoEZGVzdBgEIAEoB1IEZGVzdBIWCgZzb3VyY2UYBSABKAdSBnNvdXJjZRIdCgpyZX' + 'F1ZXN0X2lkGAYgASgHUglyZXF1ZXN0SWQSGQoIcmVwbHlfaWQYByABKAdSB3JlcGx5SWQSFAoF' + 'ZW1vamkYCCABKAdSBWVtb2ppEh8KCGJpdGZpZWxkGAkgASgNSABSCGJpdGZpZWxkiAEBQgsKCV' + '9iaXRmaWVsZA=='); + +@$core.Deprecated('Use keyVerificationDescriptor instead') +const KeyVerification$json = { + '1': 'KeyVerification', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 4, '10': 'nonce'}, + {'1': 'hash1', '3': 2, '4': 1, '5': 12, '10': 'hash1'}, + {'1': 'hash2', '3': 3, '4': 1, '5': 12, '10': 'hash2'}, + ], +}; + +/// Descriptor for `KeyVerification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List keyVerificationDescriptor = $convert.base64Decode( + 'Cg9LZXlWZXJpZmljYXRpb24SFAoFbm9uY2UYASABKARSBW5vbmNlEhQKBWhhc2gxGAIgASgMUg' + 'VoYXNoMRIUCgVoYXNoMhgDIAEoDFIFaGFzaDI='); + +@$core.Deprecated('Use waypointDescriptor instead') +const Waypoint$json = { + '1': 'Waypoint', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 13, '10': 'id'}, + { + '1': 'latitude_i', + '3': 2, + '4': 1, + '5': 15, + '9': 0, + '10': 'latitudeI', + '17': true + }, + { + '1': 'longitude_i', + '3': 3, + '4': 1, + '5': 15, + '9': 1, + '10': 'longitudeI', + '17': true + }, + {'1': 'expire', '3': 4, '4': 1, '5': 13, '10': 'expire'}, + {'1': 'locked_to', '3': 5, '4': 1, '5': 13, '10': 'lockedTo'}, + {'1': 'name', '3': 6, '4': 1, '5': 9, '10': 'name'}, + {'1': 'description', '3': 7, '4': 1, '5': 9, '10': 'description'}, + {'1': 'icon', '3': 8, '4': 1, '5': 7, '10': 'icon'}, + ], + '8': [ + {'1': '_latitude_i'}, + {'1': '_longitude_i'}, + ], +}; + +/// Descriptor for `Waypoint`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List waypointDescriptor = $convert.base64Decode( + 'CghXYXlwb2ludBIOCgJpZBgBIAEoDVICaWQSIgoKbGF0aXR1ZGVfaRgCIAEoD0gAUglsYXRpdH' + 'VkZUmIAQESJAoLbG9uZ2l0dWRlX2kYAyABKA9IAVIKbG9uZ2l0dWRlSYgBARIWCgZleHBpcmUY' + 'BCABKA1SBmV4cGlyZRIbCglsb2NrZWRfdG8YBSABKA1SCGxvY2tlZFRvEhIKBG5hbWUYBiABKA' + 'lSBG5hbWUSIAoLZGVzY3JpcHRpb24YByABKAlSC2Rlc2NyaXB0aW9uEhIKBGljb24YCCABKAdS' + 'BGljb25CDQoLX2xhdGl0dWRlX2lCDgoMX2xvbmdpdHVkZV9p'); + +@$core.Deprecated('Use mqttClientProxyMessageDescriptor instead') +const MqttClientProxyMessage$json = { + '1': 'MqttClientProxyMessage', + '2': [ + {'1': 'topic', '3': 1, '4': 1, '5': 9, '10': 'topic'}, + {'1': 'data', '3': 2, '4': 1, '5': 12, '9': 0, '10': 'data'}, + {'1': 'text', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'text'}, + {'1': 'retained', '3': 4, '4': 1, '5': 8, '10': 'retained'}, + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +/// Descriptor for `MqttClientProxyMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List mqttClientProxyMessageDescriptor = $convert.base64Decode( + 'ChZNcXR0Q2xpZW50UHJveHlNZXNzYWdlEhQKBXRvcGljGAEgASgJUgV0b3BpYxIUCgRkYXRhGA' + 'IgASgMSABSBGRhdGESFAoEdGV4dBgDIAEoCUgAUgR0ZXh0EhoKCHJldGFpbmVkGAQgASgIUghy' + 'ZXRhaW5lZEIRCg9wYXlsb2FkX3ZhcmlhbnQ='); + +@$core.Deprecated('Use meshPacketDescriptor instead') +const MeshPacket$json = { + '1': 'MeshPacket', + '2': [ + {'1': 'from', '3': 1, '4': 1, '5': 7, '10': 'from'}, + {'1': 'to', '3': 2, '4': 1, '5': 7, '10': 'to'}, + {'1': 'channel', '3': 3, '4': 1, '5': 13, '10': 'channel'}, + { + '1': 'decoded', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.Data', + '9': 0, + '10': 'decoded' + }, + {'1': 'encrypted', '3': 5, '4': 1, '5': 12, '9': 0, '10': 'encrypted'}, + {'1': 'id', '3': 6, '4': 1, '5': 7, '10': 'id'}, + {'1': 'rx_time', '3': 7, '4': 1, '5': 7, '10': 'rxTime'}, + {'1': 'rx_snr', '3': 8, '4': 1, '5': 2, '10': 'rxSnr'}, + {'1': 'hop_limit', '3': 9, '4': 1, '5': 13, '10': 'hopLimit'}, + {'1': 'want_ack', '3': 10, '4': 1, '5': 8, '10': 'wantAck'}, + { + '1': 'priority', + '3': 11, + '4': 1, + '5': 14, + '6': '.meshtastic.MeshPacket.Priority', + '10': 'priority' + }, + {'1': 'rx_rssi', '3': 12, '4': 1, '5': 5, '10': 'rxRssi'}, + { + '1': 'delayed', + '3': 13, + '4': 1, + '5': 14, + '6': '.meshtastic.MeshPacket.Delayed', + '8': {'3': true}, + '10': 'delayed', + }, + {'1': 'via_mqtt', '3': 14, '4': 1, '5': 8, '10': 'viaMqtt'}, + {'1': 'hop_start', '3': 15, '4': 1, '5': 13, '10': 'hopStart'}, + {'1': 'public_key', '3': 16, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'pki_encrypted', '3': 17, '4': 1, '5': 8, '10': 'pkiEncrypted'}, + {'1': 'next_hop', '3': 18, '4': 1, '5': 13, '10': 'nextHop'}, + {'1': 'relay_node', '3': 19, '4': 1, '5': 13, '10': 'relayNode'}, + {'1': 'tx_after', '3': 20, '4': 1, '5': 13, '10': 'txAfter'}, + { + '1': 'transport_mechanism', + '3': 21, + '4': 1, + '5': 14, + '6': '.meshtastic.MeshPacket.TransportMechanism', + '10': 'transportMechanism' + }, + ], + '4': [ + MeshPacket_Priority$json, + MeshPacket_Delayed$json, + MeshPacket_TransportMechanism$json + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +@$core.Deprecated('Use meshPacketDescriptor instead') +const MeshPacket_Priority$json = { + '1': 'Priority', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'MIN', '2': 1}, + {'1': 'BACKGROUND', '2': 10}, + {'1': 'DEFAULT', '2': 64}, + {'1': 'RELIABLE', '2': 70}, + {'1': 'RESPONSE', '2': 80}, + {'1': 'HIGH', '2': 100}, + {'1': 'ALERT', '2': 110}, + {'1': 'ACK', '2': 120}, + {'1': 'MAX', '2': 127}, + ], +}; + +@$core.Deprecated('Use meshPacketDescriptor instead') +const MeshPacket_Delayed$json = { + '1': 'Delayed', + '2': [ + {'1': 'NO_DELAY', '2': 0}, + {'1': 'DELAYED_BROADCAST', '2': 1}, + {'1': 'DELAYED_DIRECT', '2': 2}, + ], +}; + +@$core.Deprecated('Use meshPacketDescriptor instead') +const MeshPacket_TransportMechanism$json = { + '1': 'TransportMechanism', + '2': [ + {'1': 'TRANSPORT_INTERNAL', '2': 0}, + {'1': 'TRANSPORT_LORA', '2': 1}, + {'1': 'TRANSPORT_LORA_ALT1', '2': 2}, + {'1': 'TRANSPORT_LORA_ALT2', '2': 3}, + {'1': 'TRANSPORT_LORA_ALT3', '2': 4}, + {'1': 'TRANSPORT_MQTT', '2': 5}, + {'1': 'TRANSPORT_MULTICAST_UDP', '2': 6}, + {'1': 'TRANSPORT_API', '2': 7}, + ], +}; + +/// Descriptor for `MeshPacket`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List meshPacketDescriptor = $convert.base64Decode( + 'CgpNZXNoUGFja2V0EhIKBGZyb20YASABKAdSBGZyb20SDgoCdG8YAiABKAdSAnRvEhgKB2NoYW' + '5uZWwYAyABKA1SB2NoYW5uZWwSLAoHZGVjb2RlZBgEIAEoCzIQLm1lc2h0YXN0aWMuRGF0YUgA' + 'UgdkZWNvZGVkEh4KCWVuY3J5cHRlZBgFIAEoDEgAUgllbmNyeXB0ZWQSDgoCaWQYBiABKAdSAm' + 'lkEhcKB3J4X3RpbWUYByABKAdSBnJ4VGltZRIVCgZyeF9zbnIYCCABKAJSBXJ4U25yEhsKCWhv' + 'cF9saW1pdBgJIAEoDVIIaG9wTGltaXQSGQoId2FudF9hY2sYCiABKAhSB3dhbnRBY2sSOwoIcH' + 'Jpb3JpdHkYCyABKA4yHy5tZXNodGFzdGljLk1lc2hQYWNrZXQuUHJpb3JpdHlSCHByaW9yaXR5' + 'EhcKB3J4X3Jzc2kYDCABKAVSBnJ4UnNzaRI8CgdkZWxheWVkGA0gASgOMh4ubWVzaHRhc3RpYy' + '5NZXNoUGFja2V0LkRlbGF5ZWRCAhgBUgdkZWxheWVkEhkKCHZpYV9tcXR0GA4gASgIUgd2aWFN' + 'cXR0EhsKCWhvcF9zdGFydBgPIAEoDVIIaG9wU3RhcnQSHQoKcHVibGljX2tleRgQIAEoDFIJcH' + 'VibGljS2V5EiMKDXBraV9lbmNyeXB0ZWQYESABKAhSDHBraUVuY3J5cHRlZBIZCghuZXh0X2hv' + 'cBgSIAEoDVIHbmV4dEhvcBIdCgpyZWxheV9ub2RlGBMgASgNUglyZWxheU5vZGUSGQoIdHhfYW' + 'Z0ZXIYFCABKA1SB3R4QWZ0ZXISWgoTdHJhbnNwb3J0X21lY2hhbmlzbRgVIAEoDjIpLm1lc2h0' + 'YXN0aWMuTWVzaFBhY2tldC5UcmFuc3BvcnRNZWNoYW5pc21SEnRyYW5zcG9ydE1lY2hhbmlzbS' + 'J+CghQcmlvcml0eRIJCgVVTlNFVBAAEgcKA01JThABEg4KCkJBQ0tHUk9VTkQQChILCgdERUZB' + 'VUxUEEASDAoIUkVMSUFCTEUQRhIMCghSRVNQT05TRRBQEggKBEhJR0gQZBIJCgVBTEVSVBBuEg' + 'cKA0FDSxB4EgcKA01BWBB/IkIKB0RlbGF5ZWQSDAoITk9fREVMQVkQABIVChFERUxBWUVEX0JS' + 'T0FEQ0FTVBABEhIKDkRFTEFZRURfRElSRUNUEAIizwEKElRyYW5zcG9ydE1lY2hhbmlzbRIWCh' + 'JUUkFOU1BPUlRfSU5URVJOQUwQABISCg5UUkFOU1BPUlRfTE9SQRABEhcKE1RSQU5TUE9SVF9M' + 'T1JBX0FMVDEQAhIXChNUUkFOU1BPUlRfTE9SQV9BTFQyEAMSFwoTVFJBTlNQT1JUX0xPUkFfQU' + 'xUMxAEEhIKDlRSQU5TUE9SVF9NUVRUEAUSGwoXVFJBTlNQT1JUX01VTFRJQ0FTVF9VRFAQBhIR' + 'Cg1UUkFOU1BPUlRfQVBJEAdCEQoPcGF5bG9hZF92YXJpYW50'); + +@$core.Deprecated('Use nodeInfoDescriptor instead') +const NodeInfo$json = { + '1': 'NodeInfo', + '2': [ + {'1': 'num', '3': 1, '4': 1, '5': 13, '10': 'num'}, + { + '1': 'user', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.User', + '10': 'user' + }, + { + '1': 'position', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.Position', + '10': 'position' + }, + {'1': 'snr', '3': 4, '4': 1, '5': 2, '10': 'snr'}, + {'1': 'last_heard', '3': 5, '4': 1, '5': 7, '10': 'lastHeard'}, + { + '1': 'device_metrics', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceMetrics', + '10': 'deviceMetrics' + }, + {'1': 'channel', '3': 7, '4': 1, '5': 13, '10': 'channel'}, + {'1': 'via_mqtt', '3': 8, '4': 1, '5': 8, '10': 'viaMqtt'}, + { + '1': 'hops_away', + '3': 9, + '4': 1, + '5': 13, + '9': 0, + '10': 'hopsAway', + '17': true + }, + {'1': 'is_favorite', '3': 10, '4': 1, '5': 8, '10': 'isFavorite'}, + {'1': 'is_ignored', '3': 11, '4': 1, '5': 8, '10': 'isIgnored'}, + { + '1': 'is_key_manually_verified', + '3': 12, + '4': 1, + '5': 8, + '10': 'isKeyManuallyVerified' + }, + ], + '8': [ + {'1': '_hops_away'}, + ], +}; + +/// Descriptor for `NodeInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeInfoDescriptor = $convert.base64Decode( + 'CghOb2RlSW5mbxIQCgNudW0YASABKA1SA251bRIkCgR1c2VyGAIgASgLMhAubWVzaHRhc3RpYy' + '5Vc2VyUgR1c2VyEjAKCHBvc2l0aW9uGAMgASgLMhQubWVzaHRhc3RpYy5Qb3NpdGlvblIIcG9z' + 'aXRpb24SEAoDc25yGAQgASgCUgNzbnISHQoKbGFzdF9oZWFyZBgFIAEoB1IJbGFzdEhlYXJkEk' + 'AKDmRldmljZV9tZXRyaWNzGAYgASgLMhkubWVzaHRhc3RpYy5EZXZpY2VNZXRyaWNzUg1kZXZp' + 'Y2VNZXRyaWNzEhgKB2NoYW5uZWwYByABKA1SB2NoYW5uZWwSGQoIdmlhX21xdHQYCCABKAhSB3' + 'ZpYU1xdHQSIAoJaG9wc19hd2F5GAkgASgNSABSCGhvcHNBd2F5iAEBEh8KC2lzX2Zhdm9yaXRl' + 'GAogASgIUgppc0Zhdm9yaXRlEh0KCmlzX2lnbm9yZWQYCyABKAhSCWlzSWdub3JlZBI3Chhpc1' + '9rZXlfbWFudWFsbHlfdmVyaWZpZWQYDCABKAhSFWlzS2V5TWFudWFsbHlWZXJpZmllZEIMCgpf' + 'aG9wc19hd2F5'); + +@$core.Deprecated('Use myNodeInfoDescriptor instead') +const MyNodeInfo$json = { + '1': 'MyNodeInfo', + '2': [ + {'1': 'my_node_num', '3': 1, '4': 1, '5': 13, '10': 'myNodeNum'}, + {'1': 'reboot_count', '3': 8, '4': 1, '5': 13, '10': 'rebootCount'}, + {'1': 'min_app_version', '3': 11, '4': 1, '5': 13, '10': 'minAppVersion'}, + {'1': 'device_id', '3': 12, '4': 1, '5': 12, '10': 'deviceId'}, + {'1': 'pio_env', '3': 13, '4': 1, '5': 9, '10': 'pioEnv'}, + { + '1': 'firmware_edition', + '3': 14, + '4': 1, + '5': 14, + '6': '.meshtastic.FirmwareEdition', + '10': 'firmwareEdition' + }, + {'1': 'nodedb_count', '3': 15, '4': 1, '5': 13, '10': 'nodedbCount'}, + ], +}; + +/// Descriptor for `MyNodeInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List myNodeInfoDescriptor = $convert.base64Decode( + 'CgpNeU5vZGVJbmZvEh4KC215X25vZGVfbnVtGAEgASgNUglteU5vZGVOdW0SIQoMcmVib290X2' + 'NvdW50GAggASgNUgtyZWJvb3RDb3VudBImCg9taW5fYXBwX3ZlcnNpb24YCyABKA1SDW1pbkFw' + 'cFZlcnNpb24SGwoJZGV2aWNlX2lkGAwgASgMUghkZXZpY2VJZBIXCgdwaW9fZW52GA0gASgJUg' + 'ZwaW9FbnYSRgoQZmlybXdhcmVfZWRpdGlvbhgOIAEoDjIbLm1lc2h0YXN0aWMuRmlybXdhcmVF' + 'ZGl0aW9uUg9maXJtd2FyZUVkaXRpb24SIQoMbm9kZWRiX2NvdW50GA8gASgNUgtub2RlZGJDb3' + 'VudA=='); + +@$core.Deprecated('Use logRecordDescriptor instead') +const LogRecord$json = { + '1': 'LogRecord', + '2': [ + {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + {'1': 'time', '3': 2, '4': 1, '5': 7, '10': 'time'}, + {'1': 'source', '3': 3, '4': 1, '5': 9, '10': 'source'}, + { + '1': 'level', + '3': 4, + '4': 1, + '5': 14, + '6': '.meshtastic.LogRecord.Level', + '10': 'level' + }, + ], + '4': [LogRecord_Level$json], +}; + +@$core.Deprecated('Use logRecordDescriptor instead') +const LogRecord_Level$json = { + '1': 'Level', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'CRITICAL', '2': 50}, + {'1': 'ERROR', '2': 40}, + {'1': 'WARNING', '2': 30}, + {'1': 'INFO', '2': 20}, + {'1': 'DEBUG', '2': 10}, + {'1': 'TRACE', '2': 5}, + ], +}; + +/// Descriptor for `LogRecord`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List logRecordDescriptor = $convert.base64Decode( + 'CglMb2dSZWNvcmQSGAoHbWVzc2FnZRgBIAEoCVIHbWVzc2FnZRISCgR0aW1lGAIgASgHUgR0aW' + '1lEhYKBnNvdXJjZRgDIAEoCVIGc291cmNlEjEKBWxldmVsGAQgASgOMhsubWVzaHRhc3RpYy5M' + 'b2dSZWNvcmQuTGV2ZWxSBWxldmVsIlgKBUxldmVsEgkKBVVOU0VUEAASDAoIQ1JJVElDQUwQMh' + 'IJCgVFUlJPUhAoEgsKB1dBUk5JTkcQHhIICgRJTkZPEBQSCQoFREVCVUcQChIJCgVUUkFDRRAF'); + +@$core.Deprecated('Use queueStatusDescriptor instead') +const QueueStatus$json = { + '1': 'QueueStatus', + '2': [ + {'1': 'res', '3': 1, '4': 1, '5': 5, '10': 'res'}, + {'1': 'free', '3': 2, '4': 1, '5': 13, '10': 'free'}, + {'1': 'maxlen', '3': 3, '4': 1, '5': 13, '10': 'maxlen'}, + {'1': 'mesh_packet_id', '3': 4, '4': 1, '5': 13, '10': 'meshPacketId'}, + ], +}; + +/// Descriptor for `QueueStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List queueStatusDescriptor = $convert.base64Decode( + 'CgtRdWV1ZVN0YXR1cxIQCgNyZXMYASABKAVSA3JlcxISCgRmcmVlGAIgASgNUgRmcmVlEhYKBm' + '1heGxlbhgDIAEoDVIGbWF4bGVuEiQKDm1lc2hfcGFja2V0X2lkGAQgASgNUgxtZXNoUGFja2V0' + 'SWQ='); + +@$core.Deprecated('Use fromRadioDescriptor instead') +const FromRadio$json = { + '1': 'FromRadio', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 13, '10': 'id'}, + { + '1': 'packet', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.MeshPacket', + '9': 0, + '10': 'packet' + }, + { + '1': 'my_info', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.MyNodeInfo', + '9': 0, + '10': 'myInfo' + }, + { + '1': 'node_info', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.NodeInfo', + '9': 0, + '10': 'nodeInfo' + }, + { + '1': 'config', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.Config', + '9': 0, + '10': 'config' + }, + { + '1': 'log_record', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.LogRecord', + '9': 0, + '10': 'logRecord' + }, + { + '1': 'config_complete_id', + '3': 7, + '4': 1, + '5': 13, + '9': 0, + '10': 'configCompleteId' + }, + {'1': 'rebooted', '3': 8, '4': 1, '5': 8, '9': 0, '10': 'rebooted'}, + { + '1': 'moduleConfig', + '3': 9, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig', + '9': 0, + '10': 'moduleConfig' + }, + { + '1': 'channel', + '3': 10, + '4': 1, + '5': 11, + '6': '.meshtastic.Channel', + '9': 0, + '10': 'channel' + }, + { + '1': 'queueStatus', + '3': 11, + '4': 1, + '5': 11, + '6': '.meshtastic.QueueStatus', + '9': 0, + '10': 'queueStatus' + }, + { + '1': 'xmodemPacket', + '3': 12, + '4': 1, + '5': 11, + '6': '.meshtastic.XModem', + '9': 0, + '10': 'xmodemPacket' + }, + { + '1': 'metadata', + '3': 13, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceMetadata', + '9': 0, + '10': 'metadata' + }, + { + '1': 'mqttClientProxyMessage', + '3': 14, + '4': 1, + '5': 11, + '6': '.meshtastic.MqttClientProxyMessage', + '9': 0, + '10': 'mqttClientProxyMessage' + }, + { + '1': 'fileInfo', + '3': 15, + '4': 1, + '5': 11, + '6': '.meshtastic.FileInfo', + '9': 0, + '10': 'fileInfo' + }, + { + '1': 'clientNotification', + '3': 16, + '4': 1, + '5': 11, + '6': '.meshtastic.ClientNotification', + '9': 0, + '10': 'clientNotification' + }, + { + '1': 'deviceuiConfig', + '3': 17, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceUIConfig', + '9': 0, + '10': 'deviceuiConfig' + }, + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +/// Descriptor for `FromRadio`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List fromRadioDescriptor = $convert.base64Decode( + 'CglGcm9tUmFkaW8SDgoCaWQYASABKA1SAmlkEjAKBnBhY2tldBgCIAEoCzIWLm1lc2h0YXN0aW' + 'MuTWVzaFBhY2tldEgAUgZwYWNrZXQSMQoHbXlfaW5mbxgDIAEoCzIWLm1lc2h0YXN0aWMuTXlO' + 'b2RlSW5mb0gAUgZteUluZm8SMwoJbm9kZV9pbmZvGAQgASgLMhQubWVzaHRhc3RpYy5Ob2RlSW' + '5mb0gAUghub2RlSW5mbxIsCgZjb25maWcYBSABKAsyEi5tZXNodGFzdGljLkNvbmZpZ0gAUgZj' + 'b25maWcSNgoKbG9nX3JlY29yZBgGIAEoCzIVLm1lc2h0YXN0aWMuTG9nUmVjb3JkSABSCWxvZ1' + 'JlY29yZBIuChJjb25maWdfY29tcGxldGVfaWQYByABKA1IAFIQY29uZmlnQ29tcGxldGVJZBIc' + 'CghyZWJvb3RlZBgIIAEoCEgAUghyZWJvb3RlZBI+Cgxtb2R1bGVDb25maWcYCSABKAsyGC5tZX' + 'NodGFzdGljLk1vZHVsZUNvbmZpZ0gAUgxtb2R1bGVDb25maWcSLwoHY2hhbm5lbBgKIAEoCzIT' + 'Lm1lc2h0YXN0aWMuQ2hhbm5lbEgAUgdjaGFubmVsEjsKC3F1ZXVlU3RhdHVzGAsgASgLMhcubW' + 'VzaHRhc3RpYy5RdWV1ZVN0YXR1c0gAUgtxdWV1ZVN0YXR1cxI4Cgx4bW9kZW1QYWNrZXQYDCAB' + 'KAsyEi5tZXNodGFzdGljLlhNb2RlbUgAUgx4bW9kZW1QYWNrZXQSOAoIbWV0YWRhdGEYDSABKA' + 'syGi5tZXNodGFzdGljLkRldmljZU1ldGFkYXRhSABSCG1ldGFkYXRhElwKFm1xdHRDbGllbnRQ' + 'cm94eU1lc3NhZ2UYDiABKAsyIi5tZXNodGFzdGljLk1xdHRDbGllbnRQcm94eU1lc3NhZ2VIAF' + 'IWbXF0dENsaWVudFByb3h5TWVzc2FnZRIyCghmaWxlSW5mbxgPIAEoCzIULm1lc2h0YXN0aWMu' + 'RmlsZUluZm9IAFIIZmlsZUluZm8SUAoSY2xpZW50Tm90aWZpY2F0aW9uGBAgASgLMh4ubWVzaH' + 'Rhc3RpYy5DbGllbnROb3RpZmljYXRpb25IAFISY2xpZW50Tm90aWZpY2F0aW9uEkQKDmRldmlj' + 'ZXVpQ29uZmlnGBEgASgLMhoubWVzaHRhc3RpYy5EZXZpY2VVSUNvbmZpZ0gAUg5kZXZpY2V1aU' + 'NvbmZpZ0IRCg9wYXlsb2FkX3ZhcmlhbnQ='); + +@$core.Deprecated('Use clientNotificationDescriptor instead') +const ClientNotification$json = { + '1': 'ClientNotification', + '2': [ + { + '1': 'reply_id', + '3': 1, + '4': 1, + '5': 13, + '9': 1, + '10': 'replyId', + '17': true + }, + {'1': 'time', '3': 2, '4': 1, '5': 7, '10': 'time'}, + { + '1': 'level', + '3': 3, + '4': 1, + '5': 14, + '6': '.meshtastic.LogRecord.Level', + '10': 'level' + }, + {'1': 'message', '3': 4, '4': 1, '5': 9, '10': 'message'}, + { + '1': 'key_verification_number_inform', + '3': 11, + '4': 1, + '5': 11, + '6': '.meshtastic.KeyVerificationNumberInform', + '9': 0, + '10': 'keyVerificationNumberInform' + }, + { + '1': 'key_verification_number_request', + '3': 12, + '4': 1, + '5': 11, + '6': '.meshtastic.KeyVerificationNumberRequest', + '9': 0, + '10': 'keyVerificationNumberRequest' + }, + { + '1': 'key_verification_final', + '3': 13, + '4': 1, + '5': 11, + '6': '.meshtastic.KeyVerificationFinal', + '9': 0, + '10': 'keyVerificationFinal' + }, + { + '1': 'duplicated_public_key', + '3': 14, + '4': 1, + '5': 11, + '6': '.meshtastic.DuplicatedPublicKey', + '9': 0, + '10': 'duplicatedPublicKey' + }, + { + '1': 'low_entropy_key', + '3': 15, + '4': 1, + '5': 11, + '6': '.meshtastic.LowEntropyKey', + '9': 0, + '10': 'lowEntropyKey' + }, + ], + '8': [ + {'1': 'payload_variant'}, + {'1': '_reply_id'}, + ], +}; + +/// Descriptor for `ClientNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List clientNotificationDescriptor = $convert.base64Decode( + 'ChJDbGllbnROb3RpZmljYXRpb24SHgoIcmVwbHlfaWQYASABKA1IAVIHcmVwbHlJZIgBARISCg' + 'R0aW1lGAIgASgHUgR0aW1lEjEKBWxldmVsGAMgASgOMhsubWVzaHRhc3RpYy5Mb2dSZWNvcmQu' + 'TGV2ZWxSBWxldmVsEhgKB21lc3NhZ2UYBCABKAlSB21lc3NhZ2USbgoea2V5X3ZlcmlmaWNhdG' + 'lvbl9udW1iZXJfaW5mb3JtGAsgASgLMicubWVzaHRhc3RpYy5LZXlWZXJpZmljYXRpb25OdW1i' + 'ZXJJbmZvcm1IAFIba2V5VmVyaWZpY2F0aW9uTnVtYmVySW5mb3JtEnEKH2tleV92ZXJpZmljYX' + 'Rpb25fbnVtYmVyX3JlcXVlc3QYDCABKAsyKC5tZXNodGFzdGljLktleVZlcmlmaWNhdGlvbk51' + 'bWJlclJlcXVlc3RIAFIca2V5VmVyaWZpY2F0aW9uTnVtYmVyUmVxdWVzdBJYChZrZXlfdmVyaW' + 'ZpY2F0aW9uX2ZpbmFsGA0gASgLMiAubWVzaHRhc3RpYy5LZXlWZXJpZmljYXRpb25GaW5hbEgA' + 'UhRrZXlWZXJpZmljYXRpb25GaW5hbBJVChVkdXBsaWNhdGVkX3B1YmxpY19rZXkYDiABKAsyHy' + '5tZXNodGFzdGljLkR1cGxpY2F0ZWRQdWJsaWNLZXlIAFITZHVwbGljYXRlZFB1YmxpY0tleRJD' + 'Cg9sb3dfZW50cm9weV9rZXkYDyABKAsyGS5tZXNodGFzdGljLkxvd0VudHJvcHlLZXlIAFINbG' + '93RW50cm9weUtleUIRCg9wYXlsb2FkX3ZhcmlhbnRCCwoJX3JlcGx5X2lk'); + +@$core.Deprecated('Use keyVerificationNumberInformDescriptor instead') +const KeyVerificationNumberInform$json = { + '1': 'KeyVerificationNumberInform', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 4, '10': 'nonce'}, + {'1': 'remote_longname', '3': 2, '4': 1, '5': 9, '10': 'remoteLongname'}, + {'1': 'security_number', '3': 3, '4': 1, '5': 13, '10': 'securityNumber'}, + ], +}; + +/// Descriptor for `KeyVerificationNumberInform`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List keyVerificationNumberInformDescriptor = + $convert.base64Decode( + 'ChtLZXlWZXJpZmljYXRpb25OdW1iZXJJbmZvcm0SFAoFbm9uY2UYASABKARSBW5vbmNlEicKD3' + 'JlbW90ZV9sb25nbmFtZRgCIAEoCVIOcmVtb3RlTG9uZ25hbWUSJwoPc2VjdXJpdHlfbnVtYmVy' + 'GAMgASgNUg5zZWN1cml0eU51bWJlcg=='); + +@$core.Deprecated('Use keyVerificationNumberRequestDescriptor instead') +const KeyVerificationNumberRequest$json = { + '1': 'KeyVerificationNumberRequest', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 4, '10': 'nonce'}, + {'1': 'remote_longname', '3': 2, '4': 1, '5': 9, '10': 'remoteLongname'}, + ], +}; + +/// Descriptor for `KeyVerificationNumberRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List keyVerificationNumberRequestDescriptor = + $convert.base64Decode( + 'ChxLZXlWZXJpZmljYXRpb25OdW1iZXJSZXF1ZXN0EhQKBW5vbmNlGAEgASgEUgVub25jZRInCg' + '9yZW1vdGVfbG9uZ25hbWUYAiABKAlSDnJlbW90ZUxvbmduYW1l'); + +@$core.Deprecated('Use keyVerificationFinalDescriptor instead') +const KeyVerificationFinal$json = { + '1': 'KeyVerificationFinal', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 4, '10': 'nonce'}, + {'1': 'remote_longname', '3': 2, '4': 1, '5': 9, '10': 'remoteLongname'}, + {'1': 'isSender', '3': 3, '4': 1, '5': 8, '10': 'isSender'}, + { + '1': 'verification_characters', + '3': 4, + '4': 1, + '5': 9, + '10': 'verificationCharacters' + }, + ], +}; + +/// Descriptor for `KeyVerificationFinal`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List keyVerificationFinalDescriptor = $convert.base64Decode( + 'ChRLZXlWZXJpZmljYXRpb25GaW5hbBIUCgVub25jZRgBIAEoBFIFbm9uY2USJwoPcmVtb3RlX2' + 'xvbmduYW1lGAIgASgJUg5yZW1vdGVMb25nbmFtZRIaCghpc1NlbmRlchgDIAEoCFIIaXNTZW5k' + 'ZXISNwoXdmVyaWZpY2F0aW9uX2NoYXJhY3RlcnMYBCABKAlSFnZlcmlmaWNhdGlvbkNoYXJhY3' + 'RlcnM='); + +@$core.Deprecated('Use duplicatedPublicKeyDescriptor instead') +const DuplicatedPublicKey$json = { + '1': 'DuplicatedPublicKey', +}; + +/// Descriptor for `DuplicatedPublicKey`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List duplicatedPublicKeyDescriptor = + $convert.base64Decode('ChNEdXBsaWNhdGVkUHVibGljS2V5'); + +@$core.Deprecated('Use lowEntropyKeyDescriptor instead') +const LowEntropyKey$json = { + '1': 'LowEntropyKey', +}; + +/// Descriptor for `LowEntropyKey`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List lowEntropyKeyDescriptor = + $convert.base64Decode('Cg1Mb3dFbnRyb3B5S2V5'); + +@$core.Deprecated('Use fileInfoDescriptor instead') +const FileInfo$json = { + '1': 'FileInfo', + '2': [ + {'1': 'file_name', '3': 1, '4': 1, '5': 9, '10': 'fileName'}, + {'1': 'size_bytes', '3': 2, '4': 1, '5': 13, '10': 'sizeBytes'}, + ], +}; + +/// Descriptor for `FileInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List fileInfoDescriptor = $convert.base64Decode( + 'CghGaWxlSW5mbxIbCglmaWxlX25hbWUYASABKAlSCGZpbGVOYW1lEh0KCnNpemVfYnl0ZXMYAi' + 'ABKA1SCXNpemVCeXRlcw=='); + +@$core.Deprecated('Use toRadioDescriptor instead') +const ToRadio$json = { + '1': 'ToRadio', + '2': [ + { + '1': 'packet', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.MeshPacket', + '9': 0, + '10': 'packet' + }, + { + '1': 'want_config_id', + '3': 3, + '4': 1, + '5': 13, + '9': 0, + '10': 'wantConfigId' + }, + {'1': 'disconnect', '3': 4, '4': 1, '5': 8, '9': 0, '10': 'disconnect'}, + { + '1': 'xmodemPacket', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.XModem', + '9': 0, + '10': 'xmodemPacket' + }, + { + '1': 'mqttClientProxyMessage', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.MqttClientProxyMessage', + '9': 0, + '10': 'mqttClientProxyMessage' + }, + { + '1': 'heartbeat', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.Heartbeat', + '9': 0, + '10': 'heartbeat' + }, + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +/// Descriptor for `ToRadio`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List toRadioDescriptor = $convert.base64Decode( + 'CgdUb1JhZGlvEjAKBnBhY2tldBgBIAEoCzIWLm1lc2h0YXN0aWMuTWVzaFBhY2tldEgAUgZwYW' + 'NrZXQSJgoOd2FudF9jb25maWdfaWQYAyABKA1IAFIMd2FudENvbmZpZ0lkEiAKCmRpc2Nvbm5l' + 'Y3QYBCABKAhIAFIKZGlzY29ubmVjdBI4Cgx4bW9kZW1QYWNrZXQYBSABKAsyEi5tZXNodGFzdG' + 'ljLlhNb2RlbUgAUgx4bW9kZW1QYWNrZXQSXAoWbXF0dENsaWVudFByb3h5TWVzc2FnZRgGIAEo' + 'CzIiLm1lc2h0YXN0aWMuTXF0dENsaWVudFByb3h5TWVzc2FnZUgAUhZtcXR0Q2xpZW50UHJveH' + 'lNZXNzYWdlEjUKCWhlYXJ0YmVhdBgHIAEoCzIVLm1lc2h0YXN0aWMuSGVhcnRiZWF0SABSCWhl' + 'YXJ0YmVhdEIRCg9wYXlsb2FkX3ZhcmlhbnQ='); + +@$core.Deprecated('Use compressedDescriptor instead') +const Compressed$json = { + '1': 'Compressed', + '2': [ + { + '1': 'portnum', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.PortNum', + '10': 'portnum' + }, + {'1': 'data', '3': 2, '4': 1, '5': 12, '10': 'data'}, + ], +}; + +/// Descriptor for `Compressed`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List compressedDescriptor = $convert.base64Decode( + 'CgpDb21wcmVzc2VkEi0KB3BvcnRudW0YASABKA4yEy5tZXNodGFzdGljLlBvcnROdW1SB3Bvcn' + 'RudW0SEgoEZGF0YRgCIAEoDFIEZGF0YQ=='); + +@$core.Deprecated('Use neighborInfoDescriptor instead') +const NeighborInfo$json = { + '1': 'NeighborInfo', + '2': [ + {'1': 'node_id', '3': 1, '4': 1, '5': 13, '10': 'nodeId'}, + {'1': 'last_sent_by_id', '3': 2, '4': 1, '5': 13, '10': 'lastSentById'}, + { + '1': 'node_broadcast_interval_secs', + '3': 3, + '4': 1, + '5': 13, + '10': 'nodeBroadcastIntervalSecs' + }, + { + '1': 'neighbors', + '3': 4, + '4': 3, + '5': 11, + '6': '.meshtastic.Neighbor', + '10': 'neighbors' + }, + ], +}; + +/// Descriptor for `NeighborInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List neighborInfoDescriptor = $convert.base64Decode( + 'CgxOZWlnaGJvckluZm8SFwoHbm9kZV9pZBgBIAEoDVIGbm9kZUlkEiUKD2xhc3Rfc2VudF9ieV' + '9pZBgCIAEoDVIMbGFzdFNlbnRCeUlkEj8KHG5vZGVfYnJvYWRjYXN0X2ludGVydmFsX3NlY3MY' + 'AyABKA1SGW5vZGVCcm9hZGNhc3RJbnRlcnZhbFNlY3MSMgoJbmVpZ2hib3JzGAQgAygLMhQubW' + 'VzaHRhc3RpYy5OZWlnaGJvclIJbmVpZ2hib3Jz'); + +@$core.Deprecated('Use neighborDescriptor instead') +const Neighbor$json = { + '1': 'Neighbor', + '2': [ + {'1': 'node_id', '3': 1, '4': 1, '5': 13, '10': 'nodeId'}, + {'1': 'snr', '3': 2, '4': 1, '5': 2, '10': 'snr'}, + {'1': 'last_rx_time', '3': 3, '4': 1, '5': 7, '10': 'lastRxTime'}, + { + '1': 'node_broadcast_interval_secs', + '3': 4, + '4': 1, + '5': 13, + '10': 'nodeBroadcastIntervalSecs' + }, + ], +}; + +/// Descriptor for `Neighbor`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List neighborDescriptor = $convert.base64Decode( + 'CghOZWlnaGJvchIXCgdub2RlX2lkGAEgASgNUgZub2RlSWQSEAoDc25yGAIgASgCUgNzbnISIA' + 'oMbGFzdF9yeF90aW1lGAMgASgHUgpsYXN0UnhUaW1lEj8KHG5vZGVfYnJvYWRjYXN0X2ludGVy' + 'dmFsX3NlY3MYBCABKA1SGW5vZGVCcm9hZGNhc3RJbnRlcnZhbFNlY3M='); + +@$core.Deprecated('Use deviceMetadataDescriptor instead') +const DeviceMetadata$json = { + '1': 'DeviceMetadata', + '2': [ + {'1': 'firmware_version', '3': 1, '4': 1, '5': 9, '10': 'firmwareVersion'}, + { + '1': 'device_state_version', + '3': 2, + '4': 1, + '5': 13, + '10': 'deviceStateVersion' + }, + {'1': 'canShutdown', '3': 3, '4': 1, '5': 8, '10': 'canShutdown'}, + {'1': 'hasWifi', '3': 4, '4': 1, '5': 8, '10': 'hasWifi'}, + {'1': 'hasBluetooth', '3': 5, '4': 1, '5': 8, '10': 'hasBluetooth'}, + {'1': 'hasEthernet', '3': 6, '4': 1, '5': 8, '10': 'hasEthernet'}, + { + '1': 'role', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.Role', + '10': 'role' + }, + {'1': 'position_flags', '3': 8, '4': 1, '5': 13, '10': 'positionFlags'}, + { + '1': 'hw_model', + '3': 9, + '4': 1, + '5': 14, + '6': '.meshtastic.HardwareModel', + '10': 'hwModel' + }, + { + '1': 'hasRemoteHardware', + '3': 10, + '4': 1, + '5': 8, + '10': 'hasRemoteHardware' + }, + {'1': 'hasPKC', '3': 11, '4': 1, '5': 8, '10': 'hasPKC'}, + { + '1': 'excluded_modules', + '3': 12, + '4': 1, + '5': 13, + '10': 'excludedModules' + }, + ], +}; + +/// Descriptor for `DeviceMetadata`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceMetadataDescriptor = $convert.base64Decode( + 'Cg5EZXZpY2VNZXRhZGF0YRIpChBmaXJtd2FyZV92ZXJzaW9uGAEgASgJUg9maXJtd2FyZVZlcn' + 'Npb24SMAoUZGV2aWNlX3N0YXRlX3ZlcnNpb24YAiABKA1SEmRldmljZVN0YXRlVmVyc2lvbhIg' + 'CgtjYW5TaHV0ZG93bhgDIAEoCFILY2FuU2h1dGRvd24SGAoHaGFzV2lmaRgEIAEoCFIHaGFzV2' + 'lmaRIiCgxoYXNCbHVldG9vdGgYBSABKAhSDGhhc0JsdWV0b290aBIgCgtoYXNFdGhlcm5ldBgG' + 'IAEoCFILaGFzRXRoZXJuZXQSOAoEcm9sZRgHIAEoDjIkLm1lc2h0YXN0aWMuQ29uZmlnLkRldm' + 'ljZUNvbmZpZy5Sb2xlUgRyb2xlEiUKDnBvc2l0aW9uX2ZsYWdzGAggASgNUg1wb3NpdGlvbkZs' + 'YWdzEjQKCGh3X21vZGVsGAkgASgOMhkubWVzaHRhc3RpYy5IYXJkd2FyZU1vZGVsUgdod01vZG' + 'VsEiwKEWhhc1JlbW90ZUhhcmR3YXJlGAogASgIUhFoYXNSZW1vdGVIYXJkd2FyZRIWCgZoYXNQ' + 'S0MYCyABKAhSBmhhc1BLQxIpChBleGNsdWRlZF9tb2R1bGVzGAwgASgNUg9leGNsdWRlZE1vZH' + 'VsZXM='); + +@$core.Deprecated('Use heartbeatDescriptor instead') +const Heartbeat$json = { + '1': 'Heartbeat', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 13, '10': 'nonce'}, + ], +}; + +/// Descriptor for `Heartbeat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List heartbeatDescriptor = + $convert.base64Decode('CglIZWFydGJlYXQSFAoFbm9uY2UYASABKA1SBW5vbmNl'); + +@$core.Deprecated('Use nodeRemoteHardwarePinDescriptor instead') +const NodeRemoteHardwarePin$json = { + '1': 'NodeRemoteHardwarePin', + '2': [ + {'1': 'node_num', '3': 1, '4': 1, '5': 13, '10': 'nodeNum'}, + { + '1': 'pin', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.RemoteHardwarePin', + '10': 'pin' + }, + ], +}; + +/// Descriptor for `NodeRemoteHardwarePin`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeRemoteHardwarePinDescriptor = $convert.base64Decode( + 'ChVOb2RlUmVtb3RlSGFyZHdhcmVQaW4SGQoIbm9kZV9udW0YASABKA1SB25vZGVOdW0SLwoDcG' + 'luGAIgASgLMh0ubWVzaHRhc3RpYy5SZW1vdGVIYXJkd2FyZVBpblIDcGlu'); + +@$core.Deprecated('Use chunkedPayloadDescriptor instead') +const ChunkedPayload$json = { + '1': 'ChunkedPayload', + '2': [ + {'1': 'payload_id', '3': 1, '4': 1, '5': 13, '10': 'payloadId'}, + {'1': 'chunk_count', '3': 2, '4': 1, '5': 13, '10': 'chunkCount'}, + {'1': 'chunk_index', '3': 3, '4': 1, '5': 13, '10': 'chunkIndex'}, + {'1': 'payload_chunk', '3': 4, '4': 1, '5': 12, '10': 'payloadChunk'}, + ], +}; + +/// Descriptor for `ChunkedPayload`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chunkedPayloadDescriptor = $convert.base64Decode( + 'Cg5DaHVua2VkUGF5bG9hZBIdCgpwYXlsb2FkX2lkGAEgASgNUglwYXlsb2FkSWQSHwoLY2h1bm' + 'tfY291bnQYAiABKA1SCmNodW5rQ291bnQSHwoLY2h1bmtfaW5kZXgYAyABKA1SCmNodW5rSW5k' + 'ZXgSIwoNcGF5bG9hZF9jaHVuaxgEIAEoDFIMcGF5bG9hZENodW5r'); + +@$core.Deprecated('Use resend_chunksDescriptor instead') +const resend_chunks$json = { + '1': 'resend_chunks', + '2': [ + {'1': 'chunks', '3': 1, '4': 3, '5': 13, '10': 'chunks'}, + ], +}; + +/// Descriptor for `resend_chunks`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List resend_chunksDescriptor = $convert + .base64Decode('Cg1yZXNlbmRfY2h1bmtzEhYKBmNodW5rcxgBIAMoDVIGY2h1bmtz'); + +@$core.Deprecated('Use chunkedPayloadResponseDescriptor instead') +const ChunkedPayloadResponse$json = { + '1': 'ChunkedPayloadResponse', + '2': [ + {'1': 'payload_id', '3': 1, '4': 1, '5': 13, '10': 'payloadId'}, + { + '1': 'request_transfer', + '3': 2, + '4': 1, + '5': 8, + '9': 0, + '10': 'requestTransfer' + }, + { + '1': 'accept_transfer', + '3': 3, + '4': 1, + '5': 8, + '9': 0, + '10': 'acceptTransfer' + }, + { + '1': 'resend_chunks', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.resend_chunks', + '9': 0, + '10': 'resendChunks' + }, + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +/// Descriptor for `ChunkedPayloadResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chunkedPayloadResponseDescriptor = $convert.base64Decode( + 'ChZDaHVua2VkUGF5bG9hZFJlc3BvbnNlEh0KCnBheWxvYWRfaWQYASABKA1SCXBheWxvYWRJZB' + 'IrChByZXF1ZXN0X3RyYW5zZmVyGAIgASgISABSD3JlcXVlc3RUcmFuc2ZlchIpCg9hY2NlcHRf' + 'dHJhbnNmZXIYAyABKAhIAFIOYWNjZXB0VHJhbnNmZXISQAoNcmVzZW5kX2NodW5rcxgEIAEoCz' + 'IZLm1lc2h0YXN0aWMucmVzZW5kX2NodW5rc0gAUgxyZXNlbmRDaHVua3NCEQoPcGF5bG9hZF92' + 'YXJpYW50'); diff --git a/third_party/meshtastic_flutter/lib/generated/module_config.pb.dart b/third_party/meshtastic_flutter/lib/generated/module_config.pb.dart new file mode 100644 index 000000000..b1a6b8751 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/module_config.pb.dart @@ -0,0 +1,2686 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/module_config.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'module_config.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'module_config.pbenum.dart'; + +/// +/// MQTT Client Config +class ModuleConfig_MQTTConfig extends $pb.GeneratedMessage { + factory ModuleConfig_MQTTConfig({ + $core.bool? enabled, + $core.String? address, + $core.String? username, + $core.String? password, + $core.bool? encryptionEnabled, + $core.bool? jsonEnabled, + $core.bool? tlsEnabled, + $core.String? root, + $core.bool? proxyToClientEnabled, + $core.bool? mapReportingEnabled, + ModuleConfig_MapReportSettings? mapReportSettings, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (address != null) result.address = address; + if (username != null) result.username = username; + if (password != null) result.password = password; + if (encryptionEnabled != null) result.encryptionEnabled = encryptionEnabled; + if (jsonEnabled != null) result.jsonEnabled = jsonEnabled; + if (tlsEnabled != null) result.tlsEnabled = tlsEnabled; + if (root != null) result.root = root; + if (proxyToClientEnabled != null) + result.proxyToClientEnabled = proxyToClientEnabled; + if (mapReportingEnabled != null) + result.mapReportingEnabled = mapReportingEnabled; + if (mapReportSettings != null) result.mapReportSettings = mapReportSettings; + return result; + } + + ModuleConfig_MQTTConfig._(); + + factory ModuleConfig_MQTTConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_MQTTConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.MQTTConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..aOS(2, _omitFieldNames ? '' : 'address') + ..aOS(3, _omitFieldNames ? '' : 'username') + ..aOS(4, _omitFieldNames ? '' : 'password') + ..aOB(5, _omitFieldNames ? '' : 'encryptionEnabled') + ..aOB(6, _omitFieldNames ? '' : 'jsonEnabled') + ..aOB(7, _omitFieldNames ? '' : 'tlsEnabled') + ..aOS(8, _omitFieldNames ? '' : 'root') + ..aOB(9, _omitFieldNames ? '' : 'proxyToClientEnabled') + ..aOB(10, _omitFieldNames ? '' : 'mapReportingEnabled') + ..aOM( + 11, _omitFieldNames ? '' : 'mapReportSettings', + subBuilder: ModuleConfig_MapReportSettings.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_MQTTConfig clone() => + ModuleConfig_MQTTConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_MQTTConfig copyWith( + void Function(ModuleConfig_MQTTConfig) updates) => + super.copyWith((message) => updates(message as ModuleConfig_MQTTConfig)) + as ModuleConfig_MQTTConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_MQTTConfig create() => ModuleConfig_MQTTConfig._(); + @$core.override + ModuleConfig_MQTTConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_MQTTConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_MQTTConfig? _defaultInstance; + + /// + /// If a meshtastic node is able to reach the internet it will normally attempt to gateway any channels that are marked as + /// is_uplink_enabled or is_downlink_enabled. + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// The server to use for our MQTT global message gateway feature. + /// If not set, the default server will be used + @$pb.TagNumber(2) + $core.String get address => $_getSZ(1); + @$pb.TagNumber(2) + set address($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasAddress() => $_has(1); + @$pb.TagNumber(2) + void clearAddress() => $_clearField(2); + + /// + /// MQTT username to use (most useful for a custom MQTT server). + /// If using a custom server, this will be honoured even if empty. + /// If using the default server, this will only be honoured if set, otherwise the device will use the default username + @$pb.TagNumber(3) + $core.String get username => $_getSZ(2); + @$pb.TagNumber(3) + set username($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasUsername() => $_has(2); + @$pb.TagNumber(3) + void clearUsername() => $_clearField(3); + + /// + /// MQTT password to use (most useful for a custom MQTT server). + /// If using a custom server, this will be honoured even if empty. + /// If using the default server, this will only be honoured if set, otherwise the device will use the default password + @$pb.TagNumber(4) + $core.String get password => $_getSZ(3); + @$pb.TagNumber(4) + set password($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasPassword() => $_has(3); + @$pb.TagNumber(4) + void clearPassword() => $_clearField(4); + + /// + /// Whether to send encrypted or decrypted packets to MQTT. + /// This parameter is only honoured if you also set server + /// (the default official mqtt.meshtastic.org server can handle encrypted packets) + /// Decrypted packets may be useful for external systems that want to consume meshtastic packets + @$pb.TagNumber(5) + $core.bool get encryptionEnabled => $_getBF(4); + @$pb.TagNumber(5) + set encryptionEnabled($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasEncryptionEnabled() => $_has(4); + @$pb.TagNumber(5) + void clearEncryptionEnabled() => $_clearField(5); + + /// + /// Whether to send / consume json packets on MQTT + @$pb.TagNumber(6) + $core.bool get jsonEnabled => $_getBF(5); + @$pb.TagNumber(6) + set jsonEnabled($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasJsonEnabled() => $_has(5); + @$pb.TagNumber(6) + void clearJsonEnabled() => $_clearField(6); + + /// + /// If true, we attempt to establish a secure connection using TLS + @$pb.TagNumber(7) + $core.bool get tlsEnabled => $_getBF(6); + @$pb.TagNumber(7) + set tlsEnabled($core.bool value) => $_setBool(6, value); + @$pb.TagNumber(7) + $core.bool hasTlsEnabled() => $_has(6); + @$pb.TagNumber(7) + void clearTlsEnabled() => $_clearField(7); + + /// + /// The root topic to use for MQTT messages. Default is "msh". + /// This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs + @$pb.TagNumber(8) + $core.String get root => $_getSZ(7); + @$pb.TagNumber(8) + set root($core.String value) => $_setString(7, value); + @$pb.TagNumber(8) + $core.bool hasRoot() => $_has(7); + @$pb.TagNumber(8) + void clearRoot() => $_clearField(8); + + /// + /// If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection + @$pb.TagNumber(9) + $core.bool get proxyToClientEnabled => $_getBF(8); + @$pb.TagNumber(9) + set proxyToClientEnabled($core.bool value) => $_setBool(8, value); + @$pb.TagNumber(9) + $core.bool hasProxyToClientEnabled() => $_has(8); + @$pb.TagNumber(9) + void clearProxyToClientEnabled() => $_clearField(9); + + /// + /// If true, we will periodically report unencrypted information about our node to a map via MQTT + @$pb.TagNumber(10) + $core.bool get mapReportingEnabled => $_getBF(9); + @$pb.TagNumber(10) + set mapReportingEnabled($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasMapReportingEnabled() => $_has(9); + @$pb.TagNumber(10) + void clearMapReportingEnabled() => $_clearField(10); + + /// + /// Settings for reporting information about our node to a map via MQTT + @$pb.TagNumber(11) + ModuleConfig_MapReportSettings get mapReportSettings => $_getN(10); + @$pb.TagNumber(11) + set mapReportSettings(ModuleConfig_MapReportSettings value) => + $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasMapReportSettings() => $_has(10); + @$pb.TagNumber(11) + void clearMapReportSettings() => $_clearField(11); + @$pb.TagNumber(11) + ModuleConfig_MapReportSettings ensureMapReportSettings() => $_ensure(10); +} + +/// +/// Settings for reporting unencrypted information about our node to a map via MQTT +class ModuleConfig_MapReportSettings extends $pb.GeneratedMessage { + factory ModuleConfig_MapReportSettings({ + $core.int? publishIntervalSecs, + $core.int? positionPrecision, + $core.bool? shouldReportLocation, + }) { + final result = create(); + if (publishIntervalSecs != null) + result.publishIntervalSecs = publishIntervalSecs; + if (positionPrecision != null) result.positionPrecision = positionPrecision; + if (shouldReportLocation != null) + result.shouldReportLocation = shouldReportLocation; + return result; + } + + ModuleConfig_MapReportSettings._(); + + factory ModuleConfig_MapReportSettings.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_MapReportSettings.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.MapReportSettings', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'publishIntervalSecs', $pb.PbFieldType.OU3) + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'positionPrecision', $pb.PbFieldType.OU3) + ..aOB(3, _omitFieldNames ? '' : 'shouldReportLocation') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_MapReportSettings clone() => + ModuleConfig_MapReportSettings()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_MapReportSettings copyWith( + void Function(ModuleConfig_MapReportSettings) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_MapReportSettings)) + as ModuleConfig_MapReportSettings; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_MapReportSettings create() => + ModuleConfig_MapReportSettings._(); + @$core.override + ModuleConfig_MapReportSettings createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_MapReportSettings getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_MapReportSettings? _defaultInstance; + + /// + /// How often we should report our info to the map (in seconds) + @$pb.TagNumber(1) + $core.int get publishIntervalSecs => $_getIZ(0); + @$pb.TagNumber(1) + set publishIntervalSecs($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPublishIntervalSecs() => $_has(0); + @$pb.TagNumber(1) + void clearPublishIntervalSecs() => $_clearField(1); + + /// + /// Bits of precision for the location sent (default of 32 is full precision). + @$pb.TagNumber(2) + $core.int get positionPrecision => $_getIZ(1); + @$pb.TagNumber(2) + set positionPrecision($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasPositionPrecision() => $_has(1); + @$pb.TagNumber(2) + void clearPositionPrecision() => $_clearField(2); + + /// + /// Whether we have opted-in to report our location to the map + @$pb.TagNumber(3) + $core.bool get shouldReportLocation => $_getBF(2); + @$pb.TagNumber(3) + set shouldReportLocation($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasShouldReportLocation() => $_has(2); + @$pb.TagNumber(3) + void clearShouldReportLocation() => $_clearField(3); +} + +/// +/// RemoteHardwareModule Config +class ModuleConfig_RemoteHardwareConfig extends $pb.GeneratedMessage { + factory ModuleConfig_RemoteHardwareConfig({ + $core.bool? enabled, + $core.bool? allowUndefinedPinAccess, + $core.Iterable? availablePins, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (allowUndefinedPinAccess != null) + result.allowUndefinedPinAccess = allowUndefinedPinAccess; + if (availablePins != null) result.availablePins.addAll(availablePins); + return result; + } + + ModuleConfig_RemoteHardwareConfig._(); + + factory ModuleConfig_RemoteHardwareConfig.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_RemoteHardwareConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.RemoteHardwareConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..aOB(2, _omitFieldNames ? '' : 'allowUndefinedPinAccess') + ..pc( + 3, _omitFieldNames ? '' : 'availablePins', $pb.PbFieldType.PM, + subBuilder: RemoteHardwarePin.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_RemoteHardwareConfig clone() => + ModuleConfig_RemoteHardwareConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_RemoteHardwareConfig copyWith( + void Function(ModuleConfig_RemoteHardwareConfig) updates) => + super.copyWith((message) => + updates(message as ModuleConfig_RemoteHardwareConfig)) + as ModuleConfig_RemoteHardwareConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_RemoteHardwareConfig create() => + ModuleConfig_RemoteHardwareConfig._(); + @$core.override + ModuleConfig_RemoteHardwareConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_RemoteHardwareConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ModuleConfig_RemoteHardwareConfig? _defaultInstance; + + /// + /// Whether the Module is enabled + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// Whether the Module allows consumers to read / write to pins not defined in available_pins + @$pb.TagNumber(2) + $core.bool get allowUndefinedPinAccess => $_getBF(1); + @$pb.TagNumber(2) + set allowUndefinedPinAccess($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasAllowUndefinedPinAccess() => $_has(1); + @$pb.TagNumber(2) + void clearAllowUndefinedPinAccess() => $_clearField(2); + + /// + /// Exposes the available pins to the mesh for reading and writing + @$pb.TagNumber(3) + $pb.PbList get availablePins => $_getList(2); +} + +/// +/// NeighborInfoModule Config +class ModuleConfig_NeighborInfoConfig extends $pb.GeneratedMessage { + factory ModuleConfig_NeighborInfoConfig({ + $core.bool? enabled, + $core.int? updateInterval, + $core.bool? transmitOverLora, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (updateInterval != null) result.updateInterval = updateInterval; + if (transmitOverLora != null) result.transmitOverLora = transmitOverLora; + return result; + } + + ModuleConfig_NeighborInfoConfig._(); + + factory ModuleConfig_NeighborInfoConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_NeighborInfoConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.NeighborInfoConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'updateInterval', $pb.PbFieldType.OU3) + ..aOB(3, _omitFieldNames ? '' : 'transmitOverLora') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_NeighborInfoConfig clone() => + ModuleConfig_NeighborInfoConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_NeighborInfoConfig copyWith( + void Function(ModuleConfig_NeighborInfoConfig) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_NeighborInfoConfig)) + as ModuleConfig_NeighborInfoConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_NeighborInfoConfig create() => + ModuleConfig_NeighborInfoConfig._(); + @$core.override + ModuleConfig_NeighborInfoConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_NeighborInfoConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ModuleConfig_NeighborInfoConfig? _defaultInstance; + + /// + /// Whether the Module is enabled + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// Interval in seconds of how often we should try to send our + /// Neighbor Info (minimum is 14400, i.e., 4 hours) + @$pb.TagNumber(2) + $core.int get updateInterval => $_getIZ(1); + @$pb.TagNumber(2) + set updateInterval($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasUpdateInterval() => $_has(1); + @$pb.TagNumber(2) + void clearUpdateInterval() => $_clearField(2); + + /// + /// Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. + /// Note that this is not available on a channel with default key and name. + @$pb.TagNumber(3) + $core.bool get transmitOverLora => $_getBF(2); + @$pb.TagNumber(3) + set transmitOverLora($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasTransmitOverLora() => $_has(2); + @$pb.TagNumber(3) + void clearTransmitOverLora() => $_clearField(3); +} + +/// +/// Detection Sensor Module Config +class ModuleConfig_DetectionSensorConfig extends $pb.GeneratedMessage { + factory ModuleConfig_DetectionSensorConfig({ + $core.bool? enabled, + $core.int? minimumBroadcastSecs, + $core.int? stateBroadcastSecs, + $core.bool? sendBell, + $core.String? name, + $core.int? monitorPin, + ModuleConfig_DetectionSensorConfig_TriggerType? detectionTriggerType, + $core.bool? usePullup, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (minimumBroadcastSecs != null) + result.minimumBroadcastSecs = minimumBroadcastSecs; + if (stateBroadcastSecs != null) + result.stateBroadcastSecs = stateBroadcastSecs; + if (sendBell != null) result.sendBell = sendBell; + if (name != null) result.name = name; + if (monitorPin != null) result.monitorPin = monitorPin; + if (detectionTriggerType != null) + result.detectionTriggerType = detectionTriggerType; + if (usePullup != null) result.usePullup = usePullup; + return result; + } + + ModuleConfig_DetectionSensorConfig._(); + + factory ModuleConfig_DetectionSensorConfig.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_DetectionSensorConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.DetectionSensorConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'minimumBroadcastSecs', $pb.PbFieldType.OU3) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'stateBroadcastSecs', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'sendBell') + ..aOS(5, _omitFieldNames ? '' : 'name') + ..a<$core.int>(6, _omitFieldNames ? '' : 'monitorPin', $pb.PbFieldType.OU3) + ..e( + 7, _omitFieldNames ? '' : 'detectionTriggerType', $pb.PbFieldType.OE, + defaultOrMaker: + ModuleConfig_DetectionSensorConfig_TriggerType.LOGIC_LOW, + valueOf: ModuleConfig_DetectionSensorConfig_TriggerType.valueOf, + enumValues: ModuleConfig_DetectionSensorConfig_TriggerType.values) + ..aOB(8, _omitFieldNames ? '' : 'usePullup') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_DetectionSensorConfig clone() => + ModuleConfig_DetectionSensorConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_DetectionSensorConfig copyWith( + void Function(ModuleConfig_DetectionSensorConfig) updates) => + super.copyWith((message) => + updates(message as ModuleConfig_DetectionSensorConfig)) + as ModuleConfig_DetectionSensorConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_DetectionSensorConfig create() => + ModuleConfig_DetectionSensorConfig._(); + @$core.override + ModuleConfig_DetectionSensorConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_DetectionSensorConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ModuleConfig_DetectionSensorConfig? _defaultInstance; + + /// + /// Whether the Module is enabled + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// Interval in seconds of how often we can send a message to the mesh when a + /// trigger event is detected + @$pb.TagNumber(2) + $core.int get minimumBroadcastSecs => $_getIZ(1); + @$pb.TagNumber(2) + set minimumBroadcastSecs($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasMinimumBroadcastSecs() => $_has(1); + @$pb.TagNumber(2) + void clearMinimumBroadcastSecs() => $_clearField(2); + + /// + /// Interval in seconds of how often we should send a message to the mesh + /// with the current state regardless of trigger events When set to 0, only + /// trigger events will be broadcasted Works as a sort of status heartbeat + /// for peace of mind + @$pb.TagNumber(3) + $core.int get stateBroadcastSecs => $_getIZ(2); + @$pb.TagNumber(3) + set stateBroadcastSecs($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasStateBroadcastSecs() => $_has(2); + @$pb.TagNumber(3) + void clearStateBroadcastSecs() => $_clearField(3); + + /// + /// Send ASCII bell with alert message + /// Useful for triggering ext. notification on bell + @$pb.TagNumber(4) + $core.bool get sendBell => $_getBF(3); + @$pb.TagNumber(4) + set sendBell($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasSendBell() => $_has(3); + @$pb.TagNumber(4) + void clearSendBell() => $_clearField(4); + + /// + /// Friendly name used to format message sent to mesh + /// Example: A name "Motion" would result in a message "Motion detected" + /// Maximum length of 20 characters + @$pb.TagNumber(5) + $core.String get name => $_getSZ(4); + @$pb.TagNumber(5) + set name($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasName() => $_has(4); + @$pb.TagNumber(5) + void clearName() => $_clearField(5); + + /// + /// GPIO pin to monitor for state changes + @$pb.TagNumber(6) + $core.int get monitorPin => $_getIZ(5); + @$pb.TagNumber(6) + set monitorPin($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasMonitorPin() => $_has(5); + @$pb.TagNumber(6) + void clearMonitorPin() => $_clearField(6); + + /// + /// The type of trigger event to be used + @$pb.TagNumber(7) + ModuleConfig_DetectionSensorConfig_TriggerType get detectionTriggerType => + $_getN(6); + @$pb.TagNumber(7) + set detectionTriggerType( + ModuleConfig_DetectionSensorConfig_TriggerType value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasDetectionTriggerType() => $_has(6); + @$pb.TagNumber(7) + void clearDetectionTriggerType() => $_clearField(7); + + /// + /// Whether or not use INPUT_PULLUP mode for GPIO pin + /// Only applicable if the board uses pull-up resistors on the pin + @$pb.TagNumber(8) + $core.bool get usePullup => $_getBF(7); + @$pb.TagNumber(8) + set usePullup($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasUsePullup() => $_has(7); + @$pb.TagNumber(8) + void clearUsePullup() => $_clearField(8); +} + +/// +/// Audio Config for codec2 voice +class ModuleConfig_AudioConfig extends $pb.GeneratedMessage { + factory ModuleConfig_AudioConfig({ + $core.bool? codec2Enabled, + $core.int? pttPin, + ModuleConfig_AudioConfig_Audio_Baud? bitrate, + $core.int? i2sWs, + $core.int? i2sSd, + $core.int? i2sDin, + $core.int? i2sSck, + }) { + final result = create(); + if (codec2Enabled != null) result.codec2Enabled = codec2Enabled; + if (pttPin != null) result.pttPin = pttPin; + if (bitrate != null) result.bitrate = bitrate; + if (i2sWs != null) result.i2sWs = i2sWs; + if (i2sSd != null) result.i2sSd = i2sSd; + if (i2sDin != null) result.i2sDin = i2sDin; + if (i2sSck != null) result.i2sSck = i2sSck; + return result; + } + + ModuleConfig_AudioConfig._(); + + factory ModuleConfig_AudioConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_AudioConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.AudioConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'codec2Enabled') + ..a<$core.int>(2, _omitFieldNames ? '' : 'pttPin', $pb.PbFieldType.OU3) + ..e( + 3, _omitFieldNames ? '' : 'bitrate', $pb.PbFieldType.OE, + defaultOrMaker: ModuleConfig_AudioConfig_Audio_Baud.CODEC2_DEFAULT, + valueOf: ModuleConfig_AudioConfig_Audio_Baud.valueOf, + enumValues: ModuleConfig_AudioConfig_Audio_Baud.values) + ..a<$core.int>(4, _omitFieldNames ? '' : 'i2sWs', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'i2sSd', $pb.PbFieldType.OU3) + ..a<$core.int>(6, _omitFieldNames ? '' : 'i2sDin', $pb.PbFieldType.OU3) + ..a<$core.int>(7, _omitFieldNames ? '' : 'i2sSck', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_AudioConfig clone() => + ModuleConfig_AudioConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_AudioConfig copyWith( + void Function(ModuleConfig_AudioConfig) updates) => + super.copyWith((message) => updates(message as ModuleConfig_AudioConfig)) + as ModuleConfig_AudioConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_AudioConfig create() => ModuleConfig_AudioConfig._(); + @$core.override + ModuleConfig_AudioConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_AudioConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_AudioConfig? _defaultInstance; + + /// + /// Whether Audio is enabled + @$pb.TagNumber(1) + $core.bool get codec2Enabled => $_getBF(0); + @$pb.TagNumber(1) + set codec2Enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasCodec2Enabled() => $_has(0); + @$pb.TagNumber(1) + void clearCodec2Enabled() => $_clearField(1); + + /// + /// PTT Pin + @$pb.TagNumber(2) + $core.int get pttPin => $_getIZ(1); + @$pb.TagNumber(2) + set pttPin($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasPttPin() => $_has(1); + @$pb.TagNumber(2) + void clearPttPin() => $_clearField(2); + + /// + /// The audio sample rate to use for codec2 + @$pb.TagNumber(3) + ModuleConfig_AudioConfig_Audio_Baud get bitrate => $_getN(2); + @$pb.TagNumber(3) + set bitrate(ModuleConfig_AudioConfig_Audio_Baud value) => + $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasBitrate() => $_has(2); + @$pb.TagNumber(3) + void clearBitrate() => $_clearField(3); + + /// + /// I2S Word Select + @$pb.TagNumber(4) + $core.int get i2sWs => $_getIZ(3); + @$pb.TagNumber(4) + set i2sWs($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasI2sWs() => $_has(3); + @$pb.TagNumber(4) + void clearI2sWs() => $_clearField(4); + + /// + /// I2S Data IN + @$pb.TagNumber(5) + $core.int get i2sSd => $_getIZ(4); + @$pb.TagNumber(5) + set i2sSd($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasI2sSd() => $_has(4); + @$pb.TagNumber(5) + void clearI2sSd() => $_clearField(5); + + /// + /// I2S Data OUT + @$pb.TagNumber(6) + $core.int get i2sDin => $_getIZ(5); + @$pb.TagNumber(6) + set i2sDin($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasI2sDin() => $_has(5); + @$pb.TagNumber(6) + void clearI2sDin() => $_clearField(6); + + /// + /// I2S Clock + @$pb.TagNumber(7) + $core.int get i2sSck => $_getIZ(6); + @$pb.TagNumber(7) + set i2sSck($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasI2sSck() => $_has(6); + @$pb.TagNumber(7) + void clearI2sSck() => $_clearField(7); +} + +/// +/// Config for the Paxcounter Module +class ModuleConfig_PaxcounterConfig extends $pb.GeneratedMessage { + factory ModuleConfig_PaxcounterConfig({ + $core.bool? enabled, + $core.int? paxcounterUpdateInterval, + $core.int? wifiThreshold, + $core.int? bleThreshold, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (paxcounterUpdateInterval != null) + result.paxcounterUpdateInterval = paxcounterUpdateInterval; + if (wifiThreshold != null) result.wifiThreshold = wifiThreshold; + if (bleThreshold != null) result.bleThreshold = bleThreshold; + return result; + } + + ModuleConfig_PaxcounterConfig._(); + + factory ModuleConfig_PaxcounterConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_PaxcounterConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.PaxcounterConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..a<$core.int>(2, _omitFieldNames ? '' : 'paxcounterUpdateInterval', + $pb.PbFieldType.OU3) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'wifiThreshold', $pb.PbFieldType.O3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'bleThreshold', $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_PaxcounterConfig clone() => + ModuleConfig_PaxcounterConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_PaxcounterConfig copyWith( + void Function(ModuleConfig_PaxcounterConfig) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_PaxcounterConfig)) + as ModuleConfig_PaxcounterConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_PaxcounterConfig create() => + ModuleConfig_PaxcounterConfig._(); + @$core.override + ModuleConfig_PaxcounterConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_PaxcounterConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_PaxcounterConfig? _defaultInstance; + + /// + /// Enable the Paxcounter Module + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get paxcounterUpdateInterval => $_getIZ(1); + @$pb.TagNumber(2) + set paxcounterUpdateInterval($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasPaxcounterUpdateInterval() => $_has(1); + @$pb.TagNumber(2) + void clearPaxcounterUpdateInterval() => $_clearField(2); + + /// + /// WiFi RSSI threshold. Defaults to -80 + @$pb.TagNumber(3) + $core.int get wifiThreshold => $_getIZ(2); + @$pb.TagNumber(3) + set wifiThreshold($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasWifiThreshold() => $_has(2); + @$pb.TagNumber(3) + void clearWifiThreshold() => $_clearField(3); + + /// + /// BLE RSSI threshold. Defaults to -80 + @$pb.TagNumber(4) + $core.int get bleThreshold => $_getIZ(3); + @$pb.TagNumber(4) + set bleThreshold($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasBleThreshold() => $_has(3); + @$pb.TagNumber(4) + void clearBleThreshold() => $_clearField(4); +} + +/// +/// Serial Config +class ModuleConfig_SerialConfig extends $pb.GeneratedMessage { + factory ModuleConfig_SerialConfig({ + $core.bool? enabled, + $core.bool? echo, + $core.int? rxd, + $core.int? txd, + ModuleConfig_SerialConfig_Serial_Baud? baud, + $core.int? timeout, + ModuleConfig_SerialConfig_Serial_Mode? mode, + $core.bool? overrideConsoleSerialPort, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (echo != null) result.echo = echo; + if (rxd != null) result.rxd = rxd; + if (txd != null) result.txd = txd; + if (baud != null) result.baud = baud; + if (timeout != null) result.timeout = timeout; + if (mode != null) result.mode = mode; + if (overrideConsoleSerialPort != null) + result.overrideConsoleSerialPort = overrideConsoleSerialPort; + return result; + } + + ModuleConfig_SerialConfig._(); + + factory ModuleConfig_SerialConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_SerialConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.SerialConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..aOB(2, _omitFieldNames ? '' : 'echo') + ..a<$core.int>(3, _omitFieldNames ? '' : 'rxd', $pb.PbFieldType.OU3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'txd', $pb.PbFieldType.OU3) + ..e( + 5, _omitFieldNames ? '' : 'baud', $pb.PbFieldType.OE, + defaultOrMaker: ModuleConfig_SerialConfig_Serial_Baud.BAUD_DEFAULT, + valueOf: ModuleConfig_SerialConfig_Serial_Baud.valueOf, + enumValues: ModuleConfig_SerialConfig_Serial_Baud.values) + ..a<$core.int>(6, _omitFieldNames ? '' : 'timeout', $pb.PbFieldType.OU3) + ..e( + 7, _omitFieldNames ? '' : 'mode', $pb.PbFieldType.OE, + defaultOrMaker: ModuleConfig_SerialConfig_Serial_Mode.DEFAULT, + valueOf: ModuleConfig_SerialConfig_Serial_Mode.valueOf, + enumValues: ModuleConfig_SerialConfig_Serial_Mode.values) + ..aOB(8, _omitFieldNames ? '' : 'overrideConsoleSerialPort') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_SerialConfig clone() => + ModuleConfig_SerialConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_SerialConfig copyWith( + void Function(ModuleConfig_SerialConfig) updates) => + super.copyWith((message) => updates(message as ModuleConfig_SerialConfig)) + as ModuleConfig_SerialConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_SerialConfig create() => ModuleConfig_SerialConfig._(); + @$core.override + ModuleConfig_SerialConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_SerialConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_SerialConfig? _defaultInstance; + + /// + /// Preferences for the SerialModule + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + $core.bool get echo => $_getBF(1); + @$pb.TagNumber(2) + set echo($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasEcho() => $_has(1); + @$pb.TagNumber(2) + void clearEcho() => $_clearField(2); + + /// + /// RX pin (should match Arduino gpio pin number) + @$pb.TagNumber(3) + $core.int get rxd => $_getIZ(2); + @$pb.TagNumber(3) + set rxd($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasRxd() => $_has(2); + @$pb.TagNumber(3) + void clearRxd() => $_clearField(3); + + /// + /// TX pin (should match Arduino gpio pin number) + @$pb.TagNumber(4) + $core.int get txd => $_getIZ(3); + @$pb.TagNumber(4) + set txd($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasTxd() => $_has(3); + @$pb.TagNumber(4) + void clearTxd() => $_clearField(4); + + /// + /// Serial baud rate + @$pb.TagNumber(5) + ModuleConfig_SerialConfig_Serial_Baud get baud => $_getN(4); + @$pb.TagNumber(5) + set baud(ModuleConfig_SerialConfig_Serial_Baud value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasBaud() => $_has(4); + @$pb.TagNumber(5) + void clearBaud() => $_clearField(5); + + /// + /// TODO: REPLACE + @$pb.TagNumber(6) + $core.int get timeout => $_getIZ(5); + @$pb.TagNumber(6) + set timeout($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasTimeout() => $_has(5); + @$pb.TagNumber(6) + void clearTimeout() => $_clearField(6); + + /// + /// Mode for serial module operation + @$pb.TagNumber(7) + ModuleConfig_SerialConfig_Serial_Mode get mode => $_getN(6); + @$pb.TagNumber(7) + set mode(ModuleConfig_SerialConfig_Serial_Mode value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasMode() => $_has(6); + @$pb.TagNumber(7) + void clearMode() => $_clearField(7); + + /// + /// Overrides the platform's defacto Serial port instance to use with Serial module config settings + /// This is currently only usable in output modes like NMEA / CalTopo and may behave strangely or not work at all in other modes + /// Existing logging over the Serial Console will still be present + @$pb.TagNumber(8) + $core.bool get overrideConsoleSerialPort => $_getBF(7); + @$pb.TagNumber(8) + set overrideConsoleSerialPort($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasOverrideConsoleSerialPort() => $_has(7); + @$pb.TagNumber(8) + void clearOverrideConsoleSerialPort() => $_clearField(8); +} + +/// +/// External Notifications Config +class ModuleConfig_ExternalNotificationConfig extends $pb.GeneratedMessage { + factory ModuleConfig_ExternalNotificationConfig({ + $core.bool? enabled, + $core.int? outputMs, + $core.int? output, + $core.bool? active, + $core.bool? alertMessage, + $core.bool? alertBell, + $core.bool? usePwm, + $core.int? outputVibra, + $core.int? outputBuzzer, + $core.bool? alertMessageVibra, + $core.bool? alertMessageBuzzer, + $core.bool? alertBellVibra, + $core.bool? alertBellBuzzer, + $core.int? nagTimeout, + $core.bool? useI2sAsBuzzer, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (outputMs != null) result.outputMs = outputMs; + if (output != null) result.output = output; + if (active != null) result.active = active; + if (alertMessage != null) result.alertMessage = alertMessage; + if (alertBell != null) result.alertBell = alertBell; + if (usePwm != null) result.usePwm = usePwm; + if (outputVibra != null) result.outputVibra = outputVibra; + if (outputBuzzer != null) result.outputBuzzer = outputBuzzer; + if (alertMessageVibra != null) result.alertMessageVibra = alertMessageVibra; + if (alertMessageBuzzer != null) + result.alertMessageBuzzer = alertMessageBuzzer; + if (alertBellVibra != null) result.alertBellVibra = alertBellVibra; + if (alertBellBuzzer != null) result.alertBellBuzzer = alertBellBuzzer; + if (nagTimeout != null) result.nagTimeout = nagTimeout; + if (useI2sAsBuzzer != null) result.useI2sAsBuzzer = useI2sAsBuzzer; + return result; + } + + ModuleConfig_ExternalNotificationConfig._(); + + factory ModuleConfig_ExternalNotificationConfig.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_ExternalNotificationConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.ExternalNotificationConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..a<$core.int>(2, _omitFieldNames ? '' : 'outputMs', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'output', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'active') + ..aOB(5, _omitFieldNames ? '' : 'alertMessage') + ..aOB(6, _omitFieldNames ? '' : 'alertBell') + ..aOB(7, _omitFieldNames ? '' : 'usePwm') + ..a<$core.int>(8, _omitFieldNames ? '' : 'outputVibra', $pb.PbFieldType.OU3) + ..a<$core.int>( + 9, _omitFieldNames ? '' : 'outputBuzzer', $pb.PbFieldType.OU3) + ..aOB(10, _omitFieldNames ? '' : 'alertMessageVibra') + ..aOB(11, _omitFieldNames ? '' : 'alertMessageBuzzer') + ..aOB(12, _omitFieldNames ? '' : 'alertBellVibra') + ..aOB(13, _omitFieldNames ? '' : 'alertBellBuzzer') + ..a<$core.int>(14, _omitFieldNames ? '' : 'nagTimeout', $pb.PbFieldType.OU3) + ..aOB(15, _omitFieldNames ? '' : 'useI2sAsBuzzer') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_ExternalNotificationConfig clone() => + ModuleConfig_ExternalNotificationConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_ExternalNotificationConfig copyWith( + void Function(ModuleConfig_ExternalNotificationConfig) updates) => + super.copyWith((message) => + updates(message as ModuleConfig_ExternalNotificationConfig)) + as ModuleConfig_ExternalNotificationConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_ExternalNotificationConfig create() => + ModuleConfig_ExternalNotificationConfig._(); + @$core.override + ModuleConfig_ExternalNotificationConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_ExternalNotificationConfig getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ModuleConfig_ExternalNotificationConfig>(create); + static ModuleConfig_ExternalNotificationConfig? _defaultInstance; + + /// + /// Enable the ExternalNotificationModule + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// When using in On/Off mode, keep the output on for this many + /// milliseconds. Default 1000ms (1 second). + @$pb.TagNumber(2) + $core.int get outputMs => $_getIZ(1); + @$pb.TagNumber(2) + set outputMs($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasOutputMs() => $_has(1); + @$pb.TagNumber(2) + void clearOutputMs() => $_clearField(2); + + /// + /// Define the output pin GPIO setting Defaults to + /// EXT_NOTIFY_OUT if set for the board. + /// In standalone devices this pin should drive the LED to match the UI. + @$pb.TagNumber(3) + $core.int get output => $_getIZ(2); + @$pb.TagNumber(3) + set output($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasOutput() => $_has(2); + @$pb.TagNumber(3) + void clearOutput() => $_clearField(3); + + /// + /// IF this is true, the 'output' Pin will be pulled active high, false + /// means active low. + @$pb.TagNumber(4) + $core.bool get active => $_getBF(3); + @$pb.TagNumber(4) + set active($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasActive() => $_has(3); + @$pb.TagNumber(4) + void clearActive() => $_clearField(4); + + /// + /// True: Alert when a text message arrives (output) + @$pb.TagNumber(5) + $core.bool get alertMessage => $_getBF(4); + @$pb.TagNumber(5) + set alertMessage($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasAlertMessage() => $_has(4); + @$pb.TagNumber(5) + void clearAlertMessage() => $_clearField(5); + + /// + /// True: Alert when the bell character is received (output) + @$pb.TagNumber(6) + $core.bool get alertBell => $_getBF(5); + @$pb.TagNumber(6) + set alertBell($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasAlertBell() => $_has(5); + @$pb.TagNumber(6) + void clearAlertBell() => $_clearField(6); + + /// + /// use a PWM output instead of a simple on/off output. This will ignore + /// the 'output', 'output_ms' and 'active' settings and use the + /// device.buzzer_gpio instead. + @$pb.TagNumber(7) + $core.bool get usePwm => $_getBF(6); + @$pb.TagNumber(7) + set usePwm($core.bool value) => $_setBool(6, value); + @$pb.TagNumber(7) + $core.bool hasUsePwm() => $_has(6); + @$pb.TagNumber(7) + void clearUsePwm() => $_clearField(7); + + /// + /// Optional: Define a secondary output pin for a vibra motor + /// This is used in standalone devices to match the UI. + @$pb.TagNumber(8) + $core.int get outputVibra => $_getIZ(7); + @$pb.TagNumber(8) + set outputVibra($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasOutputVibra() => $_has(7); + @$pb.TagNumber(8) + void clearOutputVibra() => $_clearField(8); + + /// + /// Optional: Define a tertiary output pin for an active buzzer + /// This is used in standalone devices to to match the UI. + @$pb.TagNumber(9) + $core.int get outputBuzzer => $_getIZ(8); + @$pb.TagNumber(9) + set outputBuzzer($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasOutputBuzzer() => $_has(8); + @$pb.TagNumber(9) + void clearOutputBuzzer() => $_clearField(9); + + /// + /// True: Alert when a text message arrives (output_vibra) + @$pb.TagNumber(10) + $core.bool get alertMessageVibra => $_getBF(9); + @$pb.TagNumber(10) + set alertMessageVibra($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasAlertMessageVibra() => $_has(9); + @$pb.TagNumber(10) + void clearAlertMessageVibra() => $_clearField(10); + + /// + /// True: Alert when a text message arrives (output_buzzer) + @$pb.TagNumber(11) + $core.bool get alertMessageBuzzer => $_getBF(10); + @$pb.TagNumber(11) + set alertMessageBuzzer($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(11) + $core.bool hasAlertMessageBuzzer() => $_has(10); + @$pb.TagNumber(11) + void clearAlertMessageBuzzer() => $_clearField(11); + + /// + /// True: Alert when the bell character is received (output_vibra) + @$pb.TagNumber(12) + $core.bool get alertBellVibra => $_getBF(11); + @$pb.TagNumber(12) + set alertBellVibra($core.bool value) => $_setBool(11, value); + @$pb.TagNumber(12) + $core.bool hasAlertBellVibra() => $_has(11); + @$pb.TagNumber(12) + void clearAlertBellVibra() => $_clearField(12); + + /// + /// True: Alert when the bell character is received (output_buzzer) + @$pb.TagNumber(13) + $core.bool get alertBellBuzzer => $_getBF(12); + @$pb.TagNumber(13) + set alertBellBuzzer($core.bool value) => $_setBool(12, value); + @$pb.TagNumber(13) + $core.bool hasAlertBellBuzzer() => $_has(12); + @$pb.TagNumber(13) + void clearAlertBellBuzzer() => $_clearField(13); + + /// + /// The notification will toggle with 'output_ms' for this time of seconds. + /// Default is 0 which means don't repeat at all. 60 would mean blink + /// and/or beep for 60 seconds + @$pb.TagNumber(14) + $core.int get nagTimeout => $_getIZ(13); + @$pb.TagNumber(14) + set nagTimeout($core.int value) => $_setUnsignedInt32(13, value); + @$pb.TagNumber(14) + $core.bool hasNagTimeout() => $_has(13); + @$pb.TagNumber(14) + void clearNagTimeout() => $_clearField(14); + + /// + /// When true, enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer + /// T-Watch S3 and T-Deck for example have this capability + @$pb.TagNumber(15) + $core.bool get useI2sAsBuzzer => $_getBF(14); + @$pb.TagNumber(15) + set useI2sAsBuzzer($core.bool value) => $_setBool(14, value); + @$pb.TagNumber(15) + $core.bool hasUseI2sAsBuzzer() => $_has(14); + @$pb.TagNumber(15) + void clearUseI2sAsBuzzer() => $_clearField(15); +} + +/// +/// Store and Forward Module Config +class ModuleConfig_StoreForwardConfig extends $pb.GeneratedMessage { + factory ModuleConfig_StoreForwardConfig({ + $core.bool? enabled, + $core.bool? heartbeat, + $core.int? records, + $core.int? historyReturnMax, + $core.int? historyReturnWindow, + $core.bool? isServer, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (heartbeat != null) result.heartbeat = heartbeat; + if (records != null) result.records = records; + if (historyReturnMax != null) result.historyReturnMax = historyReturnMax; + if (historyReturnWindow != null) + result.historyReturnWindow = historyReturnWindow; + if (isServer != null) result.isServer = isServer; + return result; + } + + ModuleConfig_StoreForwardConfig._(); + + factory ModuleConfig_StoreForwardConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_StoreForwardConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.StoreForwardConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..aOB(2, _omitFieldNames ? '' : 'heartbeat') + ..a<$core.int>(3, _omitFieldNames ? '' : 'records', $pb.PbFieldType.OU3) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'historyReturnMax', $pb.PbFieldType.OU3) + ..a<$core.int>( + 5, _omitFieldNames ? '' : 'historyReturnWindow', $pb.PbFieldType.OU3) + ..aOB(6, _omitFieldNames ? '' : 'isServer') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_StoreForwardConfig clone() => + ModuleConfig_StoreForwardConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_StoreForwardConfig copyWith( + void Function(ModuleConfig_StoreForwardConfig) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_StoreForwardConfig)) + as ModuleConfig_StoreForwardConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_StoreForwardConfig create() => + ModuleConfig_StoreForwardConfig._(); + @$core.override + ModuleConfig_StoreForwardConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_StoreForwardConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ModuleConfig_StoreForwardConfig? _defaultInstance; + + /// + /// Enable the Store and Forward Module + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + $core.bool get heartbeat => $_getBF(1); + @$pb.TagNumber(2) + set heartbeat($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasHeartbeat() => $_has(1); + @$pb.TagNumber(2) + void clearHeartbeat() => $_clearField(2); + + /// + /// TODO: REPLACE + @$pb.TagNumber(3) + $core.int get records => $_getIZ(2); + @$pb.TagNumber(3) + set records($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasRecords() => $_has(2); + @$pb.TagNumber(3) + void clearRecords() => $_clearField(3); + + /// + /// TODO: REPLACE + @$pb.TagNumber(4) + $core.int get historyReturnMax => $_getIZ(3); + @$pb.TagNumber(4) + set historyReturnMax($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasHistoryReturnMax() => $_has(3); + @$pb.TagNumber(4) + void clearHistoryReturnMax() => $_clearField(4); + + /// + /// TODO: REPLACE + @$pb.TagNumber(5) + $core.int get historyReturnWindow => $_getIZ(4); + @$pb.TagNumber(5) + set historyReturnWindow($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasHistoryReturnWindow() => $_has(4); + @$pb.TagNumber(5) + void clearHistoryReturnWindow() => $_clearField(5); + + /// + /// Set to true to let this node act as a server that stores received messages and resends them upon request. + @$pb.TagNumber(6) + $core.bool get isServer => $_getBF(5); + @$pb.TagNumber(6) + set isServer($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasIsServer() => $_has(5); + @$pb.TagNumber(6) + void clearIsServer() => $_clearField(6); +} + +/// +/// Preferences for the RangeTestModule +class ModuleConfig_RangeTestConfig extends $pb.GeneratedMessage { + factory ModuleConfig_RangeTestConfig({ + $core.bool? enabled, + $core.int? sender, + $core.bool? save, + }) { + final result = create(); + if (enabled != null) result.enabled = enabled; + if (sender != null) result.sender = sender; + if (save != null) result.save = save; + return result; + } + + ModuleConfig_RangeTestConfig._(); + + factory ModuleConfig_RangeTestConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_RangeTestConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.RangeTestConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..a<$core.int>(2, _omitFieldNames ? '' : 'sender', $pb.PbFieldType.OU3) + ..aOB(3, _omitFieldNames ? '' : 'save') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_RangeTestConfig clone() => + ModuleConfig_RangeTestConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_RangeTestConfig copyWith( + void Function(ModuleConfig_RangeTestConfig) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_RangeTestConfig)) + as ModuleConfig_RangeTestConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_RangeTestConfig create() => + ModuleConfig_RangeTestConfig._(); + @$core.override + ModuleConfig_RangeTestConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_RangeTestConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_RangeTestConfig? _defaultInstance; + + /// + /// Enable the Range Test Module + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// + /// Send out range test messages from this node + @$pb.TagNumber(2) + $core.int get sender => $_getIZ(1); + @$pb.TagNumber(2) + set sender($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasSender() => $_has(1); + @$pb.TagNumber(2) + void clearSender() => $_clearField(2); + + /// + /// Bool value indicating that this node should save a RangeTest.csv file. + /// ESP32 Only + @$pb.TagNumber(3) + $core.bool get save => $_getBF(2); + @$pb.TagNumber(3) + set save($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasSave() => $_has(2); + @$pb.TagNumber(3) + void clearSave() => $_clearField(3); +} + +/// +/// Configuration for both device and environment metrics +class ModuleConfig_TelemetryConfig extends $pb.GeneratedMessage { + factory ModuleConfig_TelemetryConfig({ + $core.int? deviceUpdateInterval, + $core.int? environmentUpdateInterval, + $core.bool? environmentMeasurementEnabled, + $core.bool? environmentScreenEnabled, + $core.bool? environmentDisplayFahrenheit, + $core.bool? airQualityEnabled, + $core.int? airQualityInterval, + $core.bool? powerMeasurementEnabled, + $core.int? powerUpdateInterval, + $core.bool? powerScreenEnabled, + $core.bool? healthMeasurementEnabled, + $core.int? healthUpdateInterval, + $core.bool? healthScreenEnabled, + }) { + final result = create(); + if (deviceUpdateInterval != null) + result.deviceUpdateInterval = deviceUpdateInterval; + if (environmentUpdateInterval != null) + result.environmentUpdateInterval = environmentUpdateInterval; + if (environmentMeasurementEnabled != null) + result.environmentMeasurementEnabled = environmentMeasurementEnabled; + if (environmentScreenEnabled != null) + result.environmentScreenEnabled = environmentScreenEnabled; + if (environmentDisplayFahrenheit != null) + result.environmentDisplayFahrenheit = environmentDisplayFahrenheit; + if (airQualityEnabled != null) result.airQualityEnabled = airQualityEnabled; + if (airQualityInterval != null) + result.airQualityInterval = airQualityInterval; + if (powerMeasurementEnabled != null) + result.powerMeasurementEnabled = powerMeasurementEnabled; + if (powerUpdateInterval != null) + result.powerUpdateInterval = powerUpdateInterval; + if (powerScreenEnabled != null) + result.powerScreenEnabled = powerScreenEnabled; + if (healthMeasurementEnabled != null) + result.healthMeasurementEnabled = healthMeasurementEnabled; + if (healthUpdateInterval != null) + result.healthUpdateInterval = healthUpdateInterval; + if (healthScreenEnabled != null) + result.healthScreenEnabled = healthScreenEnabled; + return result; + } + + ModuleConfig_TelemetryConfig._(); + + factory ModuleConfig_TelemetryConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_TelemetryConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.TelemetryConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'deviceUpdateInterval', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'environmentUpdateInterval', + $pb.PbFieldType.OU3) + ..aOB(3, _omitFieldNames ? '' : 'environmentMeasurementEnabled') + ..aOB(4, _omitFieldNames ? '' : 'environmentScreenEnabled') + ..aOB(5, _omitFieldNames ? '' : 'environmentDisplayFahrenheit') + ..aOB(6, _omitFieldNames ? '' : 'airQualityEnabled') + ..a<$core.int>( + 7, _omitFieldNames ? '' : 'airQualityInterval', $pb.PbFieldType.OU3) + ..aOB(8, _omitFieldNames ? '' : 'powerMeasurementEnabled') + ..a<$core.int>( + 9, _omitFieldNames ? '' : 'powerUpdateInterval', $pb.PbFieldType.OU3) + ..aOB(10, _omitFieldNames ? '' : 'powerScreenEnabled') + ..aOB(11, _omitFieldNames ? '' : 'healthMeasurementEnabled') + ..a<$core.int>( + 12, _omitFieldNames ? '' : 'healthUpdateInterval', $pb.PbFieldType.OU3) + ..aOB(13, _omitFieldNames ? '' : 'healthScreenEnabled') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_TelemetryConfig clone() => + ModuleConfig_TelemetryConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_TelemetryConfig copyWith( + void Function(ModuleConfig_TelemetryConfig) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_TelemetryConfig)) + as ModuleConfig_TelemetryConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_TelemetryConfig create() => + ModuleConfig_TelemetryConfig._(); + @$core.override + ModuleConfig_TelemetryConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_TelemetryConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig_TelemetryConfig? _defaultInstance; + + /// + /// Interval in seconds of how often we should try to send our + /// device metrics to the mesh + @$pb.TagNumber(1) + $core.int get deviceUpdateInterval => $_getIZ(0); + @$pb.TagNumber(1) + set deviceUpdateInterval($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasDeviceUpdateInterval() => $_has(0); + @$pb.TagNumber(1) + void clearDeviceUpdateInterval() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get environmentUpdateInterval => $_getIZ(1); + @$pb.TagNumber(2) + set environmentUpdateInterval($core.int value) => + $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasEnvironmentUpdateInterval() => $_has(1); + @$pb.TagNumber(2) + void clearEnvironmentUpdateInterval() => $_clearField(2); + + /// + /// Preferences for the Telemetry Module (Environment) + /// Enable/Disable the telemetry measurement module measurement collection + @$pb.TagNumber(3) + $core.bool get environmentMeasurementEnabled => $_getBF(2); + @$pb.TagNumber(3) + set environmentMeasurementEnabled($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(3) + $core.bool hasEnvironmentMeasurementEnabled() => $_has(2); + @$pb.TagNumber(3) + void clearEnvironmentMeasurementEnabled() => $_clearField(3); + + /// + /// Enable/Disable the telemetry measurement module on-device display + @$pb.TagNumber(4) + $core.bool get environmentScreenEnabled => $_getBF(3); + @$pb.TagNumber(4) + set environmentScreenEnabled($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(4) + $core.bool hasEnvironmentScreenEnabled() => $_has(3); + @$pb.TagNumber(4) + void clearEnvironmentScreenEnabled() => $_clearField(4); + + /// + /// We'll always read the sensor in Celsius, but sometimes we might want to + /// display the results in Fahrenheit as a "user preference". + @$pb.TagNumber(5) + $core.bool get environmentDisplayFahrenheit => $_getBF(4); + @$pb.TagNumber(5) + set environmentDisplayFahrenheit($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasEnvironmentDisplayFahrenheit() => $_has(4); + @$pb.TagNumber(5) + void clearEnvironmentDisplayFahrenheit() => $_clearField(5); + + /// + /// Enable/Disable the air quality metrics + @$pb.TagNumber(6) + $core.bool get airQualityEnabled => $_getBF(5); + @$pb.TagNumber(6) + set airQualityEnabled($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasAirQualityEnabled() => $_has(5); + @$pb.TagNumber(6) + void clearAirQualityEnabled() => $_clearField(6); + + /// + /// Interval in seconds of how often we should try to send our + /// air quality metrics to the mesh + @$pb.TagNumber(7) + $core.int get airQualityInterval => $_getIZ(6); + @$pb.TagNumber(7) + set airQualityInterval($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasAirQualityInterval() => $_has(6); + @$pb.TagNumber(7) + void clearAirQualityInterval() => $_clearField(7); + + /// + /// Enable/disable Power metrics + @$pb.TagNumber(8) + $core.bool get powerMeasurementEnabled => $_getBF(7); + @$pb.TagNumber(8) + set powerMeasurementEnabled($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasPowerMeasurementEnabled() => $_has(7); + @$pb.TagNumber(8) + void clearPowerMeasurementEnabled() => $_clearField(8); + + /// + /// Interval in seconds of how often we should try to send our + /// power metrics to the mesh + @$pb.TagNumber(9) + $core.int get powerUpdateInterval => $_getIZ(8); + @$pb.TagNumber(9) + set powerUpdateInterval($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasPowerUpdateInterval() => $_has(8); + @$pb.TagNumber(9) + void clearPowerUpdateInterval() => $_clearField(9); + + /// + /// Enable/Disable the power measurement module on-device display + @$pb.TagNumber(10) + $core.bool get powerScreenEnabled => $_getBF(9); + @$pb.TagNumber(10) + set powerScreenEnabled($core.bool value) => $_setBool(9, value); + @$pb.TagNumber(10) + $core.bool hasPowerScreenEnabled() => $_has(9); + @$pb.TagNumber(10) + void clearPowerScreenEnabled() => $_clearField(10); + + /// + /// Preferences for the (Health) Telemetry Module + /// Enable/Disable the telemetry measurement module measurement collection + @$pb.TagNumber(11) + $core.bool get healthMeasurementEnabled => $_getBF(10); + @$pb.TagNumber(11) + set healthMeasurementEnabled($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(11) + $core.bool hasHealthMeasurementEnabled() => $_has(10); + @$pb.TagNumber(11) + void clearHealthMeasurementEnabled() => $_clearField(11); + + /// + /// Interval in seconds of how often we should try to send our + /// health metrics to the mesh + @$pb.TagNumber(12) + $core.int get healthUpdateInterval => $_getIZ(11); + @$pb.TagNumber(12) + set healthUpdateInterval($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasHealthUpdateInterval() => $_has(11); + @$pb.TagNumber(12) + void clearHealthUpdateInterval() => $_clearField(12); + + /// + /// Enable/Disable the health telemetry module on-device display + @$pb.TagNumber(13) + $core.bool get healthScreenEnabled => $_getBF(12); + @$pb.TagNumber(13) + set healthScreenEnabled($core.bool value) => $_setBool(12, value); + @$pb.TagNumber(13) + $core.bool hasHealthScreenEnabled() => $_has(12); + @$pb.TagNumber(13) + void clearHealthScreenEnabled() => $_clearField(13); +} + +/// +/// Canned Messages Module Config +class ModuleConfig_CannedMessageConfig extends $pb.GeneratedMessage { + factory ModuleConfig_CannedMessageConfig({ + $core.bool? rotary1Enabled, + $core.int? inputbrokerPinA, + $core.int? inputbrokerPinB, + $core.int? inputbrokerPinPress, + ModuleConfig_CannedMessageConfig_InputEventChar? inputbrokerEventCw, + ModuleConfig_CannedMessageConfig_InputEventChar? inputbrokerEventCcw, + ModuleConfig_CannedMessageConfig_InputEventChar? inputbrokerEventPress, + $core.bool? updown1Enabled, + @$core.Deprecated('This field is deprecated.') $core.bool? enabled, + @$core.Deprecated('This field is deprecated.') + $core.String? allowInputSource, + $core.bool? sendBell, + }) { + final result = create(); + if (rotary1Enabled != null) result.rotary1Enabled = rotary1Enabled; + if (inputbrokerPinA != null) result.inputbrokerPinA = inputbrokerPinA; + if (inputbrokerPinB != null) result.inputbrokerPinB = inputbrokerPinB; + if (inputbrokerPinPress != null) + result.inputbrokerPinPress = inputbrokerPinPress; + if (inputbrokerEventCw != null) + result.inputbrokerEventCw = inputbrokerEventCw; + if (inputbrokerEventCcw != null) + result.inputbrokerEventCcw = inputbrokerEventCcw; + if (inputbrokerEventPress != null) + result.inputbrokerEventPress = inputbrokerEventPress; + if (updown1Enabled != null) result.updown1Enabled = updown1Enabled; + if (enabled != null) result.enabled = enabled; + if (allowInputSource != null) result.allowInputSource = allowInputSource; + if (sendBell != null) result.sendBell = sendBell; + return result; + } + + ModuleConfig_CannedMessageConfig._(); + + factory ModuleConfig_CannedMessageConfig.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_CannedMessageConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.CannedMessageConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'rotary1Enabled') + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'inputbrokerPinA', $pb.PbFieldType.OU3) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'inputbrokerPinB', $pb.PbFieldType.OU3) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'inputbrokerPinPress', $pb.PbFieldType.OU3) + ..e( + 5, _omitFieldNames ? '' : 'inputbrokerEventCw', $pb.PbFieldType.OE, + defaultOrMaker: ModuleConfig_CannedMessageConfig_InputEventChar.NONE, + valueOf: ModuleConfig_CannedMessageConfig_InputEventChar.valueOf, + enumValues: ModuleConfig_CannedMessageConfig_InputEventChar.values) + ..e( + 6, _omitFieldNames ? '' : 'inputbrokerEventCcw', $pb.PbFieldType.OE, + defaultOrMaker: ModuleConfig_CannedMessageConfig_InputEventChar.NONE, + valueOf: ModuleConfig_CannedMessageConfig_InputEventChar.valueOf, + enumValues: ModuleConfig_CannedMessageConfig_InputEventChar.values) + ..e( + 7, _omitFieldNames ? '' : 'inputbrokerEventPress', $pb.PbFieldType.OE, + defaultOrMaker: ModuleConfig_CannedMessageConfig_InputEventChar.NONE, + valueOf: ModuleConfig_CannedMessageConfig_InputEventChar.valueOf, + enumValues: ModuleConfig_CannedMessageConfig_InputEventChar.values) + ..aOB(8, _omitFieldNames ? '' : 'updown1Enabled') + ..aOB(9, _omitFieldNames ? '' : 'enabled') + ..aOS(10, _omitFieldNames ? '' : 'allowInputSource') + ..aOB(11, _omitFieldNames ? '' : 'sendBell') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_CannedMessageConfig clone() => + ModuleConfig_CannedMessageConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_CannedMessageConfig copyWith( + void Function(ModuleConfig_CannedMessageConfig) updates) => + super.copyWith( + (message) => updates(message as ModuleConfig_CannedMessageConfig)) + as ModuleConfig_CannedMessageConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_CannedMessageConfig create() => + ModuleConfig_CannedMessageConfig._(); + @$core.override + ModuleConfig_CannedMessageConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_CannedMessageConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ModuleConfig_CannedMessageConfig? _defaultInstance; + + /// + /// Enable the rotary encoder #1. This is a 'dumb' encoder sending pulses on both A and B pins while rotating. + @$pb.TagNumber(1) + $core.bool get rotary1Enabled => $_getBF(0); + @$pb.TagNumber(1) + set rotary1Enabled($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasRotary1Enabled() => $_has(0); + @$pb.TagNumber(1) + void clearRotary1Enabled() => $_clearField(1); + + /// + /// GPIO pin for rotary encoder A port. + @$pb.TagNumber(2) + $core.int get inputbrokerPinA => $_getIZ(1); + @$pb.TagNumber(2) + set inputbrokerPinA($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasInputbrokerPinA() => $_has(1); + @$pb.TagNumber(2) + void clearInputbrokerPinA() => $_clearField(2); + + /// + /// GPIO pin for rotary encoder B port. + @$pb.TagNumber(3) + $core.int get inputbrokerPinB => $_getIZ(2); + @$pb.TagNumber(3) + set inputbrokerPinB($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasInputbrokerPinB() => $_has(2); + @$pb.TagNumber(3) + void clearInputbrokerPinB() => $_clearField(3); + + /// + /// GPIO pin for rotary encoder Press port. + @$pb.TagNumber(4) + $core.int get inputbrokerPinPress => $_getIZ(3); + @$pb.TagNumber(4) + set inputbrokerPinPress($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasInputbrokerPinPress() => $_has(3); + @$pb.TagNumber(4) + void clearInputbrokerPinPress() => $_clearField(4); + + /// + /// Generate input event on CW of this kind. + @$pb.TagNumber(5) + ModuleConfig_CannedMessageConfig_InputEventChar get inputbrokerEventCw => + $_getN(4); + @$pb.TagNumber(5) + set inputbrokerEventCw( + ModuleConfig_CannedMessageConfig_InputEventChar value) => + $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasInputbrokerEventCw() => $_has(4); + @$pb.TagNumber(5) + void clearInputbrokerEventCw() => $_clearField(5); + + /// + /// Generate input event on CCW of this kind. + @$pb.TagNumber(6) + ModuleConfig_CannedMessageConfig_InputEventChar get inputbrokerEventCcw => + $_getN(5); + @$pb.TagNumber(6) + set inputbrokerEventCcw( + ModuleConfig_CannedMessageConfig_InputEventChar value) => + $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasInputbrokerEventCcw() => $_has(5); + @$pb.TagNumber(6) + void clearInputbrokerEventCcw() => $_clearField(6); + + /// + /// Generate input event on Press of this kind. + @$pb.TagNumber(7) + ModuleConfig_CannedMessageConfig_InputEventChar get inputbrokerEventPress => + $_getN(6); + @$pb.TagNumber(7) + set inputbrokerEventPress( + ModuleConfig_CannedMessageConfig_InputEventChar value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasInputbrokerEventPress() => $_has(6); + @$pb.TagNumber(7) + void clearInputbrokerEventPress() => $_clearField(7); + + /// + /// Enable the Up/Down/Select input device. Can be RAK rotary encoder or 3 buttons. Uses the a/b/press definitions from inputbroker. + @$pb.TagNumber(8) + $core.bool get updown1Enabled => $_getBF(7); + @$pb.TagNumber(8) + set updown1Enabled($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasUpdown1Enabled() => $_has(7); + @$pb.TagNumber(8) + void clearUpdown1Enabled() => $_clearField(8); + + /// + /// Enable/disable CannedMessageModule. + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + $core.bool get enabled => $_getBF(8); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + set enabled($core.bool value) => $_setBool(8, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + $core.bool hasEnabled() => $_has(8); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(9) + void clearEnabled() => $_clearField(9); + + /// + /// Input event origin accepted by the canned message module. + /// Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any" + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(10) + $core.String get allowInputSource => $_getSZ(9); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(10) + set allowInputSource($core.String value) => $_setString(9, value); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(10) + $core.bool hasAllowInputSource() => $_has(9); + @$core.Deprecated('This field is deprecated.') + @$pb.TagNumber(10) + void clearAllowInputSource() => $_clearField(10); + + /// + /// CannedMessageModule also sends a bell character with the messages. + /// ExternalNotificationModule can benefit from this feature. + @$pb.TagNumber(11) + $core.bool get sendBell => $_getBF(10); + @$pb.TagNumber(11) + set sendBell($core.bool value) => $_setBool(10, value); + @$pb.TagNumber(11) + $core.bool hasSendBell() => $_has(10); + @$pb.TagNumber(11) + void clearSendBell() => $_clearField(11); +} + +/// +/// Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. +/// Initially created for the RAK14001 RGB LED module. +class ModuleConfig_AmbientLightingConfig extends $pb.GeneratedMessage { + factory ModuleConfig_AmbientLightingConfig({ + $core.bool? ledState, + $core.int? current, + $core.int? red, + $core.int? green, + $core.int? blue, + }) { + final result = create(); + if (ledState != null) result.ledState = ledState; + if (current != null) result.current = current; + if (red != null) result.red = red; + if (green != null) result.green = green; + if (blue != null) result.blue = blue; + return result; + } + + ModuleConfig_AmbientLightingConfig._(); + + factory ModuleConfig_AmbientLightingConfig.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig_AmbientLightingConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig.AmbientLightingConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'ledState') + ..a<$core.int>(2, _omitFieldNames ? '' : 'current', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'red', $pb.PbFieldType.OU3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'green', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'blue', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_AmbientLightingConfig clone() => + ModuleConfig_AmbientLightingConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig_AmbientLightingConfig copyWith( + void Function(ModuleConfig_AmbientLightingConfig) updates) => + super.copyWith((message) => + updates(message as ModuleConfig_AmbientLightingConfig)) + as ModuleConfig_AmbientLightingConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig_AmbientLightingConfig create() => + ModuleConfig_AmbientLightingConfig._(); + @$core.override + ModuleConfig_AmbientLightingConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig_AmbientLightingConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ModuleConfig_AmbientLightingConfig? _defaultInstance; + + /// + /// Sets LED to on or off. + @$pb.TagNumber(1) + $core.bool get ledState => $_getBF(0); + @$pb.TagNumber(1) + set ledState($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasLedState() => $_has(0); + @$pb.TagNumber(1) + void clearLedState() => $_clearField(1); + + /// + /// Sets the current for the LED output. Default is 10. + @$pb.TagNumber(2) + $core.int get current => $_getIZ(1); + @$pb.TagNumber(2) + set current($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasCurrent() => $_has(1); + @$pb.TagNumber(2) + void clearCurrent() => $_clearField(2); + + /// + /// Sets the red LED level. Values are 0-255. + @$pb.TagNumber(3) + $core.int get red => $_getIZ(2); + @$pb.TagNumber(3) + set red($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasRed() => $_has(2); + @$pb.TagNumber(3) + void clearRed() => $_clearField(3); + + /// + /// Sets the green LED level. Values are 0-255. + @$pb.TagNumber(4) + $core.int get green => $_getIZ(3); + @$pb.TagNumber(4) + set green($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasGreen() => $_has(3); + @$pb.TagNumber(4) + void clearGreen() => $_clearField(4); + + /// + /// Sets the blue LED level. Values are 0-255. + @$pb.TagNumber(5) + $core.int get blue => $_getIZ(4); + @$pb.TagNumber(5) + set blue($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasBlue() => $_has(4); + @$pb.TagNumber(5) + void clearBlue() => $_clearField(5); +} + +enum ModuleConfig_PayloadVariant { + mqtt, + serial, + externalNotification, + storeForward, + rangeTest, + telemetry, + cannedMessage, + audio, + remoteHardware, + neighborInfo, + ambientLighting, + detectionSensor, + paxcounter, + notSet +} + +/// +/// Module Config +class ModuleConfig extends $pb.GeneratedMessage { + factory ModuleConfig({ + ModuleConfig_MQTTConfig? mqtt, + ModuleConfig_SerialConfig? serial, + ModuleConfig_ExternalNotificationConfig? externalNotification, + ModuleConfig_StoreForwardConfig? storeForward, + ModuleConfig_RangeTestConfig? rangeTest, + ModuleConfig_TelemetryConfig? telemetry, + ModuleConfig_CannedMessageConfig? cannedMessage, + ModuleConfig_AudioConfig? audio, + ModuleConfig_RemoteHardwareConfig? remoteHardware, + ModuleConfig_NeighborInfoConfig? neighborInfo, + ModuleConfig_AmbientLightingConfig? ambientLighting, + ModuleConfig_DetectionSensorConfig? detectionSensor, + ModuleConfig_PaxcounterConfig? paxcounter, + }) { + final result = create(); + if (mqtt != null) result.mqtt = mqtt; + if (serial != null) result.serial = serial; + if (externalNotification != null) + result.externalNotification = externalNotification; + if (storeForward != null) result.storeForward = storeForward; + if (rangeTest != null) result.rangeTest = rangeTest; + if (telemetry != null) result.telemetry = telemetry; + if (cannedMessage != null) result.cannedMessage = cannedMessage; + if (audio != null) result.audio = audio; + if (remoteHardware != null) result.remoteHardware = remoteHardware; + if (neighborInfo != null) result.neighborInfo = neighborInfo; + if (ambientLighting != null) result.ambientLighting = ambientLighting; + if (detectionSensor != null) result.detectionSensor = detectionSensor; + if (paxcounter != null) result.paxcounter = paxcounter; + return result; + } + + ModuleConfig._(); + + factory ModuleConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ModuleConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, ModuleConfig_PayloadVariant> + _ModuleConfig_PayloadVariantByTag = { + 1: ModuleConfig_PayloadVariant.mqtt, + 2: ModuleConfig_PayloadVariant.serial, + 3: ModuleConfig_PayloadVariant.externalNotification, + 4: ModuleConfig_PayloadVariant.storeForward, + 5: ModuleConfig_PayloadVariant.rangeTest, + 6: ModuleConfig_PayloadVariant.telemetry, + 7: ModuleConfig_PayloadVariant.cannedMessage, + 8: ModuleConfig_PayloadVariant.audio, + 9: ModuleConfig_PayloadVariant.remoteHardware, + 10: ModuleConfig_PayloadVariant.neighborInfo, + 11: ModuleConfig_PayloadVariant.ambientLighting, + 12: ModuleConfig_PayloadVariant.detectionSensor, + 13: ModuleConfig_PayloadVariant.paxcounter, + 0: ModuleConfig_PayloadVariant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ModuleConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) + ..aOM(1, _omitFieldNames ? '' : 'mqtt', + subBuilder: ModuleConfig_MQTTConfig.create) + ..aOM(2, _omitFieldNames ? '' : 'serial', + subBuilder: ModuleConfig_SerialConfig.create) + ..aOM( + 3, _omitFieldNames ? '' : 'externalNotification', + subBuilder: ModuleConfig_ExternalNotificationConfig.create) + ..aOM( + 4, _omitFieldNames ? '' : 'storeForward', + subBuilder: ModuleConfig_StoreForwardConfig.create) + ..aOM(5, _omitFieldNames ? '' : 'rangeTest', + subBuilder: ModuleConfig_RangeTestConfig.create) + ..aOM(6, _omitFieldNames ? '' : 'telemetry', + subBuilder: ModuleConfig_TelemetryConfig.create) + ..aOM( + 7, _omitFieldNames ? '' : 'cannedMessage', + subBuilder: ModuleConfig_CannedMessageConfig.create) + ..aOM(8, _omitFieldNames ? '' : 'audio', + subBuilder: ModuleConfig_AudioConfig.create) + ..aOM( + 9, _omitFieldNames ? '' : 'remoteHardware', + subBuilder: ModuleConfig_RemoteHardwareConfig.create) + ..aOM( + 10, _omitFieldNames ? '' : 'neighborInfo', + subBuilder: ModuleConfig_NeighborInfoConfig.create) + ..aOM( + 11, _omitFieldNames ? '' : 'ambientLighting', + subBuilder: ModuleConfig_AmbientLightingConfig.create) + ..aOM( + 12, _omitFieldNames ? '' : 'detectionSensor', + subBuilder: ModuleConfig_DetectionSensorConfig.create) + ..aOM( + 13, _omitFieldNames ? '' : 'paxcounter', + subBuilder: ModuleConfig_PaxcounterConfig.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig clone() => ModuleConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ModuleConfig copyWith(void Function(ModuleConfig) updates) => + super.copyWith((message) => updates(message as ModuleConfig)) + as ModuleConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ModuleConfig create() => ModuleConfig._(); + @$core.override + ModuleConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ModuleConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ModuleConfig? _defaultInstance; + + ModuleConfig_PayloadVariant whichPayloadVariant() => + _ModuleConfig_PayloadVariantByTag[$_whichOneof(0)]!; + void clearPayloadVariant() => $_clearField($_whichOneof(0)); + + /// + /// TODO: REPLACE + @$pb.TagNumber(1) + ModuleConfig_MQTTConfig get mqtt => $_getN(0); + @$pb.TagNumber(1) + set mqtt(ModuleConfig_MQTTConfig value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasMqtt() => $_has(0); + @$pb.TagNumber(1) + void clearMqtt() => $_clearField(1); + @$pb.TagNumber(1) + ModuleConfig_MQTTConfig ensureMqtt() => $_ensure(0); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + ModuleConfig_SerialConfig get serial => $_getN(1); + @$pb.TagNumber(2) + set serial(ModuleConfig_SerialConfig value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasSerial() => $_has(1); + @$pb.TagNumber(2) + void clearSerial() => $_clearField(2); + @$pb.TagNumber(2) + ModuleConfig_SerialConfig ensureSerial() => $_ensure(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(3) + ModuleConfig_ExternalNotificationConfig get externalNotification => $_getN(2); + @$pb.TagNumber(3) + set externalNotification(ModuleConfig_ExternalNotificationConfig value) => + $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasExternalNotification() => $_has(2); + @$pb.TagNumber(3) + void clearExternalNotification() => $_clearField(3); + @$pb.TagNumber(3) + ModuleConfig_ExternalNotificationConfig ensureExternalNotification() => + $_ensure(2); + + /// + /// TODO: REPLACE + @$pb.TagNumber(4) + ModuleConfig_StoreForwardConfig get storeForward => $_getN(3); + @$pb.TagNumber(4) + set storeForward(ModuleConfig_StoreForwardConfig value) => + $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasStoreForward() => $_has(3); + @$pb.TagNumber(4) + void clearStoreForward() => $_clearField(4); + @$pb.TagNumber(4) + ModuleConfig_StoreForwardConfig ensureStoreForward() => $_ensure(3); + + /// + /// TODO: REPLACE + @$pb.TagNumber(5) + ModuleConfig_RangeTestConfig get rangeTest => $_getN(4); + @$pb.TagNumber(5) + set rangeTest(ModuleConfig_RangeTestConfig value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasRangeTest() => $_has(4); + @$pb.TagNumber(5) + void clearRangeTest() => $_clearField(5); + @$pb.TagNumber(5) + ModuleConfig_RangeTestConfig ensureRangeTest() => $_ensure(4); + + /// + /// TODO: REPLACE + @$pb.TagNumber(6) + ModuleConfig_TelemetryConfig get telemetry => $_getN(5); + @$pb.TagNumber(6) + set telemetry(ModuleConfig_TelemetryConfig value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasTelemetry() => $_has(5); + @$pb.TagNumber(6) + void clearTelemetry() => $_clearField(6); + @$pb.TagNumber(6) + ModuleConfig_TelemetryConfig ensureTelemetry() => $_ensure(5); + + /// + /// TODO: REPLACE + @$pb.TagNumber(7) + ModuleConfig_CannedMessageConfig get cannedMessage => $_getN(6); + @$pb.TagNumber(7) + set cannedMessage(ModuleConfig_CannedMessageConfig value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasCannedMessage() => $_has(6); + @$pb.TagNumber(7) + void clearCannedMessage() => $_clearField(7); + @$pb.TagNumber(7) + ModuleConfig_CannedMessageConfig ensureCannedMessage() => $_ensure(6); + + /// + /// TODO: REPLACE + @$pb.TagNumber(8) + ModuleConfig_AudioConfig get audio => $_getN(7); + @$pb.TagNumber(8) + set audio(ModuleConfig_AudioConfig value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasAudio() => $_has(7); + @$pb.TagNumber(8) + void clearAudio() => $_clearField(8); + @$pb.TagNumber(8) + ModuleConfig_AudioConfig ensureAudio() => $_ensure(7); + + /// + /// TODO: REPLACE + @$pb.TagNumber(9) + ModuleConfig_RemoteHardwareConfig get remoteHardware => $_getN(8); + @$pb.TagNumber(9) + set remoteHardware(ModuleConfig_RemoteHardwareConfig value) => + $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasRemoteHardware() => $_has(8); + @$pb.TagNumber(9) + void clearRemoteHardware() => $_clearField(9); + @$pb.TagNumber(9) + ModuleConfig_RemoteHardwareConfig ensureRemoteHardware() => $_ensure(8); + + /// + /// TODO: REPLACE + @$pb.TagNumber(10) + ModuleConfig_NeighborInfoConfig get neighborInfo => $_getN(9); + @$pb.TagNumber(10) + set neighborInfo(ModuleConfig_NeighborInfoConfig value) => + $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasNeighborInfo() => $_has(9); + @$pb.TagNumber(10) + void clearNeighborInfo() => $_clearField(10); + @$pb.TagNumber(10) + ModuleConfig_NeighborInfoConfig ensureNeighborInfo() => $_ensure(9); + + /// + /// TODO: REPLACE + @$pb.TagNumber(11) + ModuleConfig_AmbientLightingConfig get ambientLighting => $_getN(10); + @$pb.TagNumber(11) + set ambientLighting(ModuleConfig_AmbientLightingConfig value) => + $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasAmbientLighting() => $_has(10); + @$pb.TagNumber(11) + void clearAmbientLighting() => $_clearField(11); + @$pb.TagNumber(11) + ModuleConfig_AmbientLightingConfig ensureAmbientLighting() => $_ensure(10); + + /// + /// TODO: REPLACE + @$pb.TagNumber(12) + ModuleConfig_DetectionSensorConfig get detectionSensor => $_getN(11); + @$pb.TagNumber(12) + set detectionSensor(ModuleConfig_DetectionSensorConfig value) => + $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasDetectionSensor() => $_has(11); + @$pb.TagNumber(12) + void clearDetectionSensor() => $_clearField(12); + @$pb.TagNumber(12) + ModuleConfig_DetectionSensorConfig ensureDetectionSensor() => $_ensure(11); + + /// + /// TODO: REPLACE + @$pb.TagNumber(13) + ModuleConfig_PaxcounterConfig get paxcounter => $_getN(12); + @$pb.TagNumber(13) + set paxcounter(ModuleConfig_PaxcounterConfig value) => $_setField(13, value); + @$pb.TagNumber(13) + $core.bool hasPaxcounter() => $_has(12); + @$pb.TagNumber(13) + void clearPaxcounter() => $_clearField(13); + @$pb.TagNumber(13) + ModuleConfig_PaxcounterConfig ensurePaxcounter() => $_ensure(12); +} + +/// +/// A GPIO pin definition for remote hardware module +class RemoteHardwarePin extends $pb.GeneratedMessage { + factory RemoteHardwarePin({ + $core.int? gpioPin, + $core.String? name, + RemoteHardwarePinType? type, + }) { + final result = create(); + if (gpioPin != null) result.gpioPin = gpioPin; + if (name != null) result.name = name; + if (type != null) result.type = type; + return result; + } + + RemoteHardwarePin._(); + + factory RemoteHardwarePin.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RemoteHardwarePin.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RemoteHardwarePin', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'gpioPin', $pb.PbFieldType.OU3) + ..aOS(2, _omitFieldNames ? '' : 'name') + ..e( + 3, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, + defaultOrMaker: RemoteHardwarePinType.UNKNOWN, + valueOf: RemoteHardwarePinType.valueOf, + enumValues: RemoteHardwarePinType.values) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RemoteHardwarePin clone() => RemoteHardwarePin()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RemoteHardwarePin copyWith(void Function(RemoteHardwarePin) updates) => + super.copyWith((message) => updates(message as RemoteHardwarePin)) + as RemoteHardwarePin; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RemoteHardwarePin create() => RemoteHardwarePin._(); + @$core.override + RemoteHardwarePin createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RemoteHardwarePin getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RemoteHardwarePin? _defaultInstance; + + /// + /// GPIO Pin number (must match Arduino) + @$pb.TagNumber(1) + $core.int get gpioPin => $_getIZ(0); + @$pb.TagNumber(1) + set gpioPin($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasGpioPin() => $_has(0); + @$pb.TagNumber(1) + void clearGpioPin() => $_clearField(1); + + /// + /// Name for the GPIO pin (i.e. Front gate, mailbox, etc) + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => $_clearField(2); + + /// + /// Type of GPIO access available to consumers on the mesh + @$pb.TagNumber(3) + RemoteHardwarePinType get type => $_getN(2); + @$pb.TagNumber(3) + set type(RemoteHardwarePinType value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasType() => $_has(2); + @$pb.TagNumber(3) + void clearType() => $_clearField(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/module_config.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/module_config.pbenum.dart new file mode 100644 index 000000000..8d2e74a20 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/module_config.pbenum.dart @@ -0,0 +1,368 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/module_config.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class RemoteHardwarePinType extends $pb.ProtobufEnum { + /// + /// Unset/unused + static const RemoteHardwarePinType UNKNOWN = + RemoteHardwarePinType._(0, _omitEnumNames ? '' : 'UNKNOWN'); + + /// + /// GPIO pin can be read (if it is high / low) + static const RemoteHardwarePinType DIGITAL_READ = + RemoteHardwarePinType._(1, _omitEnumNames ? '' : 'DIGITAL_READ'); + + /// + /// GPIO pin can be written to (high / low) + static const RemoteHardwarePinType DIGITAL_WRITE = + RemoteHardwarePinType._(2, _omitEnumNames ? '' : 'DIGITAL_WRITE'); + + static const $core.List values = + [ + UNKNOWN, + DIGITAL_READ, + DIGITAL_WRITE, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static RemoteHardwarePinType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const RemoteHardwarePinType._(super.value, super.name); +} + +class ModuleConfig_DetectionSensorConfig_TriggerType extends $pb.ProtobufEnum { + /// Event is triggered if pin is low + static const ModuleConfig_DetectionSensorConfig_TriggerType LOGIC_LOW = + ModuleConfig_DetectionSensorConfig_TriggerType._( + 0, _omitEnumNames ? '' : 'LOGIC_LOW'); + + /// Event is triggered if pin is high + static const ModuleConfig_DetectionSensorConfig_TriggerType LOGIC_HIGH = + ModuleConfig_DetectionSensorConfig_TriggerType._( + 1, _omitEnumNames ? '' : 'LOGIC_HIGH'); + + /// Event is triggered when pin goes high to low + static const ModuleConfig_DetectionSensorConfig_TriggerType FALLING_EDGE = + ModuleConfig_DetectionSensorConfig_TriggerType._( + 2, _omitEnumNames ? '' : 'FALLING_EDGE'); + + /// Event is triggered when pin goes low to high + static const ModuleConfig_DetectionSensorConfig_TriggerType RISING_EDGE = + ModuleConfig_DetectionSensorConfig_TriggerType._( + 3, _omitEnumNames ? '' : 'RISING_EDGE'); + + /// Event is triggered on every pin state change, low is considered to be + /// "active" + static const ModuleConfig_DetectionSensorConfig_TriggerType + EITHER_EDGE_ACTIVE_LOW = ModuleConfig_DetectionSensorConfig_TriggerType._( + 4, _omitEnumNames ? '' : 'EITHER_EDGE_ACTIVE_LOW'); + + /// Event is triggered on every pin state change, high is considered to be + /// "active" + static const ModuleConfig_DetectionSensorConfig_TriggerType + EITHER_EDGE_ACTIVE_HIGH = + ModuleConfig_DetectionSensorConfig_TriggerType._( + 5, _omitEnumNames ? '' : 'EITHER_EDGE_ACTIVE_HIGH'); + + static const $core.List + values = [ + LOGIC_LOW, + LOGIC_HIGH, + FALLING_EDGE, + RISING_EDGE, + EITHER_EDGE_ACTIVE_LOW, + EITHER_EDGE_ACTIVE_HIGH, + ]; + + static final $core.List + _byValue = $pb.ProtobufEnum.$_initByValueList(values, 5); + static ModuleConfig_DetectionSensorConfig_TriggerType? valueOf( + $core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const ModuleConfig_DetectionSensorConfig_TriggerType._( + super.value, super.name); +} + +/// +/// Baudrate for codec2 voice +class ModuleConfig_AudioConfig_Audio_Baud extends $pb.ProtobufEnum { + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_DEFAULT = + ModuleConfig_AudioConfig_Audio_Baud._( + 0, _omitEnumNames ? '' : 'CODEC2_DEFAULT'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_3200 = + ModuleConfig_AudioConfig_Audio_Baud._( + 1, _omitEnumNames ? '' : 'CODEC2_3200'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_2400 = + ModuleConfig_AudioConfig_Audio_Baud._( + 2, _omitEnumNames ? '' : 'CODEC2_2400'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_1600 = + ModuleConfig_AudioConfig_Audio_Baud._( + 3, _omitEnumNames ? '' : 'CODEC2_1600'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_1400 = + ModuleConfig_AudioConfig_Audio_Baud._( + 4, _omitEnumNames ? '' : 'CODEC2_1400'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_1300 = + ModuleConfig_AudioConfig_Audio_Baud._( + 5, _omitEnumNames ? '' : 'CODEC2_1300'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_1200 = + ModuleConfig_AudioConfig_Audio_Baud._( + 6, _omitEnumNames ? '' : 'CODEC2_1200'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_700 = + ModuleConfig_AudioConfig_Audio_Baud._( + 7, _omitEnumNames ? '' : 'CODEC2_700'); + static const ModuleConfig_AudioConfig_Audio_Baud CODEC2_700B = + ModuleConfig_AudioConfig_Audio_Baud._( + 8, _omitEnumNames ? '' : 'CODEC2_700B'); + + static const $core.List values = + [ + CODEC2_DEFAULT, + CODEC2_3200, + CODEC2_2400, + CODEC2_1600, + CODEC2_1400, + CODEC2_1300, + CODEC2_1200, + CODEC2_700, + CODEC2_700B, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 8); + static ModuleConfig_AudioConfig_Audio_Baud? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const ModuleConfig_AudioConfig_Audio_Baud._(super.value, super.name); +} + +/// +/// TODO: REPLACE +class ModuleConfig_SerialConfig_Serial_Baud extends $pb.ProtobufEnum { + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_DEFAULT = + ModuleConfig_SerialConfig_Serial_Baud._( + 0, _omitEnumNames ? '' : 'BAUD_DEFAULT'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_110 = + ModuleConfig_SerialConfig_Serial_Baud._( + 1, _omitEnumNames ? '' : 'BAUD_110'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_300 = + ModuleConfig_SerialConfig_Serial_Baud._( + 2, _omitEnumNames ? '' : 'BAUD_300'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_600 = + ModuleConfig_SerialConfig_Serial_Baud._( + 3, _omitEnumNames ? '' : 'BAUD_600'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_1200 = + ModuleConfig_SerialConfig_Serial_Baud._( + 4, _omitEnumNames ? '' : 'BAUD_1200'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_2400 = + ModuleConfig_SerialConfig_Serial_Baud._( + 5, _omitEnumNames ? '' : 'BAUD_2400'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_4800 = + ModuleConfig_SerialConfig_Serial_Baud._( + 6, _omitEnumNames ? '' : 'BAUD_4800'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_9600 = + ModuleConfig_SerialConfig_Serial_Baud._( + 7, _omitEnumNames ? '' : 'BAUD_9600'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_19200 = + ModuleConfig_SerialConfig_Serial_Baud._( + 8, _omitEnumNames ? '' : 'BAUD_19200'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_38400 = + ModuleConfig_SerialConfig_Serial_Baud._( + 9, _omitEnumNames ? '' : 'BAUD_38400'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_57600 = + ModuleConfig_SerialConfig_Serial_Baud._( + 10, _omitEnumNames ? '' : 'BAUD_57600'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_115200 = + ModuleConfig_SerialConfig_Serial_Baud._( + 11, _omitEnumNames ? '' : 'BAUD_115200'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_230400 = + ModuleConfig_SerialConfig_Serial_Baud._( + 12, _omitEnumNames ? '' : 'BAUD_230400'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_460800 = + ModuleConfig_SerialConfig_Serial_Baud._( + 13, _omitEnumNames ? '' : 'BAUD_460800'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_576000 = + ModuleConfig_SerialConfig_Serial_Baud._( + 14, _omitEnumNames ? '' : 'BAUD_576000'); + static const ModuleConfig_SerialConfig_Serial_Baud BAUD_921600 = + ModuleConfig_SerialConfig_Serial_Baud._( + 15, _omitEnumNames ? '' : 'BAUD_921600'); + + static const $core.List values = + [ + BAUD_DEFAULT, + BAUD_110, + BAUD_300, + BAUD_600, + BAUD_1200, + BAUD_2400, + BAUD_4800, + BAUD_9600, + BAUD_19200, + BAUD_38400, + BAUD_57600, + BAUD_115200, + BAUD_230400, + BAUD_460800, + BAUD_576000, + BAUD_921600, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 15); + static ModuleConfig_SerialConfig_Serial_Baud? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const ModuleConfig_SerialConfig_Serial_Baud._(super.value, super.name); +} + +/// +/// TODO: REPLACE +class ModuleConfig_SerialConfig_Serial_Mode extends $pb.ProtobufEnum { + static const ModuleConfig_SerialConfig_Serial_Mode DEFAULT = + ModuleConfig_SerialConfig_Serial_Mode._( + 0, _omitEnumNames ? '' : 'DEFAULT'); + static const ModuleConfig_SerialConfig_Serial_Mode SIMPLE = + ModuleConfig_SerialConfig_Serial_Mode._( + 1, _omitEnumNames ? '' : 'SIMPLE'); + static const ModuleConfig_SerialConfig_Serial_Mode PROTO = + ModuleConfig_SerialConfig_Serial_Mode._(2, _omitEnumNames ? '' : 'PROTO'); + static const ModuleConfig_SerialConfig_Serial_Mode TEXTMSG = + ModuleConfig_SerialConfig_Serial_Mode._( + 3, _omitEnumNames ? '' : 'TEXTMSG'); + static const ModuleConfig_SerialConfig_Serial_Mode NMEA = + ModuleConfig_SerialConfig_Serial_Mode._(4, _omitEnumNames ? '' : 'NMEA'); + + /// NMEA messages specifically tailored for CalTopo + static const ModuleConfig_SerialConfig_Serial_Mode CALTOPO = + ModuleConfig_SerialConfig_Serial_Mode._( + 5, _omitEnumNames ? '' : 'CALTOPO'); + + /// Ecowitt WS85 weather station + static const ModuleConfig_SerialConfig_Serial_Mode WS85 = + ModuleConfig_SerialConfig_Serial_Mode._(6, _omitEnumNames ? '' : 'WS85'); + + /// VE.Direct is a serial protocol used by Victron Energy products + /// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable + static const ModuleConfig_SerialConfig_Serial_Mode VE_DIRECT = + ModuleConfig_SerialConfig_Serial_Mode._( + 7, _omitEnumNames ? '' : 'VE_DIRECT'); + + /// Used to configure and view some parameters of MeshSolar. + /// https://heltec.org/project/meshsolar/ + static const ModuleConfig_SerialConfig_Serial_Mode MS_CONFIG = + ModuleConfig_SerialConfig_Serial_Mode._( + 8, _omitEnumNames ? '' : 'MS_CONFIG'); + + static const $core.List values = + [ + DEFAULT, + SIMPLE, + PROTO, + TEXTMSG, + NMEA, + CALTOPO, + WS85, + VE_DIRECT, + MS_CONFIG, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 8); + static ModuleConfig_SerialConfig_Serial_Mode? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const ModuleConfig_SerialConfig_Serial_Mode._(super.value, super.name); +} + +/// +/// TODO: REPLACE +class ModuleConfig_CannedMessageConfig_InputEventChar extends $pb.ProtobufEnum { + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar NONE = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 0, _omitEnumNames ? '' : 'NONE'); + + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar UP = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 17, _omitEnumNames ? '' : 'UP'); + + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar DOWN = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 18, _omitEnumNames ? '' : 'DOWN'); + + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar LEFT = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 19, _omitEnumNames ? '' : 'LEFT'); + + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar RIGHT = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 20, _omitEnumNames ? '' : 'RIGHT'); + + /// + /// '\n' + static const ModuleConfig_CannedMessageConfig_InputEventChar SELECT = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 10, _omitEnumNames ? '' : 'SELECT'); + + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar BACK = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 27, _omitEnumNames ? '' : 'BACK'); + + /// + /// TODO: REPLACE + static const ModuleConfig_CannedMessageConfig_InputEventChar CANCEL = + ModuleConfig_CannedMessageConfig_InputEventChar._( + 24, _omitEnumNames ? '' : 'CANCEL'); + + static const $core.List + values = [ + NONE, + UP, + DOWN, + LEFT, + RIGHT, + SELECT, + BACK, + CANCEL, + ]; + + static final $core + .Map<$core.int, ModuleConfig_CannedMessageConfig_InputEventChar> + _byValue = $pb.ProtobufEnum.initByValue(values); + static ModuleConfig_CannedMessageConfig_InputEventChar? valueOf( + $core.int value) => + _byValue[value]; + + const ModuleConfig_CannedMessageConfig_InputEventChar._( + super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/module_config.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/module_config.pbjson.dart new file mode 100644 index 000000000..b58b0cc47 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/module_config.pbjson.dart @@ -0,0 +1,879 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/module_config.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use remoteHardwarePinTypeDescriptor instead') +const RemoteHardwarePinType$json = { + '1': 'RemoteHardwarePinType', + '2': [ + {'1': 'UNKNOWN', '2': 0}, + {'1': 'DIGITAL_READ', '2': 1}, + {'1': 'DIGITAL_WRITE', '2': 2}, + ], +}; + +/// Descriptor for `RemoteHardwarePinType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List remoteHardwarePinTypeDescriptor = $convert.base64Decode( + 'ChVSZW1vdGVIYXJkd2FyZVBpblR5cGUSCwoHVU5LTk9XThAAEhAKDERJR0lUQUxfUkVBRBABEh' + 'EKDURJR0lUQUxfV1JJVEUQAg=='); + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig$json = { + '1': 'ModuleConfig', + '2': [ + { + '1': 'mqtt', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.MQTTConfig', + '9': 0, + '10': 'mqtt' + }, + { + '1': 'serial', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.SerialConfig', + '9': 0, + '10': 'serial' + }, + { + '1': 'external_notification', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.ExternalNotificationConfig', + '9': 0, + '10': 'externalNotification' + }, + { + '1': 'store_forward', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.StoreForwardConfig', + '9': 0, + '10': 'storeForward' + }, + { + '1': 'range_test', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.RangeTestConfig', + '9': 0, + '10': 'rangeTest' + }, + { + '1': 'telemetry', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.TelemetryConfig', + '9': 0, + '10': 'telemetry' + }, + { + '1': 'canned_message', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.CannedMessageConfig', + '9': 0, + '10': 'cannedMessage' + }, + { + '1': 'audio', + '3': 8, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.AudioConfig', + '9': 0, + '10': 'audio' + }, + { + '1': 'remote_hardware', + '3': 9, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.RemoteHardwareConfig', + '9': 0, + '10': 'remoteHardware' + }, + { + '1': 'neighbor_info', + '3': 10, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.NeighborInfoConfig', + '9': 0, + '10': 'neighborInfo' + }, + { + '1': 'ambient_lighting', + '3': 11, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.AmbientLightingConfig', + '9': 0, + '10': 'ambientLighting' + }, + { + '1': 'detection_sensor', + '3': 12, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.DetectionSensorConfig', + '9': 0, + '10': 'detectionSensor' + }, + { + '1': 'paxcounter', + '3': 13, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.PaxcounterConfig', + '9': 0, + '10': 'paxcounter' + }, + ], + '3': [ + ModuleConfig_MQTTConfig$json, + ModuleConfig_MapReportSettings$json, + ModuleConfig_RemoteHardwareConfig$json, + ModuleConfig_NeighborInfoConfig$json, + ModuleConfig_DetectionSensorConfig$json, + ModuleConfig_AudioConfig$json, + ModuleConfig_PaxcounterConfig$json, + ModuleConfig_SerialConfig$json, + ModuleConfig_ExternalNotificationConfig$json, + ModuleConfig_StoreForwardConfig$json, + ModuleConfig_RangeTestConfig$json, + ModuleConfig_TelemetryConfig$json, + ModuleConfig_CannedMessageConfig$json, + ModuleConfig_AmbientLightingConfig$json + ], + '8': [ + {'1': 'payload_variant'}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_MQTTConfig$json = { + '1': 'MQTTConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'address', '3': 2, '4': 1, '5': 9, '10': 'address'}, + {'1': 'username', '3': 3, '4': 1, '5': 9, '10': 'username'}, + {'1': 'password', '3': 4, '4': 1, '5': 9, '10': 'password'}, + { + '1': 'encryption_enabled', + '3': 5, + '4': 1, + '5': 8, + '10': 'encryptionEnabled' + }, + {'1': 'json_enabled', '3': 6, '4': 1, '5': 8, '10': 'jsonEnabled'}, + {'1': 'tls_enabled', '3': 7, '4': 1, '5': 8, '10': 'tlsEnabled'}, + {'1': 'root', '3': 8, '4': 1, '5': 9, '10': 'root'}, + { + '1': 'proxy_to_client_enabled', + '3': 9, + '4': 1, + '5': 8, + '10': 'proxyToClientEnabled' + }, + { + '1': 'map_reporting_enabled', + '3': 10, + '4': 1, + '5': 8, + '10': 'mapReportingEnabled' + }, + { + '1': 'map_report_settings', + '3': 11, + '4': 1, + '5': 11, + '6': '.meshtastic.ModuleConfig.MapReportSettings', + '10': 'mapReportSettings' + }, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_MapReportSettings$json = { + '1': 'MapReportSettings', + '2': [ + { + '1': 'publish_interval_secs', + '3': 1, + '4': 1, + '5': 13, + '10': 'publishIntervalSecs' + }, + { + '1': 'position_precision', + '3': 2, + '4': 1, + '5': 13, + '10': 'positionPrecision' + }, + { + '1': 'should_report_location', + '3': 3, + '4': 1, + '5': 8, + '10': 'shouldReportLocation' + }, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_RemoteHardwareConfig$json = { + '1': 'RemoteHardwareConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + { + '1': 'allow_undefined_pin_access', + '3': 2, + '4': 1, + '5': 8, + '10': 'allowUndefinedPinAccess' + }, + { + '1': 'available_pins', + '3': 3, + '4': 3, + '5': 11, + '6': '.meshtastic.RemoteHardwarePin', + '10': 'availablePins' + }, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_NeighborInfoConfig$json = { + '1': 'NeighborInfoConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'update_interval', '3': 2, '4': 1, '5': 13, '10': 'updateInterval'}, + { + '1': 'transmit_over_lora', + '3': 3, + '4': 1, + '5': 8, + '10': 'transmitOverLora' + }, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_DetectionSensorConfig$json = { + '1': 'DetectionSensorConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + { + '1': 'minimum_broadcast_secs', + '3': 2, + '4': 1, + '5': 13, + '10': 'minimumBroadcastSecs' + }, + { + '1': 'state_broadcast_secs', + '3': 3, + '4': 1, + '5': 13, + '10': 'stateBroadcastSecs' + }, + {'1': 'send_bell', '3': 4, '4': 1, '5': 8, '10': 'sendBell'}, + {'1': 'name', '3': 5, '4': 1, '5': 9, '10': 'name'}, + {'1': 'monitor_pin', '3': 6, '4': 1, '5': 13, '10': 'monitorPin'}, + { + '1': 'detection_trigger_type', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.DetectionSensorConfig.TriggerType', + '10': 'detectionTriggerType' + }, + {'1': 'use_pullup', '3': 8, '4': 1, '5': 8, '10': 'usePullup'}, + ], + '4': [ModuleConfig_DetectionSensorConfig_TriggerType$json], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_DetectionSensorConfig_TriggerType$json = { + '1': 'TriggerType', + '2': [ + {'1': 'LOGIC_LOW', '2': 0}, + {'1': 'LOGIC_HIGH', '2': 1}, + {'1': 'FALLING_EDGE', '2': 2}, + {'1': 'RISING_EDGE', '2': 3}, + {'1': 'EITHER_EDGE_ACTIVE_LOW', '2': 4}, + {'1': 'EITHER_EDGE_ACTIVE_HIGH', '2': 5}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_AudioConfig$json = { + '1': 'AudioConfig', + '2': [ + {'1': 'codec2_enabled', '3': 1, '4': 1, '5': 8, '10': 'codec2Enabled'}, + {'1': 'ptt_pin', '3': 2, '4': 1, '5': 13, '10': 'pttPin'}, + { + '1': 'bitrate', + '3': 3, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.AudioConfig.Audio_Baud', + '10': 'bitrate' + }, + {'1': 'i2s_ws', '3': 4, '4': 1, '5': 13, '10': 'i2sWs'}, + {'1': 'i2s_sd', '3': 5, '4': 1, '5': 13, '10': 'i2sSd'}, + {'1': 'i2s_din', '3': 6, '4': 1, '5': 13, '10': 'i2sDin'}, + {'1': 'i2s_sck', '3': 7, '4': 1, '5': 13, '10': 'i2sSck'}, + ], + '4': [ModuleConfig_AudioConfig_Audio_Baud$json], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_AudioConfig_Audio_Baud$json = { + '1': 'Audio_Baud', + '2': [ + {'1': 'CODEC2_DEFAULT', '2': 0}, + {'1': 'CODEC2_3200', '2': 1}, + {'1': 'CODEC2_2400', '2': 2}, + {'1': 'CODEC2_1600', '2': 3}, + {'1': 'CODEC2_1400', '2': 4}, + {'1': 'CODEC2_1300', '2': 5}, + {'1': 'CODEC2_1200', '2': 6}, + {'1': 'CODEC2_700', '2': 7}, + {'1': 'CODEC2_700B', '2': 8}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_PaxcounterConfig$json = { + '1': 'PaxcounterConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + { + '1': 'paxcounter_update_interval', + '3': 2, + '4': 1, + '5': 13, + '10': 'paxcounterUpdateInterval' + }, + {'1': 'wifi_threshold', '3': 3, '4': 1, '5': 5, '10': 'wifiThreshold'}, + {'1': 'ble_threshold', '3': 4, '4': 1, '5': 5, '10': 'bleThreshold'}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_SerialConfig$json = { + '1': 'SerialConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'echo', '3': 2, '4': 1, '5': 8, '10': 'echo'}, + {'1': 'rxd', '3': 3, '4': 1, '5': 13, '10': 'rxd'}, + {'1': 'txd', '3': 4, '4': 1, '5': 13, '10': 'txd'}, + { + '1': 'baud', + '3': 5, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.SerialConfig.Serial_Baud', + '10': 'baud' + }, + {'1': 'timeout', '3': 6, '4': 1, '5': 13, '10': 'timeout'}, + { + '1': 'mode', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.SerialConfig.Serial_Mode', + '10': 'mode' + }, + { + '1': 'override_console_serial_port', + '3': 8, + '4': 1, + '5': 8, + '10': 'overrideConsoleSerialPort' + }, + ], + '4': [ + ModuleConfig_SerialConfig_Serial_Baud$json, + ModuleConfig_SerialConfig_Serial_Mode$json + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_SerialConfig_Serial_Baud$json = { + '1': 'Serial_Baud', + '2': [ + {'1': 'BAUD_DEFAULT', '2': 0}, + {'1': 'BAUD_110', '2': 1}, + {'1': 'BAUD_300', '2': 2}, + {'1': 'BAUD_600', '2': 3}, + {'1': 'BAUD_1200', '2': 4}, + {'1': 'BAUD_2400', '2': 5}, + {'1': 'BAUD_4800', '2': 6}, + {'1': 'BAUD_9600', '2': 7}, + {'1': 'BAUD_19200', '2': 8}, + {'1': 'BAUD_38400', '2': 9}, + {'1': 'BAUD_57600', '2': 10}, + {'1': 'BAUD_115200', '2': 11}, + {'1': 'BAUD_230400', '2': 12}, + {'1': 'BAUD_460800', '2': 13}, + {'1': 'BAUD_576000', '2': 14}, + {'1': 'BAUD_921600', '2': 15}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_SerialConfig_Serial_Mode$json = { + '1': 'Serial_Mode', + '2': [ + {'1': 'DEFAULT', '2': 0}, + {'1': 'SIMPLE', '2': 1}, + {'1': 'PROTO', '2': 2}, + {'1': 'TEXTMSG', '2': 3}, + {'1': 'NMEA', '2': 4}, + {'1': 'CALTOPO', '2': 5}, + {'1': 'WS85', '2': 6}, + {'1': 'VE_DIRECT', '2': 7}, + {'1': 'MS_CONFIG', '2': 8}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_ExternalNotificationConfig$json = { + '1': 'ExternalNotificationConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'output_ms', '3': 2, '4': 1, '5': 13, '10': 'outputMs'}, + {'1': 'output', '3': 3, '4': 1, '5': 13, '10': 'output'}, + {'1': 'output_vibra', '3': 8, '4': 1, '5': 13, '10': 'outputVibra'}, + {'1': 'output_buzzer', '3': 9, '4': 1, '5': 13, '10': 'outputBuzzer'}, + {'1': 'active', '3': 4, '4': 1, '5': 8, '10': 'active'}, + {'1': 'alert_message', '3': 5, '4': 1, '5': 8, '10': 'alertMessage'}, + { + '1': 'alert_message_vibra', + '3': 10, + '4': 1, + '5': 8, + '10': 'alertMessageVibra' + }, + { + '1': 'alert_message_buzzer', + '3': 11, + '4': 1, + '5': 8, + '10': 'alertMessageBuzzer' + }, + {'1': 'alert_bell', '3': 6, '4': 1, '5': 8, '10': 'alertBell'}, + {'1': 'alert_bell_vibra', '3': 12, '4': 1, '5': 8, '10': 'alertBellVibra'}, + { + '1': 'alert_bell_buzzer', + '3': 13, + '4': 1, + '5': 8, + '10': 'alertBellBuzzer' + }, + {'1': 'use_pwm', '3': 7, '4': 1, '5': 8, '10': 'usePwm'}, + {'1': 'nag_timeout', '3': 14, '4': 1, '5': 13, '10': 'nagTimeout'}, + {'1': 'use_i2s_as_buzzer', '3': 15, '4': 1, '5': 8, '10': 'useI2sAsBuzzer'}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_StoreForwardConfig$json = { + '1': 'StoreForwardConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'heartbeat', '3': 2, '4': 1, '5': 8, '10': 'heartbeat'}, + {'1': 'records', '3': 3, '4': 1, '5': 13, '10': 'records'}, + { + '1': 'history_return_max', + '3': 4, + '4': 1, + '5': 13, + '10': 'historyReturnMax' + }, + { + '1': 'history_return_window', + '3': 5, + '4': 1, + '5': 13, + '10': 'historyReturnWindow' + }, + {'1': 'is_server', '3': 6, '4': 1, '5': 8, '10': 'isServer'}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_RangeTestConfig$json = { + '1': 'RangeTestConfig', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'sender', '3': 2, '4': 1, '5': 13, '10': 'sender'}, + {'1': 'save', '3': 3, '4': 1, '5': 8, '10': 'save'}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_TelemetryConfig$json = { + '1': 'TelemetryConfig', + '2': [ + { + '1': 'device_update_interval', + '3': 1, + '4': 1, + '5': 13, + '10': 'deviceUpdateInterval' + }, + { + '1': 'environment_update_interval', + '3': 2, + '4': 1, + '5': 13, + '10': 'environmentUpdateInterval' + }, + { + '1': 'environment_measurement_enabled', + '3': 3, + '4': 1, + '5': 8, + '10': 'environmentMeasurementEnabled' + }, + { + '1': 'environment_screen_enabled', + '3': 4, + '4': 1, + '5': 8, + '10': 'environmentScreenEnabled' + }, + { + '1': 'environment_display_fahrenheit', + '3': 5, + '4': 1, + '5': 8, + '10': 'environmentDisplayFahrenheit' + }, + { + '1': 'air_quality_enabled', + '3': 6, + '4': 1, + '5': 8, + '10': 'airQualityEnabled' + }, + { + '1': 'air_quality_interval', + '3': 7, + '4': 1, + '5': 13, + '10': 'airQualityInterval' + }, + { + '1': 'power_measurement_enabled', + '3': 8, + '4': 1, + '5': 8, + '10': 'powerMeasurementEnabled' + }, + { + '1': 'power_update_interval', + '3': 9, + '4': 1, + '5': 13, + '10': 'powerUpdateInterval' + }, + { + '1': 'power_screen_enabled', + '3': 10, + '4': 1, + '5': 8, + '10': 'powerScreenEnabled' + }, + { + '1': 'health_measurement_enabled', + '3': 11, + '4': 1, + '5': 8, + '10': 'healthMeasurementEnabled' + }, + { + '1': 'health_update_interval', + '3': 12, + '4': 1, + '5': 13, + '10': 'healthUpdateInterval' + }, + { + '1': 'health_screen_enabled', + '3': 13, + '4': 1, + '5': 8, + '10': 'healthScreenEnabled' + }, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_CannedMessageConfig$json = { + '1': 'CannedMessageConfig', + '2': [ + {'1': 'rotary1_enabled', '3': 1, '4': 1, '5': 8, '10': 'rotary1Enabled'}, + { + '1': 'inputbroker_pin_a', + '3': 2, + '4': 1, + '5': 13, + '10': 'inputbrokerPinA' + }, + { + '1': 'inputbroker_pin_b', + '3': 3, + '4': 1, + '5': 13, + '10': 'inputbrokerPinB' + }, + { + '1': 'inputbroker_pin_press', + '3': 4, + '4': 1, + '5': 13, + '10': 'inputbrokerPinPress' + }, + { + '1': 'inputbroker_event_cw', + '3': 5, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.CannedMessageConfig.InputEventChar', + '10': 'inputbrokerEventCw' + }, + { + '1': 'inputbroker_event_ccw', + '3': 6, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.CannedMessageConfig.InputEventChar', + '10': 'inputbrokerEventCcw' + }, + { + '1': 'inputbroker_event_press', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.ModuleConfig.CannedMessageConfig.InputEventChar', + '10': 'inputbrokerEventPress' + }, + {'1': 'updown1_enabled', '3': 8, '4': 1, '5': 8, '10': 'updown1Enabled'}, + { + '1': 'enabled', + '3': 9, + '4': 1, + '5': 8, + '8': {'3': true}, + '10': 'enabled', + }, + { + '1': 'allow_input_source', + '3': 10, + '4': 1, + '5': 9, + '8': {'3': true}, + '10': 'allowInputSource', + }, + {'1': 'send_bell', '3': 11, '4': 1, '5': 8, '10': 'sendBell'}, + ], + '4': [ModuleConfig_CannedMessageConfig_InputEventChar$json], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_CannedMessageConfig_InputEventChar$json = { + '1': 'InputEventChar', + '2': [ + {'1': 'NONE', '2': 0}, + {'1': 'UP', '2': 17}, + {'1': 'DOWN', '2': 18}, + {'1': 'LEFT', '2': 19}, + {'1': 'RIGHT', '2': 20}, + {'1': 'SELECT', '2': 10}, + {'1': 'BACK', '2': 27}, + {'1': 'CANCEL', '2': 24}, + ], +}; + +@$core.Deprecated('Use moduleConfigDescriptor instead') +const ModuleConfig_AmbientLightingConfig$json = { + '1': 'AmbientLightingConfig', + '2': [ + {'1': 'led_state', '3': 1, '4': 1, '5': 8, '10': 'ledState'}, + {'1': 'current', '3': 2, '4': 1, '5': 13, '10': 'current'}, + {'1': 'red', '3': 3, '4': 1, '5': 13, '10': 'red'}, + {'1': 'green', '3': 4, '4': 1, '5': 13, '10': 'green'}, + {'1': 'blue', '3': 5, '4': 1, '5': 13, '10': 'blue'}, + ], +}; + +/// Descriptor for `ModuleConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List moduleConfigDescriptor = $convert.base64Decode( + 'CgxNb2R1bGVDb25maWcSOQoEbXF0dBgBIAEoCzIjLm1lc2h0YXN0aWMuTW9kdWxlQ29uZmlnLk' + '1RVFRDb25maWdIAFIEbXF0dBI/CgZzZXJpYWwYAiABKAsyJS5tZXNodGFzdGljLk1vZHVsZUNv' + 'bmZpZy5TZXJpYWxDb25maWdIAFIGc2VyaWFsEmoKFWV4dGVybmFsX25vdGlmaWNhdGlvbhgDIA' + 'EoCzIzLm1lc2h0YXN0aWMuTW9kdWxlQ29uZmlnLkV4dGVybmFsTm90aWZpY2F0aW9uQ29uZmln' + 'SABSFGV4dGVybmFsTm90aWZpY2F0aW9uElIKDXN0b3JlX2ZvcndhcmQYBCABKAsyKy5tZXNodG' + 'FzdGljLk1vZHVsZUNvbmZpZy5TdG9yZUZvcndhcmRDb25maWdIAFIMc3RvcmVGb3J3YXJkEkkK' + 'CnJhbmdlX3Rlc3QYBSABKAsyKC5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5SYW5nZVRlc3RDb2' + '5maWdIAFIJcmFuZ2VUZXN0EkgKCXRlbGVtZXRyeRgGIAEoCzIoLm1lc2h0YXN0aWMuTW9kdWxl' + 'Q29uZmlnLlRlbGVtZXRyeUNvbmZpZ0gAUgl0ZWxlbWV0cnkSVQoOY2FubmVkX21lc3NhZ2UYBy' + 'ABKAsyLC5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5DYW5uZWRNZXNzYWdlQ29uZmlnSABSDWNh' + 'bm5lZE1lc3NhZ2USPAoFYXVkaW8YCCABKAsyJC5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5BdW' + 'Rpb0NvbmZpZ0gAUgVhdWRpbxJYCg9yZW1vdGVfaGFyZHdhcmUYCSABKAsyLS5tZXNodGFzdGlj' + 'Lk1vZHVsZUNvbmZpZy5SZW1vdGVIYXJkd2FyZUNvbmZpZ0gAUg5yZW1vdGVIYXJkd2FyZRJSCg' + '1uZWlnaGJvcl9pbmZvGAogASgLMisubWVzaHRhc3RpYy5Nb2R1bGVDb25maWcuTmVpZ2hib3JJ' + 'bmZvQ29uZmlnSABSDG5laWdoYm9ySW5mbxJbChBhbWJpZW50X2xpZ2h0aW5nGAsgASgLMi4ubW' + 'VzaHRhc3RpYy5Nb2R1bGVDb25maWcuQW1iaWVudExpZ2h0aW5nQ29uZmlnSABSD2FtYmllbnRM' + 'aWdodGluZxJbChBkZXRlY3Rpb25fc2Vuc29yGAwgASgLMi4ubWVzaHRhc3RpYy5Nb2R1bGVDb2' + '5maWcuRGV0ZWN0aW9uU2Vuc29yQ29uZmlnSABSD2RldGVjdGlvblNlbnNvchJLCgpwYXhjb3Vu' + 'dGVyGA0gASgLMikubWVzaHRhc3RpYy5Nb2R1bGVDb25maWcuUGF4Y291bnRlckNvbmZpZ0gAUg' + 'pwYXhjb3VudGVyGsYDCgpNUVRUQ29uZmlnEhgKB2VuYWJsZWQYASABKAhSB2VuYWJsZWQSGAoH' + 'YWRkcmVzcxgCIAEoCVIHYWRkcmVzcxIaCgh1c2VybmFtZRgDIAEoCVIIdXNlcm5hbWUSGgoIcG' + 'Fzc3dvcmQYBCABKAlSCHBhc3N3b3JkEi0KEmVuY3J5cHRpb25fZW5hYmxlZBgFIAEoCFIRZW5j' + 'cnlwdGlvbkVuYWJsZWQSIQoManNvbl9lbmFibGVkGAYgASgIUgtqc29uRW5hYmxlZBIfCgt0bH' + 'NfZW5hYmxlZBgHIAEoCFIKdGxzRW5hYmxlZBISCgRyb290GAggASgJUgRyb290EjUKF3Byb3h5' + 'X3RvX2NsaWVudF9lbmFibGVkGAkgASgIUhRwcm94eVRvQ2xpZW50RW5hYmxlZBIyChVtYXBfcm' + 'Vwb3J0aW5nX2VuYWJsZWQYCiABKAhSE21hcFJlcG9ydGluZ0VuYWJsZWQSWgoTbWFwX3JlcG9y' + 'dF9zZXR0aW5ncxgLIAEoCzIqLm1lc2h0YXN0aWMuTW9kdWxlQ29uZmlnLk1hcFJlcG9ydFNldH' + 'RpbmdzUhFtYXBSZXBvcnRTZXR0aW5ncxqsAQoRTWFwUmVwb3J0U2V0dGluZ3MSMgoVcHVibGlz' + 'aF9pbnRlcnZhbF9zZWNzGAEgASgNUhNwdWJsaXNoSW50ZXJ2YWxTZWNzEi0KEnBvc2l0aW9uX3' + 'ByZWNpc2lvbhgCIAEoDVIRcG9zaXRpb25QcmVjaXNpb24SNAoWc2hvdWxkX3JlcG9ydF9sb2Nh' + 'dGlvbhgDIAEoCFIUc2hvdWxkUmVwb3J0TG9jYXRpb24aswEKFFJlbW90ZUhhcmR3YXJlQ29uZm' + 'lnEhgKB2VuYWJsZWQYASABKAhSB2VuYWJsZWQSOwoaYWxsb3dfdW5kZWZpbmVkX3Bpbl9hY2Nl' + 'c3MYAiABKAhSF2FsbG93VW5kZWZpbmVkUGluQWNjZXNzEkQKDmF2YWlsYWJsZV9waW5zGAMgAy' + 'gLMh0ubWVzaHRhc3RpYy5SZW1vdGVIYXJkd2FyZVBpblINYXZhaWxhYmxlUGlucxqFAQoSTmVp' + 'Z2hib3JJbmZvQ29uZmlnEhgKB2VuYWJsZWQYASABKAhSB2VuYWJsZWQSJwoPdXBkYXRlX2ludG' + 'VydmFsGAIgASgNUg51cGRhdGVJbnRlcnZhbBIsChJ0cmFuc21pdF9vdmVyX2xvcmEYAyABKAhS' + 'EHRyYW5zbWl0T3ZlckxvcmEahwQKFURldGVjdGlvblNlbnNvckNvbmZpZxIYCgdlbmFibGVkGA' + 'EgASgIUgdlbmFibGVkEjQKFm1pbmltdW1fYnJvYWRjYXN0X3NlY3MYAiABKA1SFG1pbmltdW1C' + 'cm9hZGNhc3RTZWNzEjAKFHN0YXRlX2Jyb2FkY2FzdF9zZWNzGAMgASgNUhJzdGF0ZUJyb2FkY2' + 'FzdFNlY3MSGwoJc2VuZF9iZWxsGAQgASgIUghzZW5kQmVsbBISCgRuYW1lGAUgASgJUgRuYW1l' + 'Eh8KC21vbml0b3JfcGluGAYgASgNUgptb25pdG9yUGluEnAKFmRldGVjdGlvbl90cmlnZ2VyX3' + 'R5cGUYByABKA4yOi5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5EZXRlY3Rpb25TZW5zb3JDb25m' + 'aWcuVHJpZ2dlclR5cGVSFGRldGVjdGlvblRyaWdnZXJUeXBlEh0KCnVzZV9wdWxsdXAYCCABKA' + 'hSCXVzZVB1bGx1cCKIAQoLVHJpZ2dlclR5cGUSDQoJTE9HSUNfTE9XEAASDgoKTE9HSUNfSElH' + 'SBABEhAKDEZBTExJTkdfRURHRRACEg8KC1JJU0lOR19FREdFEAMSGgoWRUlUSEVSX0VER0VfQU' + 'NUSVZFX0xPVxAEEhsKF0VJVEhFUl9FREdFX0FDVElWRV9ISUdIEAUaogMKC0F1ZGlvQ29uZmln' + 'EiUKDmNvZGVjMl9lbmFibGVkGAEgASgIUg1jb2RlYzJFbmFibGVkEhcKB3B0dF9waW4YAiABKA' + '1SBnB0dFBpbhJJCgdiaXRyYXRlGAMgASgOMi8ubWVzaHRhc3RpYy5Nb2R1bGVDb25maWcuQXVk' + 'aW9Db25maWcuQXVkaW9fQmF1ZFIHYml0cmF0ZRIVCgZpMnNfd3MYBCABKA1SBWkyc1dzEhUKBm' + 'kyc19zZBgFIAEoDVIFaTJzU2QSFwoHaTJzX2RpbhgGIAEoDVIGaTJzRGluEhcKB2kyc19zY2sY' + 'ByABKA1SBmkyc1NjayKnAQoKQXVkaW9fQmF1ZBISCg5DT0RFQzJfREVGQVVMVBAAEg8KC0NPRE' + 'VDMl8zMjAwEAESDwoLQ09ERUMyXzI0MDAQAhIPCgtDT0RFQzJfMTYwMBADEg8KC0NPREVDMl8x' + 'NDAwEAQSDwoLQ09ERUMyXzEzMDAQBRIPCgtDT0RFQzJfMTIwMBAGEg4KCkNPREVDMl83MDAQBx' + 'IPCgtDT0RFQzJfNzAwQhAIGrYBChBQYXhjb3VudGVyQ29uZmlnEhgKB2VuYWJsZWQYASABKAhS' + 'B2VuYWJsZWQSPAoacGF4Y291bnRlcl91cGRhdGVfaW50ZXJ2YWwYAiABKA1SGHBheGNvdW50ZX' + 'JVcGRhdGVJbnRlcnZhbBIlCg53aWZpX3RocmVzaG9sZBgDIAEoBVINd2lmaVRocmVzaG9sZBIj' + 'Cg1ibGVfdGhyZXNob2xkGAQgASgFUgxibGVUaHJlc2hvbGQa1QUKDFNlcmlhbENvbmZpZxIYCg' + 'dlbmFibGVkGAEgASgIUgdlbmFibGVkEhIKBGVjaG8YAiABKAhSBGVjaG8SEAoDcnhkGAMgASgN' + 'UgNyeGQSEAoDdHhkGAQgASgNUgN0eGQSRQoEYmF1ZBgFIAEoDjIxLm1lc2h0YXN0aWMuTW9kdW' + 'xlQ29uZmlnLlNlcmlhbENvbmZpZy5TZXJpYWxfQmF1ZFIEYmF1ZBIYCgd0aW1lb3V0GAYgASgN' + 'Ugd0aW1lb3V0EkUKBG1vZGUYByABKA4yMS5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5TZXJpYW' + 'xDb25maWcuU2VyaWFsX01vZGVSBG1vZGUSPwocb3ZlcnJpZGVfY29uc29sZV9zZXJpYWxfcG9y' + 'dBgIIAEoCFIZb3ZlcnJpZGVDb25zb2xlU2VyaWFsUG9ydCKKAgoLU2VyaWFsX0JhdWQSEAoMQk' + 'FVRF9ERUZBVUxUEAASDAoIQkFVRF8xMTAQARIMCghCQVVEXzMwMBACEgwKCEJBVURfNjAwEAMS' + 'DQoJQkFVRF8xMjAwEAQSDQoJQkFVRF8yNDAwEAUSDQoJQkFVRF80ODAwEAYSDQoJQkFVRF85Nj' + 'AwEAcSDgoKQkFVRF8xOTIwMBAIEg4KCkJBVURfMzg0MDAQCRIOCgpCQVVEXzU3NjAwEAoSDwoL' + 'QkFVRF8xMTUyMDAQCxIPCgtCQVVEXzIzMDQwMBAMEg8KC0JBVURfNDYwODAwEA0SDwoLQkFVRF' + '81NzYwMDAQDhIPCgtCQVVEXzkyMTYwMBAPIn0KC1NlcmlhbF9Nb2RlEgsKB0RFRkFVTFQQABIK' + 'CgZTSU1QTEUQARIJCgVQUk9UTxACEgsKB1RFWFRNU0cQAxIICgROTUVBEAQSCwoHQ0FMVE9QTx' + 'AFEggKBFdTODUQBhINCglWRV9ESVJFQ1QQBxINCglNU19DT05GSUcQCBqsBAoaRXh0ZXJuYWxO' + 'b3RpZmljYXRpb25Db25maWcSGAoHZW5hYmxlZBgBIAEoCFIHZW5hYmxlZBIbCglvdXRwdXRfbX' + 'MYAiABKA1SCG91dHB1dE1zEhYKBm91dHB1dBgDIAEoDVIGb3V0cHV0EiEKDG91dHB1dF92aWJy' + 'YRgIIAEoDVILb3V0cHV0VmlicmESIwoNb3V0cHV0X2J1enplchgJIAEoDVIMb3V0cHV0QnV6em' + 'VyEhYKBmFjdGl2ZRgEIAEoCFIGYWN0aXZlEiMKDWFsZXJ0X21lc3NhZ2UYBSABKAhSDGFsZXJ0' + 'TWVzc2FnZRIuChNhbGVydF9tZXNzYWdlX3ZpYnJhGAogASgIUhFhbGVydE1lc3NhZ2VWaWJyYR' + 'IwChRhbGVydF9tZXNzYWdlX2J1enplchgLIAEoCFISYWxlcnRNZXNzYWdlQnV6emVyEh0KCmFs' + 'ZXJ0X2JlbGwYBiABKAhSCWFsZXJ0QmVsbBIoChBhbGVydF9iZWxsX3ZpYnJhGAwgASgIUg5hbG' + 'VydEJlbGxWaWJyYRIqChFhbGVydF9iZWxsX2J1enplchgNIAEoCFIPYWxlcnRCZWxsQnV6emVy' + 'EhcKB3VzZV9wd20YByABKAhSBnVzZVB3bRIfCgtuYWdfdGltZW91dBgOIAEoDVIKbmFnVGltZW' + '91dBIpChF1c2VfaTJzX2FzX2J1enplchgPIAEoCFIOdXNlSTJzQXNCdXp6ZXIa5QEKElN0b3Jl' + 'Rm9yd2FyZENvbmZpZxIYCgdlbmFibGVkGAEgASgIUgdlbmFibGVkEhwKCWhlYXJ0YmVhdBgCIA' + 'EoCFIJaGVhcnRiZWF0EhgKB3JlY29yZHMYAyABKA1SB3JlY29yZHMSLAoSaGlzdG9yeV9yZXR1' + 'cm5fbWF4GAQgASgNUhBoaXN0b3J5UmV0dXJuTWF4EjIKFWhpc3RvcnlfcmV0dXJuX3dpbmRvdx' + 'gFIAEoDVITaGlzdG9yeVJldHVybldpbmRvdxIbCglpc19zZXJ2ZXIYBiABKAhSCGlzU2VydmVy' + 'GlcKD1JhbmdlVGVzdENvbmZpZxIYCgdlbmFibGVkGAEgASgIUgdlbmFibGVkEhYKBnNlbmRlch' + 'gCIAEoDVIGc2VuZGVyEhIKBHNhdmUYAyABKAhSBHNhdmUa/wUKD1RlbGVtZXRyeUNvbmZpZxI0' + 'ChZkZXZpY2VfdXBkYXRlX2ludGVydmFsGAEgASgNUhRkZXZpY2VVcGRhdGVJbnRlcnZhbBI+Ch' + 'tlbnZpcm9ubWVudF91cGRhdGVfaW50ZXJ2YWwYAiABKA1SGWVudmlyb25tZW50VXBkYXRlSW50' + 'ZXJ2YWwSRgofZW52aXJvbm1lbnRfbWVhc3VyZW1lbnRfZW5hYmxlZBgDIAEoCFIdZW52aXJvbm' + '1lbnRNZWFzdXJlbWVudEVuYWJsZWQSPAoaZW52aXJvbm1lbnRfc2NyZWVuX2VuYWJsZWQYBCAB' + 'KAhSGGVudmlyb25tZW50U2NyZWVuRW5hYmxlZBJECh5lbnZpcm9ubWVudF9kaXNwbGF5X2ZhaH' + 'JlbmhlaXQYBSABKAhSHGVudmlyb25tZW50RGlzcGxheUZhaHJlbmhlaXQSLgoTYWlyX3F1YWxp' + 'dHlfZW5hYmxlZBgGIAEoCFIRYWlyUXVhbGl0eUVuYWJsZWQSMAoUYWlyX3F1YWxpdHlfaW50ZX' + 'J2YWwYByABKA1SEmFpclF1YWxpdHlJbnRlcnZhbBI6Chlwb3dlcl9tZWFzdXJlbWVudF9lbmFi' + 'bGVkGAggASgIUhdwb3dlck1lYXN1cmVtZW50RW5hYmxlZBIyChVwb3dlcl91cGRhdGVfaW50ZX' + 'J2YWwYCSABKA1SE3Bvd2VyVXBkYXRlSW50ZXJ2YWwSMAoUcG93ZXJfc2NyZWVuX2VuYWJsZWQY' + 'CiABKAhSEnBvd2VyU2NyZWVuRW5hYmxlZBI8ChpoZWFsdGhfbWVhc3VyZW1lbnRfZW5hYmxlZB' + 'gLIAEoCFIYaGVhbHRoTWVhc3VyZW1lbnRFbmFibGVkEjQKFmhlYWx0aF91cGRhdGVfaW50ZXJ2' + 'YWwYDCABKA1SFGhlYWx0aFVwZGF0ZUludGVydmFsEjIKFWhlYWx0aF9zY3JlZW5fZW5hYmxlZB' + 'gNIAEoCFITaGVhbHRoU2NyZWVuRW5hYmxlZBqaBgoTQ2FubmVkTWVzc2FnZUNvbmZpZxInCg9y' + 'b3RhcnkxX2VuYWJsZWQYASABKAhSDnJvdGFyeTFFbmFibGVkEioKEWlucHV0YnJva2VyX3Bpbl' + '9hGAIgASgNUg9pbnB1dGJyb2tlclBpbkESKgoRaW5wdXRicm9rZXJfcGluX2IYAyABKA1SD2lu' + 'cHV0YnJva2VyUGluQhIyChVpbnB1dGJyb2tlcl9waW5fcHJlc3MYBCABKA1SE2lucHV0YnJva2' + 'VyUGluUHJlc3MSbQoUaW5wdXRicm9rZXJfZXZlbnRfY3cYBSABKA4yOy5tZXNodGFzdGljLk1v' + 'ZHVsZUNvbmZpZy5DYW5uZWRNZXNzYWdlQ29uZmlnLklucHV0RXZlbnRDaGFyUhJpbnB1dGJyb2' + 'tlckV2ZW50Q3cSbwoVaW5wdXRicm9rZXJfZXZlbnRfY2N3GAYgASgOMjsubWVzaHRhc3RpYy5N' + 'b2R1bGVDb25maWcuQ2FubmVkTWVzc2FnZUNvbmZpZy5JbnB1dEV2ZW50Q2hhclITaW5wdXRicm' + '9rZXJFdmVudENjdxJzChdpbnB1dGJyb2tlcl9ldmVudF9wcmVzcxgHIAEoDjI7Lm1lc2h0YXN0' + 'aWMuTW9kdWxlQ29uZmlnLkNhbm5lZE1lc3NhZ2VDb25maWcuSW5wdXRFdmVudENoYXJSFWlucH' + 'V0YnJva2VyRXZlbnRQcmVzcxInCg91cGRvd24xX2VuYWJsZWQYCCABKAhSDnVwZG93bjFFbmFi' + 'bGVkEhwKB2VuYWJsZWQYCSABKAhCAhgBUgdlbmFibGVkEjAKEmFsbG93X2lucHV0X3NvdXJjZR' + 'gKIAEoCUICGAFSEGFsbG93SW5wdXRTb3VyY2USGwoJc2VuZF9iZWxsGAsgASgIUghzZW5kQmVs' + 'bCJjCg5JbnB1dEV2ZW50Q2hhchIICgROT05FEAASBgoCVVAQERIICgRET1dOEBISCAoETEVGVB' + 'ATEgkKBVJJR0hUEBQSCgoGU0VMRUNUEAoSCAoEQkFDSxAbEgoKBkNBTkNFTBAYGooBChVBbWJp' + 'ZW50TGlnaHRpbmdDb25maWcSGwoJbGVkX3N0YXRlGAEgASgIUghsZWRTdGF0ZRIYCgdjdXJyZW' + '50GAIgASgNUgdjdXJyZW50EhAKA3JlZBgDIAEoDVIDcmVkEhQKBWdyZWVuGAQgASgNUgVncmVl' + 'bhISCgRibHVlGAUgASgNUgRibHVlQhEKD3BheWxvYWRfdmFyaWFudA=='); + +@$core.Deprecated('Use remoteHardwarePinDescriptor instead') +const RemoteHardwarePin$json = { + '1': 'RemoteHardwarePin', + '2': [ + {'1': 'gpio_pin', '3': 1, '4': 1, '5': 13, '10': 'gpioPin'}, + {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + { + '1': 'type', + '3': 3, + '4': 1, + '5': 14, + '6': '.meshtastic.RemoteHardwarePinType', + '10': 'type' + }, + ], +}; + +/// Descriptor for `RemoteHardwarePin`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List remoteHardwarePinDescriptor = $convert.base64Decode( + 'ChFSZW1vdGVIYXJkd2FyZVBpbhIZCghncGlvX3BpbhgBIAEoDVIHZ3Bpb1BpbhISCgRuYW1lGA' + 'IgASgJUgRuYW1lEjUKBHR5cGUYAyABKA4yIS5tZXNodGFzdGljLlJlbW90ZUhhcmR3YXJlUGlu' + 'VHlwZVIEdHlwZQ=='); diff --git a/third_party/meshtastic_flutter/lib/generated/mqtt.pb.dart b/third_party/meshtastic_flutter/lib/generated/mqtt.pb.dart new file mode 100644 index 000000000..8b6629b68 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/mqtt.pb.dart @@ -0,0 +1,383 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/mqtt.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'config.pbenum.dart' as $1; +import 'mesh.pb.dart' as $0; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// This message wraps a MeshPacket with extra metadata about the sender and how it arrived. +class ServiceEnvelope extends $pb.GeneratedMessage { + factory ServiceEnvelope({ + $0.MeshPacket? packet, + $core.String? channelId, + $core.String? gatewayId, + }) { + final result = create(); + if (packet != null) result.packet = packet; + if (channelId != null) result.channelId = channelId; + if (gatewayId != null) result.gatewayId = gatewayId; + return result; + } + + ServiceEnvelope._(); + + factory ServiceEnvelope.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ServiceEnvelope.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ServiceEnvelope', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOM<$0.MeshPacket>(1, _omitFieldNames ? '' : 'packet', + subBuilder: $0.MeshPacket.create) + ..aOS(2, _omitFieldNames ? '' : 'channelId') + ..aOS(3, _omitFieldNames ? '' : 'gatewayId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ServiceEnvelope clone() => ServiceEnvelope()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ServiceEnvelope copyWith(void Function(ServiceEnvelope) updates) => + super.copyWith((message) => updates(message as ServiceEnvelope)) + as ServiceEnvelope; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ServiceEnvelope create() => ServiceEnvelope._(); + @$core.override + ServiceEnvelope createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ServiceEnvelope getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ServiceEnvelope? _defaultInstance; + + /// + /// The (probably encrypted) packet + @$pb.TagNumber(1) + $0.MeshPacket get packet => $_getN(0); + @$pb.TagNumber(1) + set packet($0.MeshPacket value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasPacket() => $_has(0); + @$pb.TagNumber(1) + void clearPacket() => $_clearField(1); + @$pb.TagNumber(1) + $0.MeshPacket ensurePacket() => $_ensure(0); + + /// + /// The global channel ID it was sent on + @$pb.TagNumber(2) + $core.String get channelId => $_getSZ(1); + @$pb.TagNumber(2) + set channelId($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasChannelId() => $_has(1); + @$pb.TagNumber(2) + void clearChannelId() => $_clearField(2); + + /// + /// The sending gateway node ID. Can we use this to authenticate/prevent fake + /// nodeid impersonation for senders? - i.e. use gateway/mesh id (which is authenticated) + local node id as + /// the globally trusted nodenum + @$pb.TagNumber(3) + $core.String get gatewayId => $_getSZ(2); + @$pb.TagNumber(3) + set gatewayId($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasGatewayId() => $_has(2); + @$pb.TagNumber(3) + void clearGatewayId() => $_clearField(3); +} + +/// +/// Information about a node intended to be reported unencrypted to a map using MQTT. +class MapReport extends $pb.GeneratedMessage { + factory MapReport({ + $core.String? longName, + $core.String? shortName, + $1.Config_DeviceConfig_Role? role, + $0.HardwareModel? hwModel, + $core.String? firmwareVersion, + $1.Config_LoRaConfig_RegionCode? region, + $1.Config_LoRaConfig_ModemPreset? modemPreset, + $core.bool? hasDefaultChannel, + $core.int? latitudeI, + $core.int? longitudeI, + $core.int? altitude, + $core.int? positionPrecision, + $core.int? numOnlineLocalNodes, + $core.bool? hasOptedReportLocation, + }) { + final result = create(); + if (longName != null) result.longName = longName; + if (shortName != null) result.shortName = shortName; + if (role != null) result.role = role; + if (hwModel != null) result.hwModel = hwModel; + if (firmwareVersion != null) result.firmwareVersion = firmwareVersion; + if (region != null) result.region = region; + if (modemPreset != null) result.modemPreset = modemPreset; + if (hasDefaultChannel != null) result.hasDefaultChannel = hasDefaultChannel; + if (latitudeI != null) result.latitudeI = latitudeI; + if (longitudeI != null) result.longitudeI = longitudeI; + if (altitude != null) result.altitude = altitude; + if (positionPrecision != null) result.positionPrecision = positionPrecision; + if (numOnlineLocalNodes != null) + result.numOnlineLocalNodes = numOnlineLocalNodes; + if (hasOptedReportLocation != null) + result.hasOptedReportLocation = hasOptedReportLocation; + return result; + } + + MapReport._(); + + factory MapReport.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory MapReport.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'MapReport', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'longName') + ..aOS(2, _omitFieldNames ? '' : 'shortName') + ..e<$1.Config_DeviceConfig_Role>( + 3, _omitFieldNames ? '' : 'role', $pb.PbFieldType.OE, + defaultOrMaker: $1.Config_DeviceConfig_Role.CLIENT, + valueOf: $1.Config_DeviceConfig_Role.valueOf, + enumValues: $1.Config_DeviceConfig_Role.values) + ..e<$0.HardwareModel>( + 4, _omitFieldNames ? '' : 'hwModel', $pb.PbFieldType.OE, + defaultOrMaker: $0.HardwareModel.UNSET, + valueOf: $0.HardwareModel.valueOf, + enumValues: $0.HardwareModel.values) + ..aOS(5, _omitFieldNames ? '' : 'firmwareVersion') + ..e<$1.Config_LoRaConfig_RegionCode>( + 6, _omitFieldNames ? '' : 'region', $pb.PbFieldType.OE, + defaultOrMaker: $1.Config_LoRaConfig_RegionCode.UNSET, + valueOf: $1.Config_LoRaConfig_RegionCode.valueOf, + enumValues: $1.Config_LoRaConfig_RegionCode.values) + ..e<$1.Config_LoRaConfig_ModemPreset>( + 7, _omitFieldNames ? '' : 'modemPreset', $pb.PbFieldType.OE, + defaultOrMaker: $1.Config_LoRaConfig_ModemPreset.LONG_FAST, + valueOf: $1.Config_LoRaConfig_ModemPreset.valueOf, + enumValues: $1.Config_LoRaConfig_ModemPreset.values) + ..aOB(8, _omitFieldNames ? '' : 'hasDefaultChannel') + ..a<$core.int>(9, _omitFieldNames ? '' : 'latitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>( + 10, _omitFieldNames ? '' : 'longitudeI', $pb.PbFieldType.OSF3) + ..a<$core.int>(11, _omitFieldNames ? '' : 'altitude', $pb.PbFieldType.O3) + ..a<$core.int>( + 12, _omitFieldNames ? '' : 'positionPrecision', $pb.PbFieldType.OU3) + ..a<$core.int>( + 13, _omitFieldNames ? '' : 'numOnlineLocalNodes', $pb.PbFieldType.OU3) + ..aOB(14, _omitFieldNames ? '' : 'hasOptedReportLocation') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MapReport clone() => MapReport()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + MapReport copyWith(void Function(MapReport) updates) => + super.copyWith((message) => updates(message as MapReport)) as MapReport; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static MapReport create() => MapReport._(); + @$core.override + MapReport createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static MapReport getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static MapReport? _defaultInstance; + + /// + /// A full name for this user, i.e. "Kevin Hester" + @$pb.TagNumber(1) + $core.String get longName => $_getSZ(0); + @$pb.TagNumber(1) + set longName($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasLongName() => $_has(0); + @$pb.TagNumber(1) + void clearLongName() => $_clearField(1); + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + @$pb.TagNumber(2) + $core.String get shortName => $_getSZ(1); + @$pb.TagNumber(2) + set shortName($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasShortName() => $_has(1); + @$pb.TagNumber(2) + void clearShortName() => $_clearField(2); + + /// + /// Role of the node that applies specific settings for a particular use-case + @$pb.TagNumber(3) + $1.Config_DeviceConfig_Role get role => $_getN(2); + @$pb.TagNumber(3) + set role($1.Config_DeviceConfig_Role value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasRole() => $_has(2); + @$pb.TagNumber(3) + void clearRole() => $_clearField(3); + + /// + /// Hardware model of the node, i.e. T-Beam, Heltec V3, etc... + @$pb.TagNumber(4) + $0.HardwareModel get hwModel => $_getN(3); + @$pb.TagNumber(4) + set hwModel($0.HardwareModel value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasHwModel() => $_has(3); + @$pb.TagNumber(4) + void clearHwModel() => $_clearField(4); + + /// + /// Device firmware version string + @$pb.TagNumber(5) + $core.String get firmwareVersion => $_getSZ(4); + @$pb.TagNumber(5) + set firmwareVersion($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasFirmwareVersion() => $_has(4); + @$pb.TagNumber(5) + void clearFirmwareVersion() => $_clearField(5); + + /// + /// The region code for the radio (US, CN, EU433, etc...) + @$pb.TagNumber(6) + $1.Config_LoRaConfig_RegionCode get region => $_getN(5); + @$pb.TagNumber(6) + set region($1.Config_LoRaConfig_RegionCode value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasRegion() => $_has(5); + @$pb.TagNumber(6) + void clearRegion() => $_clearField(6); + + /// + /// Modem preset used by the radio (LongFast, MediumSlow, etc...) + @$pb.TagNumber(7) + $1.Config_LoRaConfig_ModemPreset get modemPreset => $_getN(6); + @$pb.TagNumber(7) + set modemPreset($1.Config_LoRaConfig_ModemPreset value) => + $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasModemPreset() => $_has(6); + @$pb.TagNumber(7) + void clearModemPreset() => $_clearField(7); + + /// + /// Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...) + /// and it uses the default frequency slot given the region and modem preset. + @$pb.TagNumber(8) + $core.bool get hasDefaultChannel => $_getBF(7); + @$pb.TagNumber(8) + set hasDefaultChannel($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(8) + $core.bool hasHasDefaultChannel() => $_has(7); + @$pb.TagNumber(8) + void clearHasDefaultChannel() => $_clearField(8); + + /// + /// Latitude: multiply by 1e-7 to get degrees in floating point + @$pb.TagNumber(9) + $core.int get latitudeI => $_getIZ(8); + @$pb.TagNumber(9) + set latitudeI($core.int value) => $_setSignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasLatitudeI() => $_has(8); + @$pb.TagNumber(9) + void clearLatitudeI() => $_clearField(9); + + /// + /// Longitude: multiply by 1e-7 to get degrees in floating point + @$pb.TagNumber(10) + $core.int get longitudeI => $_getIZ(9); + @$pb.TagNumber(10) + set longitudeI($core.int value) => $_setSignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasLongitudeI() => $_has(9); + @$pb.TagNumber(10) + void clearLongitudeI() => $_clearField(10); + + /// + /// Altitude in meters above MSL + @$pb.TagNumber(11) + $core.int get altitude => $_getIZ(10); + @$pb.TagNumber(11) + set altitude($core.int value) => $_setSignedInt32(10, value); + @$pb.TagNumber(11) + $core.bool hasAltitude() => $_has(10); + @$pb.TagNumber(11) + void clearAltitude() => $_clearField(11); + + /// + /// Indicates the bits of precision for latitude and longitude set by the sending node + @$pb.TagNumber(12) + $core.int get positionPrecision => $_getIZ(11); + @$pb.TagNumber(12) + set positionPrecision($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasPositionPrecision() => $_has(11); + @$pb.TagNumber(12) + void clearPositionPrecision() => $_clearField(12); + + /// + /// Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) + @$pb.TagNumber(13) + $core.int get numOnlineLocalNodes => $_getIZ(12); + @$pb.TagNumber(13) + set numOnlineLocalNodes($core.int value) => $_setUnsignedInt32(12, value); + @$pb.TagNumber(13) + $core.bool hasNumOnlineLocalNodes() => $_has(12); + @$pb.TagNumber(13) + void clearNumOnlineLocalNodes() => $_clearField(13); + + /// + /// User has opted in to share their location (map report) with the mqtt server + /// Controlled by map_report.should_report_location + @$pb.TagNumber(14) + $core.bool get hasOptedReportLocation => $_getBF(13); + @$pb.TagNumber(14) + set hasOptedReportLocation($core.bool value) => $_setBool(13, value); + @$pb.TagNumber(14) + $core.bool hasHasOptedReportLocation() => $_has(13); + @$pb.TagNumber(14) + void clearHasOptedReportLocation() => $_clearField(14); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/mqtt.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/mqtt.pbenum.dart new file mode 100644 index 000000000..7179565a6 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/mqtt.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/mqtt.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/mqtt.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/mqtt.pbjson.dart new file mode 100644 index 000000000..e8cefbe96 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/mqtt.pbjson.dart @@ -0,0 +1,127 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/mqtt.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use serviceEnvelopeDescriptor instead') +const ServiceEnvelope$json = { + '1': 'ServiceEnvelope', + '2': [ + { + '1': 'packet', + '3': 1, + '4': 1, + '5': 11, + '6': '.meshtastic.MeshPacket', + '10': 'packet' + }, + {'1': 'channel_id', '3': 2, '4': 1, '5': 9, '10': 'channelId'}, + {'1': 'gateway_id', '3': 3, '4': 1, '5': 9, '10': 'gatewayId'}, + ], +}; + +/// Descriptor for `ServiceEnvelope`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List serviceEnvelopeDescriptor = $convert.base64Decode( + 'Cg9TZXJ2aWNlRW52ZWxvcGUSLgoGcGFja2V0GAEgASgLMhYubWVzaHRhc3RpYy5NZXNoUGFja2' + 'V0UgZwYWNrZXQSHQoKY2hhbm5lbF9pZBgCIAEoCVIJY2hhbm5lbElkEh0KCmdhdGV3YXlfaWQY' + 'AyABKAlSCWdhdGV3YXlJZA=='); + +@$core.Deprecated('Use mapReportDescriptor instead') +const MapReport$json = { + '1': 'MapReport', + '2': [ + {'1': 'long_name', '3': 1, '4': 1, '5': 9, '10': 'longName'}, + {'1': 'short_name', '3': 2, '4': 1, '5': 9, '10': 'shortName'}, + { + '1': 'role', + '3': 3, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.DeviceConfig.Role', + '10': 'role' + }, + { + '1': 'hw_model', + '3': 4, + '4': 1, + '5': 14, + '6': '.meshtastic.HardwareModel', + '10': 'hwModel' + }, + {'1': 'firmware_version', '3': 5, '4': 1, '5': 9, '10': 'firmwareVersion'}, + { + '1': 'region', + '3': 6, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.LoRaConfig.RegionCode', + '10': 'region' + }, + { + '1': 'modem_preset', + '3': 7, + '4': 1, + '5': 14, + '6': '.meshtastic.Config.LoRaConfig.ModemPreset', + '10': 'modemPreset' + }, + { + '1': 'has_default_channel', + '3': 8, + '4': 1, + '5': 8, + '10': 'hasDefaultChannel' + }, + {'1': 'latitude_i', '3': 9, '4': 1, '5': 15, '10': 'latitudeI'}, + {'1': 'longitude_i', '3': 10, '4': 1, '5': 15, '10': 'longitudeI'}, + {'1': 'altitude', '3': 11, '4': 1, '5': 5, '10': 'altitude'}, + { + '1': 'position_precision', + '3': 12, + '4': 1, + '5': 13, + '10': 'positionPrecision' + }, + { + '1': 'num_online_local_nodes', + '3': 13, + '4': 1, + '5': 13, + '10': 'numOnlineLocalNodes' + }, + { + '1': 'has_opted_report_location', + '3': 14, + '4': 1, + '5': 8, + '10': 'hasOptedReportLocation' + }, + ], +}; + +/// Descriptor for `MapReport`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List mapReportDescriptor = $convert.base64Decode( + 'CglNYXBSZXBvcnQSGwoJbG9uZ19uYW1lGAEgASgJUghsb25nTmFtZRIdCgpzaG9ydF9uYW1lGA' + 'IgASgJUglzaG9ydE5hbWUSOAoEcm9sZRgDIAEoDjIkLm1lc2h0YXN0aWMuQ29uZmlnLkRldmlj' + 'ZUNvbmZpZy5Sb2xlUgRyb2xlEjQKCGh3X21vZGVsGAQgASgOMhkubWVzaHRhc3RpYy5IYXJkd2' + 'FyZU1vZGVsUgdod01vZGVsEikKEGZpcm13YXJlX3ZlcnNpb24YBSABKAlSD2Zpcm13YXJlVmVy' + 'c2lvbhJACgZyZWdpb24YBiABKA4yKC5tZXNodGFzdGljLkNvbmZpZy5Mb1JhQ29uZmlnLlJlZ2' + 'lvbkNvZGVSBnJlZ2lvbhJMCgxtb2RlbV9wcmVzZXQYByABKA4yKS5tZXNodGFzdGljLkNvbmZp' + 'Zy5Mb1JhQ29uZmlnLk1vZGVtUHJlc2V0Ugttb2RlbVByZXNldBIuChNoYXNfZGVmYXVsdF9jaG' + 'FubmVsGAggASgIUhFoYXNEZWZhdWx0Q2hhbm5lbBIdCgpsYXRpdHVkZV9pGAkgASgPUglsYXRp' + 'dHVkZUkSHwoLbG9uZ2l0dWRlX2kYCiABKA9SCmxvbmdpdHVkZUkSGgoIYWx0aXR1ZGUYCyABKA' + 'VSCGFsdGl0dWRlEi0KEnBvc2l0aW9uX3ByZWNpc2lvbhgMIAEoDVIRcG9zaXRpb25QcmVjaXNp' + 'b24SMwoWbnVtX29ubGluZV9sb2NhbF9ub2RlcxgNIAEoDVITbnVtT25saW5lTG9jYWxOb2Rlcx' + 'I5ChloYXNfb3B0ZWRfcmVwb3J0X2xvY2F0aW9uGA4gASgIUhZoYXNPcHRlZFJlcG9ydExvY2F0' + 'aW9u'); diff --git a/third_party/meshtastic_flutter/lib/generated/paxcount.pb.dart b/third_party/meshtastic_flutter/lib/generated/paxcount.pb.dart new file mode 100644 index 000000000..16986662f --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/paxcount.pb.dart @@ -0,0 +1,108 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/paxcount.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// TODO: REPLACE +class Paxcount extends $pb.GeneratedMessage { + factory Paxcount({ + $core.int? wifi, + $core.int? ble, + $core.int? uptime, + }) { + final result = create(); + if (wifi != null) result.wifi = wifi; + if (ble != null) result.ble = ble; + if (uptime != null) result.uptime = uptime; + return result; + } + + Paxcount._(); + + factory Paxcount.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Paxcount.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Paxcount', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'wifi', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'ble', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'uptime', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Paxcount clone() => Paxcount()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Paxcount copyWith(void Function(Paxcount) updates) => + super.copyWith((message) => updates(message as Paxcount)) as Paxcount; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Paxcount create() => Paxcount._(); + @$core.override + Paxcount createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Paxcount getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Paxcount? _defaultInstance; + + /// + /// seen Wifi devices + @$pb.TagNumber(1) + $core.int get wifi => $_getIZ(0); + @$pb.TagNumber(1) + set wifi($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasWifi() => $_has(0); + @$pb.TagNumber(1) + void clearWifi() => $_clearField(1); + + /// + /// Seen BLE devices + @$pb.TagNumber(2) + $core.int get ble => $_getIZ(1); + @$pb.TagNumber(2) + set ble($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasBle() => $_has(1); + @$pb.TagNumber(2) + void clearBle() => $_clearField(2); + + /// + /// Uptime in seconds + @$pb.TagNumber(3) + $core.int get uptime => $_getIZ(2); + @$pb.TagNumber(3) + set uptime($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasUptime() => $_has(2); + @$pb.TagNumber(3) + void clearUptime() => $_clearField(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/paxcount.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/paxcount.pbenum.dart new file mode 100644 index 000000000..adcffb46a --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/paxcount.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/paxcount.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/paxcount.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/paxcount.pbjson.dart new file mode 100644 index 000000000..c93315acb --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/paxcount.pbjson.dart @@ -0,0 +1,30 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/paxcount.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use paxcountDescriptor instead') +const Paxcount$json = { + '1': 'Paxcount', + '2': [ + {'1': 'wifi', '3': 1, '4': 1, '5': 13, '10': 'wifi'}, + {'1': 'ble', '3': 2, '4': 1, '5': 13, '10': 'ble'}, + {'1': 'uptime', '3': 3, '4': 1, '5': 13, '10': 'uptime'}, + ], +}; + +/// Descriptor for `Paxcount`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List paxcountDescriptor = $convert.base64Decode( + 'CghQYXhjb3VudBISCgR3aWZpGAEgASgNUgR3aWZpEhAKA2JsZRgCIAEoDVIDYmxlEhYKBnVwdG' + 'ltZRgDIAEoDVIGdXB0aW1l'); diff --git a/third_party/meshtastic_flutter/lib/generated/portnums.pb.dart b/third_party/meshtastic_flutter/lib/generated/portnums.pb.dart new file mode 100644 index 000000000..cf3f9c412 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/portnums.pb.dart @@ -0,0 +1,17 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/portnums.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'portnums.pbenum.dart'; diff --git a/third_party/meshtastic_flutter/lib/generated/portnums.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/portnums.pbenum.dart new file mode 100644 index 000000000..f7ce7c520 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/portnums.pbenum.dart @@ -0,0 +1,291 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/portnums.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// For any new 'apps' that run on the device or via sister apps on phones/PCs they should pick and use a +/// unique 'portnum' for their application. +/// If you are making a new app using meshtastic, please send in a pull request to add your 'portnum' to this +/// master table. +/// PortNums should be assigned in the following range: +/// 0-63 Core Meshtastic use, do not use for third party apps +/// 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application +/// 256-511 Use one of these portnums for your private applications that you don't want to register publically +/// All other values are reserved. +/// Note: This was formerly a Type enum named 'typ' with the same id # +/// We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. +/// This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. +class PortNum extends $pb.ProtobufEnum { + /// + /// Deprecated: do not use in new code (formerly called OPAQUE) + /// A message sent from a device outside of the mesh, in a form the mesh does not understand + /// NOTE: This must be 0, because it is documented in IMeshService.aidl to be so + /// ENCODING: binary undefined + static const PortNum UNKNOWN_APP = + PortNum._(0, _omitEnumNames ? '' : 'UNKNOWN_APP'); + + /// + /// A simple UTF-8 text message, which even the little micros in the mesh + /// can understand and show on their screen eventually in some circumstances + /// even signal might send messages in this form (see below) + /// ENCODING: UTF-8 Plaintext (?) + static const PortNum TEXT_MESSAGE_APP = + PortNum._(1, _omitEnumNames ? '' : 'TEXT_MESSAGE_APP'); + + /// + /// Reserved for built-in GPIO/example app. + /// See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number + /// ENCODING: Protobuf + static const PortNum REMOTE_HARDWARE_APP = + PortNum._(2, _omitEnumNames ? '' : 'REMOTE_HARDWARE_APP'); + + /// + /// The built-in position messaging app. + /// Payload is a Position message. + /// ENCODING: Protobuf + static const PortNum POSITION_APP = + PortNum._(3, _omitEnumNames ? '' : 'POSITION_APP'); + + /// + /// The built-in user info app. + /// Payload is a User message. + /// ENCODING: Protobuf + static const PortNum NODEINFO_APP = + PortNum._(4, _omitEnumNames ? '' : 'NODEINFO_APP'); + + /// + /// Protocol control packets for mesh protocol use. + /// Payload is a Routing message. + /// ENCODING: Protobuf + static const PortNum ROUTING_APP = + PortNum._(5, _omitEnumNames ? '' : 'ROUTING_APP'); + + /// + /// Admin control packets. + /// Payload is a AdminMessage message. + /// ENCODING: Protobuf + static const PortNum ADMIN_APP = + PortNum._(6, _omitEnumNames ? '' : 'ADMIN_APP'); + + /// + /// Compressed TEXT_MESSAGE payloads. + /// ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression + /// NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed + /// payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress + /// any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP. + static const PortNum TEXT_MESSAGE_COMPRESSED_APP = + PortNum._(7, _omitEnumNames ? '' : 'TEXT_MESSAGE_COMPRESSED_APP'); + + /// + /// Waypoint payloads. + /// Payload is a Waypoint message. + /// ENCODING: Protobuf + static const PortNum WAYPOINT_APP = + PortNum._(8, _omitEnumNames ? '' : 'WAYPOINT_APP'); + + /// + /// Audio Payloads. + /// Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now + /// ENCODING: codec2 audio frames + /// NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate. + /// This marker comes from the 'moduleConfig.audio.bitrate' enum minus one. + static const PortNum AUDIO_APP = + PortNum._(9, _omitEnumNames ? '' : 'AUDIO_APP'); + + /// + /// Same as Text Message but originating from Detection Sensor Module. + /// NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 + static const PortNum DETECTION_SENSOR_APP = + PortNum._(10, _omitEnumNames ? '' : 'DETECTION_SENSOR_APP'); + + /// + /// Same as Text Message but used for critical alerts. + static const PortNum ALERT_APP = + PortNum._(11, _omitEnumNames ? '' : 'ALERT_APP'); + + /// + /// Module/port for handling key verification requests. + static const PortNum KEY_VERIFICATION_APP = + PortNum._(12, _omitEnumNames ? '' : 'KEY_VERIFICATION_APP'); + + /// + /// Provides a 'ping' service that replies to any packet it receives. + /// Also serves as a small example module. + /// ENCODING: ASCII Plaintext + static const PortNum REPLY_APP = + PortNum._(32, _omitEnumNames ? '' : 'REPLY_APP'); + + /// + /// Used for the python IP tunnel feature + /// ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on. + static const PortNum IP_TUNNEL_APP = + PortNum._(33, _omitEnumNames ? '' : 'IP_TUNNEL_APP'); + + /// + /// Paxcounter lib included in the firmware + /// ENCODING: protobuf + static const PortNum PAXCOUNTER_APP = + PortNum._(34, _omitEnumNames ? '' : 'PAXCOUNTER_APP'); + + /// + /// Provides a hardware serial interface to send and receive from the Meshtastic network. + /// Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic + /// network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. + /// Maximum packet size of 240 bytes. + /// Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp. + /// ENCODING: binary undefined + static const PortNum SERIAL_APP = + PortNum._(64, _omitEnumNames ? '' : 'SERIAL_APP'); + + /// + /// STORE_FORWARD_APP (Work in Progress) + /// Maintained by Jm Casler (MC Hamster) : jm@casler.org + /// ENCODING: Protobuf + static const PortNum STORE_FORWARD_APP = + PortNum._(65, _omitEnumNames ? '' : 'STORE_FORWARD_APP'); + + /// + /// Optional port for messages for the range test module. + /// ENCODING: ASCII Plaintext + /// NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 + static const PortNum RANGE_TEST_APP = + PortNum._(66, _omitEnumNames ? '' : 'RANGE_TEST_APP'); + + /// + /// Provides a format to send and receive telemetry data from the Meshtastic network. + /// Maintained by Charles Crossan (crossan007) : crossan007@gmail.com + /// ENCODING: Protobuf + static const PortNum TELEMETRY_APP = + PortNum._(67, _omitEnumNames ? '' : 'TELEMETRY_APP'); + + /// + /// Experimental tools for estimating node position without a GPS + /// Maintained by Github user a-f-G-U-C (a Meshtastic contributor) + /// Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS + /// ENCODING: arrays of int64 fields + static const PortNum ZPS_APP = PortNum._(68, _omitEnumNames ? '' : 'ZPS_APP'); + + /// + /// Used to let multiple instances of Linux native applications communicate + /// as if they did using their LoRa chip. + /// Maintained by GitHub user GUVWAF. + /// Project files at https://github.com/GUVWAF/Meshtasticator + /// ENCODING: Protobuf (?) + static const PortNum SIMULATOR_APP = + PortNum._(69, _omitEnumNames ? '' : 'SIMULATOR_APP'); + + /// + /// Provides a traceroute functionality to show the route a packet towards + /// a certain destination would take on the mesh. Contains a RouteDiscovery message as payload. + /// ENCODING: Protobuf + static const PortNum TRACEROUTE_APP = + PortNum._(70, _omitEnumNames ? '' : 'TRACEROUTE_APP'); + + /// + /// Aggregates edge info for the network by sending out a list of each node's neighbors + /// ENCODING: Protobuf + static const PortNum NEIGHBORINFO_APP = + PortNum._(71, _omitEnumNames ? '' : 'NEIGHBORINFO_APP'); + + /// + /// ATAK Plugin + /// Portnum for payloads from the official Meshtastic ATAK plugin + static const PortNum ATAK_PLUGIN = + PortNum._(72, _omitEnumNames ? '' : 'ATAK_PLUGIN'); + + /// + /// Provides unencrypted information about a node for consumption by a map via MQTT + static const PortNum MAP_REPORT_APP = + PortNum._(73, _omitEnumNames ? '' : 'MAP_REPORT_APP'); + + /// + /// PowerStress based monitoring support (for automated power consumption testing) + static const PortNum POWERSTRESS_APP = + PortNum._(74, _omitEnumNames ? '' : 'POWERSTRESS_APP'); + + /// + /// Reticulum Network Stack Tunnel App + /// ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface + static const PortNum RETICULUM_TUNNEL_APP = + PortNum._(76, _omitEnumNames ? '' : 'RETICULUM_TUNNEL_APP'); + + /// + /// App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send + /// arbitrary telemetry over meshtastic that is not covered by telemetry.proto + /// ENCODING: CayenneLLP + static const PortNum CAYENNE_APP = + PortNum._(77, _omitEnumNames ? '' : 'CAYENNE_APP'); + + /// + /// Private applications should use portnums >= 256. + /// To simplify initial development and testing you can use "PRIVATE_APP" + /// in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) + static const PortNum PRIVATE_APP = + PortNum._(256, _omitEnumNames ? '' : 'PRIVATE_APP'); + + /// + /// ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder + /// ENCODING: libcotshrink + static const PortNum ATAK_FORWARDER = + PortNum._(257, _omitEnumNames ? '' : 'ATAK_FORWARDER'); + + /// + /// Currently we limit port nums to no higher than this value + static const PortNum MAX = PortNum._(511, _omitEnumNames ? '' : 'MAX'); + + static const $core.List values = [ + UNKNOWN_APP, + TEXT_MESSAGE_APP, + REMOTE_HARDWARE_APP, + POSITION_APP, + NODEINFO_APP, + ROUTING_APP, + ADMIN_APP, + TEXT_MESSAGE_COMPRESSED_APP, + WAYPOINT_APP, + AUDIO_APP, + DETECTION_SENSOR_APP, + ALERT_APP, + KEY_VERIFICATION_APP, + REPLY_APP, + IP_TUNNEL_APP, + PAXCOUNTER_APP, + SERIAL_APP, + STORE_FORWARD_APP, + RANGE_TEST_APP, + TELEMETRY_APP, + ZPS_APP, + SIMULATOR_APP, + TRACEROUTE_APP, + NEIGHBORINFO_APP, + ATAK_PLUGIN, + MAP_REPORT_APP, + POWERSTRESS_APP, + RETICULUM_TUNNEL_APP, + CAYENNE_APP, + PRIVATE_APP, + ATAK_FORWARDER, + MAX, + ]; + + static final $core.Map<$core.int, PortNum> _byValue = + $pb.ProtobufEnum.initByValue(values); + static PortNum? valueOf($core.int value) => _byValue[value]; + + const PortNum._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/portnums.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/portnums.pbjson.dart new file mode 100644 index 000000000..c3e832f4f --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/portnums.pbjson.dart @@ -0,0 +1,69 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/portnums.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use portNumDescriptor instead') +const PortNum$json = { + '1': 'PortNum', + '2': [ + {'1': 'UNKNOWN_APP', '2': 0}, + {'1': 'TEXT_MESSAGE_APP', '2': 1}, + {'1': 'REMOTE_HARDWARE_APP', '2': 2}, + {'1': 'POSITION_APP', '2': 3}, + {'1': 'NODEINFO_APP', '2': 4}, + {'1': 'ROUTING_APP', '2': 5}, + {'1': 'ADMIN_APP', '2': 6}, + {'1': 'TEXT_MESSAGE_COMPRESSED_APP', '2': 7}, + {'1': 'WAYPOINT_APP', '2': 8}, + {'1': 'AUDIO_APP', '2': 9}, + {'1': 'DETECTION_SENSOR_APP', '2': 10}, + {'1': 'ALERT_APP', '2': 11}, + {'1': 'KEY_VERIFICATION_APP', '2': 12}, + {'1': 'REPLY_APP', '2': 32}, + {'1': 'IP_TUNNEL_APP', '2': 33}, + {'1': 'PAXCOUNTER_APP', '2': 34}, + {'1': 'SERIAL_APP', '2': 64}, + {'1': 'STORE_FORWARD_APP', '2': 65}, + {'1': 'RANGE_TEST_APP', '2': 66}, + {'1': 'TELEMETRY_APP', '2': 67}, + {'1': 'ZPS_APP', '2': 68}, + {'1': 'SIMULATOR_APP', '2': 69}, + {'1': 'TRACEROUTE_APP', '2': 70}, + {'1': 'NEIGHBORINFO_APP', '2': 71}, + {'1': 'ATAK_PLUGIN', '2': 72}, + {'1': 'MAP_REPORT_APP', '2': 73}, + {'1': 'POWERSTRESS_APP', '2': 74}, + {'1': 'RETICULUM_TUNNEL_APP', '2': 76}, + {'1': 'CAYENNE_APP', '2': 77}, + {'1': 'PRIVATE_APP', '2': 256}, + {'1': 'ATAK_FORWARDER', '2': 257}, + {'1': 'MAX', '2': 511}, + ], +}; + +/// Descriptor for `PortNum`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List portNumDescriptor = $convert.base64Decode( + 'CgdQb3J0TnVtEg8KC1VOS05PV05fQVBQEAASFAoQVEVYVF9NRVNTQUdFX0FQUBABEhcKE1JFTU' + '9URV9IQVJEV0FSRV9BUFAQAhIQCgxQT1NJVElPTl9BUFAQAxIQCgxOT0RFSU5GT19BUFAQBBIP' + 'CgtST1VUSU5HX0FQUBAFEg0KCUFETUlOX0FQUBAGEh8KG1RFWFRfTUVTU0FHRV9DT01QUkVTU0' + 'VEX0FQUBAHEhAKDFdBWVBPSU5UX0FQUBAIEg0KCUFVRElPX0FQUBAJEhgKFERFVEVDVElPTl9T' + 'RU5TT1JfQVBQEAoSDQoJQUxFUlRfQVBQEAsSGAoUS0VZX1ZFUklGSUNBVElPTl9BUFAQDBINCg' + 'lSRVBMWV9BUFAQIBIRCg1JUF9UVU5ORUxfQVBQECESEgoOUEFYQ09VTlRFUl9BUFAQIhIOCgpT' + 'RVJJQUxfQVBQEEASFQoRU1RPUkVfRk9SV0FSRF9BUFAQQRISCg5SQU5HRV9URVNUX0FQUBBCEh' + 'EKDVRFTEVNRVRSWV9BUFAQQxILCgdaUFNfQVBQEEQSEQoNU0lNVUxBVE9SX0FQUBBFEhIKDlRS' + 'QUNFUk9VVEVfQVBQEEYSFAoQTkVJR0hCT1JJTkZPX0FQUBBHEg8KC0FUQUtfUExVR0lOEEgSEg' + 'oOTUFQX1JFUE9SVF9BUFAQSRITCg9QT1dFUlNUUkVTU19BUFAQShIYChRSRVRJQ1VMVU1fVFVO' + 'TkVMX0FQUBBMEg8KC0NBWUVOTkVfQVBQEE0SEAoLUFJJVkFURV9BUFAQgAISEwoOQVRBS19GT1' + 'JXQVJERVIQgQISCAoDTUFYEP8D'); diff --git a/third_party/meshtastic_flutter/lib/generated/powermon.pb.dart b/third_party/meshtastic_flutter/lib/generated/powermon.pb.dart new file mode 100644 index 000000000..5ac768e78 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/powermon.pb.dart @@ -0,0 +1,143 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/powermon.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'powermon.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'powermon.pbenum.dart'; + +/// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). +/// But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) +class PowerMon extends $pb.GeneratedMessage { + factory PowerMon() => create(); + + PowerMon._(); + + factory PowerMon.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory PowerMon.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'PowerMon', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PowerMon clone() => PowerMon()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PowerMon copyWith(void Function(PowerMon) updates) => + super.copyWith((message) => updates(message as PowerMon)) as PowerMon; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PowerMon create() => PowerMon._(); + @$core.override + PowerMon createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PowerMon getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PowerMon? _defaultInstance; +} + +/// +/// PowerStress testing support via the C++ PowerStress module +class PowerStressMessage extends $pb.GeneratedMessage { + factory PowerStressMessage({ + PowerStressMessage_Opcode? cmd, + $core.double? numSeconds, + }) { + final result = create(); + if (cmd != null) result.cmd = cmd; + if (numSeconds != null) result.numSeconds = numSeconds; + return result; + } + + PowerStressMessage._(); + + factory PowerStressMessage.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory PowerStressMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'PowerStressMessage', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e( + 1, _omitFieldNames ? '' : 'cmd', $pb.PbFieldType.OE, + defaultOrMaker: PowerStressMessage_Opcode.UNSET, + valueOf: PowerStressMessage_Opcode.valueOf, + enumValues: PowerStressMessage_Opcode.values) + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'numSeconds', $pb.PbFieldType.OF) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PowerStressMessage clone() => PowerStressMessage()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PowerStressMessage copyWith(void Function(PowerStressMessage) updates) => + super.copyWith((message) => updates(message as PowerStressMessage)) + as PowerStressMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PowerStressMessage create() => PowerStressMessage._(); + @$core.override + PowerStressMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PowerStressMessage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static PowerStressMessage? _defaultInstance; + + /// + /// What type of HardwareMessage is this? + @$pb.TagNumber(1) + PowerStressMessage_Opcode get cmd => $_getN(0); + @$pb.TagNumber(1) + set cmd(PowerStressMessage_Opcode value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasCmd() => $_has(0); + @$pb.TagNumber(1) + void clearCmd() => $_clearField(1); + + @$pb.TagNumber(2) + $core.double get numSeconds => $_getN(1); + @$pb.TagNumber(2) + set numSeconds($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasNumSeconds() => $_has(1); + @$pb.TagNumber(2) + void clearNumSeconds() => $_clearField(2); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/powermon.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/powermon.pbenum.dart new file mode 100644 index 000000000..13ed7df00 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/powermon.pbenum.dart @@ -0,0 +1,164 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/powermon.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// Any significant power changing event in meshtastic should be tagged with a powermon state transition. +/// If you are making new meshtastic features feel free to add new entries at the end of this definition. +class PowerMon_State extends $pb.ProtobufEnum { + static const PowerMon_State None = + PowerMon_State._(0, _omitEnumNames ? '' : 'None'); + static const PowerMon_State CPU_DeepSleep = + PowerMon_State._(1, _omitEnumNames ? '' : 'CPU_DeepSleep'); + static const PowerMon_State CPU_LightSleep = + PowerMon_State._(2, _omitEnumNames ? '' : 'CPU_LightSleep'); + + /// + /// The external Vext1 power is on. Many boards have auxillary power rails that the CPU turns on only + /// occasionally. In cases where that rail has multiple devices on it we usually want to have logging on + /// the state of that rail as an independent record. + /// For instance on the Heltec Tracker 1.1 board, this rail is the power source for the GPS and screen. + /// + /// The log messages will be short and complete (see PowerMon.Event in the protobufs for details). + /// something like "S:PM:C,0x00001234,REASON" where the hex number is the bitmask of all current states. + /// (We use a bitmask for states so that if a log message gets lost it won't be fatal) + static const PowerMon_State Vext1_On = + PowerMon_State._(4, _omitEnumNames ? '' : 'Vext1_On'); + static const PowerMon_State Lora_RXOn = + PowerMon_State._(8, _omitEnumNames ? '' : 'Lora_RXOn'); + static const PowerMon_State Lora_TXOn = + PowerMon_State._(16, _omitEnumNames ? '' : 'Lora_TXOn'); + static const PowerMon_State Lora_RXActive = + PowerMon_State._(32, _omitEnumNames ? '' : 'Lora_RXActive'); + static const PowerMon_State BT_On = + PowerMon_State._(64, _omitEnumNames ? '' : 'BT_On'); + static const PowerMon_State LED_On = + PowerMon_State._(128, _omitEnumNames ? '' : 'LED_On'); + static const PowerMon_State Screen_On = + PowerMon_State._(256, _omitEnumNames ? '' : 'Screen_On'); + static const PowerMon_State Screen_Drawing = + PowerMon_State._(512, _omitEnumNames ? '' : 'Screen_Drawing'); + static const PowerMon_State Wifi_On = + PowerMon_State._(1024, _omitEnumNames ? '' : 'Wifi_On'); + + /// + /// GPS is actively trying to find our location + /// See GPSPowerState for more details + static const PowerMon_State GPS_Active = + PowerMon_State._(2048, _omitEnumNames ? '' : 'GPS_Active'); + + static const $core.List values = [ + None, + CPU_DeepSleep, + CPU_LightSleep, + Vext1_On, + Lora_RXOn, + Lora_TXOn, + Lora_RXActive, + BT_On, + LED_On, + Screen_On, + Screen_Drawing, + Wifi_On, + GPS_Active, + ]; + + static final $core.Map<$core.int, PowerMon_State> _byValue = + $pb.ProtobufEnum.initByValue(values); + static PowerMon_State? valueOf($core.int value) => _byValue[value]; + + const PowerMon_State._(super.value, super.name); +} + +/// +/// What operation would we like the UUT to perform. +/// note: senders should probably set want_response in their request packets, so that they can know when the state +/// machine has started processing their request +class PowerStressMessage_Opcode extends $pb.ProtobufEnum { + /// + /// Unset/unused + static const PowerStressMessage_Opcode UNSET = + PowerStressMessage_Opcode._(0, _omitEnumNames ? '' : 'UNSET'); + static const PowerStressMessage_Opcode PRINT_INFO = + PowerStressMessage_Opcode._(1, _omitEnumNames ? '' : 'PRINT_INFO'); + static const PowerStressMessage_Opcode FORCE_QUIET = + PowerStressMessage_Opcode._(2, _omitEnumNames ? '' : 'FORCE_QUIET'); + static const PowerStressMessage_Opcode END_QUIET = + PowerStressMessage_Opcode._(3, _omitEnumNames ? '' : 'END_QUIET'); + static const PowerStressMessage_Opcode SCREEN_ON = + PowerStressMessage_Opcode._(16, _omitEnumNames ? '' : 'SCREEN_ON'); + static const PowerStressMessage_Opcode SCREEN_OFF = + PowerStressMessage_Opcode._(17, _omitEnumNames ? '' : 'SCREEN_OFF'); + static const PowerStressMessage_Opcode CPU_IDLE = + PowerStressMessage_Opcode._(32, _omitEnumNames ? '' : 'CPU_IDLE'); + static const PowerStressMessage_Opcode CPU_DEEPSLEEP = + PowerStressMessage_Opcode._(33, _omitEnumNames ? '' : 'CPU_DEEPSLEEP'); + static const PowerStressMessage_Opcode CPU_FULLON = + PowerStressMessage_Opcode._(34, _omitEnumNames ? '' : 'CPU_FULLON'); + static const PowerStressMessage_Opcode LED_ON = + PowerStressMessage_Opcode._(48, _omitEnumNames ? '' : 'LED_ON'); + static const PowerStressMessage_Opcode LED_OFF = + PowerStressMessage_Opcode._(49, _omitEnumNames ? '' : 'LED_OFF'); + static const PowerStressMessage_Opcode LORA_OFF = + PowerStressMessage_Opcode._(64, _omitEnumNames ? '' : 'LORA_OFF'); + static const PowerStressMessage_Opcode LORA_TX = + PowerStressMessage_Opcode._(65, _omitEnumNames ? '' : 'LORA_TX'); + static const PowerStressMessage_Opcode LORA_RX = + PowerStressMessage_Opcode._(66, _omitEnumNames ? '' : 'LORA_RX'); + static const PowerStressMessage_Opcode BT_OFF = + PowerStressMessage_Opcode._(80, _omitEnumNames ? '' : 'BT_OFF'); + static const PowerStressMessage_Opcode BT_ON = + PowerStressMessage_Opcode._(81, _omitEnumNames ? '' : 'BT_ON'); + static const PowerStressMessage_Opcode WIFI_OFF = + PowerStressMessage_Opcode._(96, _omitEnumNames ? '' : 'WIFI_OFF'); + static const PowerStressMessage_Opcode WIFI_ON = + PowerStressMessage_Opcode._(97, _omitEnumNames ? '' : 'WIFI_ON'); + static const PowerStressMessage_Opcode GPS_OFF = + PowerStressMessage_Opcode._(112, _omitEnumNames ? '' : 'GPS_OFF'); + static const PowerStressMessage_Opcode GPS_ON = + PowerStressMessage_Opcode._(113, _omitEnumNames ? '' : 'GPS_ON'); + + static const $core.List values = + [ + UNSET, + PRINT_INFO, + FORCE_QUIET, + END_QUIET, + SCREEN_ON, + SCREEN_OFF, + CPU_IDLE, + CPU_DEEPSLEEP, + CPU_FULLON, + LED_ON, + LED_OFF, + LORA_OFF, + LORA_TX, + LORA_RX, + BT_OFF, + BT_ON, + WIFI_OFF, + WIFI_ON, + GPS_OFF, + GPS_ON, + ]; + + static final $core.Map<$core.int, PowerStressMessage_Opcode> _byValue = + $pb.ProtobufEnum.initByValue(values); + static PowerStressMessage_Opcode? valueOf($core.int value) => _byValue[value]; + + const PowerStressMessage_Opcode._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/powermon.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/powermon.pbjson.dart new file mode 100644 index 000000000..fdbcff6f3 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/powermon.pbjson.dart @@ -0,0 +1,104 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/powermon.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use powerMonDescriptor instead') +const PowerMon$json = { + '1': 'PowerMon', + '4': [PowerMon_State$json], +}; + +@$core.Deprecated('Use powerMonDescriptor instead') +const PowerMon_State$json = { + '1': 'State', + '2': [ + {'1': 'None', '2': 0}, + {'1': 'CPU_DeepSleep', '2': 1}, + {'1': 'CPU_LightSleep', '2': 2}, + {'1': 'Vext1_On', '2': 4}, + {'1': 'Lora_RXOn', '2': 8}, + {'1': 'Lora_TXOn', '2': 16}, + {'1': 'Lora_RXActive', '2': 32}, + {'1': 'BT_On', '2': 64}, + {'1': 'LED_On', '2': 128}, + {'1': 'Screen_On', '2': 256}, + {'1': 'Screen_Drawing', '2': 512}, + {'1': 'Wifi_On', '2': 1024}, + {'1': 'GPS_Active', '2': 2048}, + ], +}; + +/// Descriptor for `PowerMon`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List powerMonDescriptor = $convert.base64Decode( + 'CghQb3dlck1vbiLTAQoFU3RhdGUSCAoETm9uZRAAEhEKDUNQVV9EZWVwU2xlZXAQARISCg5DUF' + 'VfTGlnaHRTbGVlcBACEgwKCFZleHQxX09uEAQSDQoJTG9yYV9SWE9uEAgSDQoJTG9yYV9UWE9u' + 'EBASEQoNTG9yYV9SWEFjdGl2ZRAgEgkKBUJUX09uEEASCwoGTEVEX09uEIABEg4KCVNjcmVlbl' + '9PbhCAAhITCg5TY3JlZW5fRHJhd2luZxCABBIMCgdXaWZpX09uEIAIEg8KCkdQU19BY3RpdmUQ' + 'gBA='); + +@$core.Deprecated('Use powerStressMessageDescriptor instead') +const PowerStressMessage$json = { + '1': 'PowerStressMessage', + '2': [ + { + '1': 'cmd', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.PowerStressMessage.Opcode', + '10': 'cmd' + }, + {'1': 'num_seconds', '3': 2, '4': 1, '5': 2, '10': 'numSeconds'}, + ], + '4': [PowerStressMessage_Opcode$json], +}; + +@$core.Deprecated('Use powerStressMessageDescriptor instead') +const PowerStressMessage_Opcode$json = { + '1': 'Opcode', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'PRINT_INFO', '2': 1}, + {'1': 'FORCE_QUIET', '2': 2}, + {'1': 'END_QUIET', '2': 3}, + {'1': 'SCREEN_ON', '2': 16}, + {'1': 'SCREEN_OFF', '2': 17}, + {'1': 'CPU_IDLE', '2': 32}, + {'1': 'CPU_DEEPSLEEP', '2': 33}, + {'1': 'CPU_FULLON', '2': 34}, + {'1': 'LED_ON', '2': 48}, + {'1': 'LED_OFF', '2': 49}, + {'1': 'LORA_OFF', '2': 64}, + {'1': 'LORA_TX', '2': 65}, + {'1': 'LORA_RX', '2': 66}, + {'1': 'BT_OFF', '2': 80}, + {'1': 'BT_ON', '2': 81}, + {'1': 'WIFI_OFF', '2': 96}, + {'1': 'WIFI_ON', '2': 97}, + {'1': 'GPS_OFF', '2': 112}, + {'1': 'GPS_ON', '2': 113}, + ], +}; + +/// Descriptor for `PowerStressMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List powerStressMessageDescriptor = $convert.base64Decode( + 'ChJQb3dlclN0cmVzc01lc3NhZ2USNwoDY21kGAEgASgOMiUubWVzaHRhc3RpYy5Qb3dlclN0cm' + 'Vzc01lc3NhZ2UuT3Bjb2RlUgNjbWQSHwoLbnVtX3NlY29uZHMYAiABKAJSCm51bVNlY29uZHMi' + 'nwIKBk9wY29kZRIJCgVVTlNFVBAAEg4KClBSSU5UX0lORk8QARIPCgtGT1JDRV9RVUlFVBACEg' + '0KCUVORF9RVUlFVBADEg0KCVNDUkVFTl9PThAQEg4KClNDUkVFTl9PRkYQERIMCghDUFVfSURM' + 'RRAgEhEKDUNQVV9ERUVQU0xFRVAQIRIOCgpDUFVfRlVMTE9OECISCgoGTEVEX09OEDASCwoHTE' + 'VEX09GRhAxEgwKCExPUkFfT0ZGEEASCwoHTE9SQV9UWBBBEgsKB0xPUkFfUlgQQhIKCgZCVF9P' + 'RkYQUBIJCgVCVF9PThBREgwKCFdJRklfT0ZGEGASCwoHV0lGSV9PThBhEgsKB0dQU19PRkYQcB' + 'IKCgZHUFNfT04QcQ=='); diff --git a/third_party/meshtastic_flutter/lib/generated/remote_hardware.pb.dart b/third_party/meshtastic_flutter/lib/generated/remote_hardware.pb.dart new file mode 100644 index 000000000..cdee76e82 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/remote_hardware.pb.dart @@ -0,0 +1,132 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/remote_hardware.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'remote_hardware.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'remote_hardware.pbenum.dart'; + +/// +/// An example app to show off the module system. This message is used for +/// REMOTE_HARDWARE_APP PortNums. +/// Also provides easy remote access to any GPIO. +/// In the future other remote hardware operations can be added based on user interest +/// (i.e. serial output, spi/i2c input/output). +/// FIXME - currently this feature is turned on by default which is dangerous +/// because no security yet (beyond the channel mechanism). +/// It should be off by default and then protected based on some TBD mechanism +/// (a special channel once multichannel support is included?) +class HardwareMessage extends $pb.GeneratedMessage { + factory HardwareMessage({ + HardwareMessage_Type? type, + $fixnum.Int64? gpioMask, + $fixnum.Int64? gpioValue, + }) { + final result = create(); + if (type != null) result.type = type; + if (gpioMask != null) result.gpioMask = gpioMask; + if (gpioValue != null) result.gpioValue = gpioValue; + return result; + } + + HardwareMessage._(); + + factory HardwareMessage.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory HardwareMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'HardwareMessage', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e( + 1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, + defaultOrMaker: HardwareMessage_Type.UNSET, + valueOf: HardwareMessage_Type.valueOf, + enumValues: HardwareMessage_Type.values) + ..a<$fixnum.Int64>( + 2, _omitFieldNames ? '' : 'gpioMask', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>( + 3, _omitFieldNames ? '' : 'gpioValue', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HardwareMessage clone() => HardwareMessage()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HardwareMessage copyWith(void Function(HardwareMessage) updates) => + super.copyWith((message) => updates(message as HardwareMessage)) + as HardwareMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HardwareMessage create() => HardwareMessage._(); + @$core.override + HardwareMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static HardwareMessage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static HardwareMessage? _defaultInstance; + + /// + /// What type of HardwareMessage is this? + @$pb.TagNumber(1) + HardwareMessage_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(HardwareMessage_Type value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => $_clearField(1); + + /// + /// What gpios are we changing. Not used for all MessageTypes, see MessageType for details + @$pb.TagNumber(2) + $fixnum.Int64 get gpioMask => $_getI64(1); + @$pb.TagNumber(2) + set gpioMask($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasGpioMask() => $_has(1); + @$pb.TagNumber(2) + void clearGpioMask() => $_clearField(2); + + /// + /// For gpios that were listed in gpio_mask as valid, what are the signal levels for those gpios. + /// Not used for all MessageTypes, see MessageType for details + @$pb.TagNumber(3) + $fixnum.Int64 get gpioValue => $_getI64(2); + @$pb.TagNumber(3) + set gpioValue($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasGpioValue() => $_has(2); + @$pb.TagNumber(3) + void clearGpioValue() => $_clearField(3); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/remote_hardware.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/remote_hardware.pbenum.dart new file mode 100644 index 000000000..79e0094c4 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/remote_hardware.pbenum.dart @@ -0,0 +1,70 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/remote_hardware.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// TODO: REPLACE +class HardwareMessage_Type extends $pb.ProtobufEnum { + /// + /// Unset/unused + static const HardwareMessage_Type UNSET = + HardwareMessage_Type._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// Set gpio gpios based on gpio_mask/gpio_value + static const HardwareMessage_Type WRITE_GPIOS = + HardwareMessage_Type._(1, _omitEnumNames ? '' : 'WRITE_GPIOS'); + + /// + /// We are now interested in watching the gpio_mask gpios. + /// If the selected gpios change, please broadcast GPIOS_CHANGED. + /// Will implicitly change the gpios requested to be INPUT gpios. + static const HardwareMessage_Type WATCH_GPIOS = + HardwareMessage_Type._(2, _omitEnumNames ? '' : 'WATCH_GPIOS'); + + /// + /// The gpios listed in gpio_mask have changed, the new values are listed in gpio_value + static const HardwareMessage_Type GPIOS_CHANGED = + HardwareMessage_Type._(3, _omitEnumNames ? '' : 'GPIOS_CHANGED'); + + /// + /// Read the gpios specified in gpio_mask, send back a READ_GPIOS_REPLY reply with gpio_value populated + static const HardwareMessage_Type READ_GPIOS = + HardwareMessage_Type._(4, _omitEnumNames ? '' : 'READ_GPIOS'); + + /// + /// A reply to READ_GPIOS. gpio_mask and gpio_value will be populated + static const HardwareMessage_Type READ_GPIOS_REPLY = + HardwareMessage_Type._(5, _omitEnumNames ? '' : 'READ_GPIOS_REPLY'); + + static const $core.List values = [ + UNSET, + WRITE_GPIOS, + WATCH_GPIOS, + GPIOS_CHANGED, + READ_GPIOS, + READ_GPIOS_REPLY, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 5); + static HardwareMessage_Type? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const HardwareMessage_Type._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/remote_hardware.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/remote_hardware.pbjson.dart new file mode 100644 index 000000000..108e5d049 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/remote_hardware.pbjson.dart @@ -0,0 +1,54 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/remote_hardware.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use hardwareMessageDescriptor instead') +const HardwareMessage$json = { + '1': 'HardwareMessage', + '2': [ + { + '1': 'type', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.HardwareMessage.Type', + '10': 'type' + }, + {'1': 'gpio_mask', '3': 2, '4': 1, '5': 4, '10': 'gpioMask'}, + {'1': 'gpio_value', '3': 3, '4': 1, '5': 4, '10': 'gpioValue'}, + ], + '4': [HardwareMessage_Type$json], +}; + +@$core.Deprecated('Use hardwareMessageDescriptor instead') +const HardwareMessage_Type$json = { + '1': 'Type', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'WRITE_GPIOS', '2': 1}, + {'1': 'WATCH_GPIOS', '2': 2}, + {'1': 'GPIOS_CHANGED', '2': 3}, + {'1': 'READ_GPIOS', '2': 4}, + {'1': 'READ_GPIOS_REPLY', '2': 5}, + ], +}; + +/// Descriptor for `HardwareMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List hardwareMessageDescriptor = $convert.base64Decode( + 'Cg9IYXJkd2FyZU1lc3NhZ2USNAoEdHlwZRgBIAEoDjIgLm1lc2h0YXN0aWMuSGFyZHdhcmVNZX' + 'NzYWdlLlR5cGVSBHR5cGUSGwoJZ3Bpb19tYXNrGAIgASgEUghncGlvTWFzaxIdCgpncGlvX3Zh' + 'bHVlGAMgASgEUglncGlvVmFsdWUibAoEVHlwZRIJCgVVTlNFVBAAEg8KC1dSSVRFX0dQSU9TEA' + 'ESDwoLV0FUQ0hfR1BJT1MQAhIRCg1HUElPU19DSEFOR0VEEAMSDgoKUkVBRF9HUElPUxAEEhQK' + 'EFJFQURfR1BJT1NfUkVQTFkQBQ=='); diff --git a/third_party/meshtastic_flutter/lib/generated/rtttl.pb.dart b/third_party/meshtastic_flutter/lib/generated/rtttl.pb.dart new file mode 100644 index 000000000..3782552de --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/rtttl.pb.dart @@ -0,0 +1,81 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/rtttl.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +/// +/// Canned message module configuration. +class RTTTLConfig extends $pb.GeneratedMessage { + factory RTTTLConfig({ + $core.String? ringtone, + }) { + final result = create(); + if (ringtone != null) result.ringtone = ringtone; + return result; + } + + RTTTLConfig._(); + + factory RTTTLConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory RTTTLConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'RTTTLConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'ringtone') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RTTTLConfig clone() => RTTTLConfig()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + RTTTLConfig copyWith(void Function(RTTTLConfig) updates) => + super.copyWith((message) => updates(message as RTTTLConfig)) + as RTTTLConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RTTTLConfig create() => RTTTLConfig._(); + @$core.override + RTTTLConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RTTTLConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RTTTLConfig? _defaultInstance; + + /// + /// Ringtone for PWM Buzzer in RTTTL Format. + @$pb.TagNumber(1) + $core.String get ringtone => $_getSZ(0); + @$pb.TagNumber(1) + set ringtone($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasRingtone() => $_has(0); + @$pb.TagNumber(1) + void clearRingtone() => $_clearField(1); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/rtttl.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/rtttl.pbenum.dart new file mode 100644 index 000000000..5c6bcf214 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/rtttl.pbenum.dart @@ -0,0 +1,11 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/rtttl.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names diff --git a/third_party/meshtastic_flutter/lib/generated/rtttl.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/rtttl.pbjson.dart new file mode 100644 index 000000000..54fc59f10 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/rtttl.pbjson.dart @@ -0,0 +1,27 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/rtttl.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use rTTTLConfigDescriptor instead') +const RTTTLConfig$json = { + '1': 'RTTTLConfig', + '2': [ + {'1': 'ringtone', '3': 1, '4': 1, '5': 9, '10': 'ringtone'}, + ], +}; + +/// Descriptor for `RTTTLConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List rTTTLConfigDescriptor = $convert + .base64Decode('CgtSVFRUTENvbmZpZxIaCghyaW5ndG9uZRgBIAEoCVIIcmluZ3RvbmU='); diff --git a/third_party/meshtastic_flutter/lib/generated/storeforward.pb.dart b/third_party/meshtastic_flutter/lib/generated/storeforward.pb.dart new file mode 100644 index 000000000..785f6f286 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/storeforward.pb.dart @@ -0,0 +1,518 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/storeforward.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'storeforward.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'storeforward.pbenum.dart'; + +/// +/// TODO: REPLACE +class StoreAndForward_Statistics extends $pb.GeneratedMessage { + factory StoreAndForward_Statistics({ + $core.int? messagesTotal, + $core.int? messagesSaved, + $core.int? messagesMax, + $core.int? upTime, + $core.int? requests, + $core.int? requestsHistory, + $core.bool? heartbeat, + $core.int? returnMax, + $core.int? returnWindow, + }) { + final result = create(); + if (messagesTotal != null) result.messagesTotal = messagesTotal; + if (messagesSaved != null) result.messagesSaved = messagesSaved; + if (messagesMax != null) result.messagesMax = messagesMax; + if (upTime != null) result.upTime = upTime; + if (requests != null) result.requests = requests; + if (requestsHistory != null) result.requestsHistory = requestsHistory; + if (heartbeat != null) result.heartbeat = heartbeat; + if (returnMax != null) result.returnMax = returnMax; + if (returnWindow != null) result.returnWindow = returnWindow; + return result; + } + + StoreAndForward_Statistics._(); + + factory StoreAndForward_Statistics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StoreAndForward_Statistics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StoreAndForward.Statistics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'messagesTotal', $pb.PbFieldType.OU3) + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'messagesSaved', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'messagesMax', $pb.PbFieldType.OU3) + ..a<$core.int>(4, _omitFieldNames ? '' : 'upTime', $pb.PbFieldType.OU3) + ..a<$core.int>(5, _omitFieldNames ? '' : 'requests', $pb.PbFieldType.OU3) + ..a<$core.int>( + 6, _omitFieldNames ? '' : 'requestsHistory', $pb.PbFieldType.OU3) + ..aOB(7, _omitFieldNames ? '' : 'heartbeat') + ..a<$core.int>(8, _omitFieldNames ? '' : 'returnMax', $pb.PbFieldType.OU3) + ..a<$core.int>( + 9, _omitFieldNames ? '' : 'returnWindow', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward_Statistics clone() => + StoreAndForward_Statistics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward_Statistics copyWith( + void Function(StoreAndForward_Statistics) updates) => + super.copyWith( + (message) => updates(message as StoreAndForward_Statistics)) + as StoreAndForward_Statistics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StoreAndForward_Statistics create() => StoreAndForward_Statistics._(); + @$core.override + StoreAndForward_Statistics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StoreAndForward_Statistics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StoreAndForward_Statistics? _defaultInstance; + + /// + /// Number of messages we have ever seen + @$pb.TagNumber(1) + $core.int get messagesTotal => $_getIZ(0); + @$pb.TagNumber(1) + set messagesTotal($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasMessagesTotal() => $_has(0); + @$pb.TagNumber(1) + void clearMessagesTotal() => $_clearField(1); + + /// + /// Number of messages we have currently saved our history. + @$pb.TagNumber(2) + $core.int get messagesSaved => $_getIZ(1); + @$pb.TagNumber(2) + set messagesSaved($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasMessagesSaved() => $_has(1); + @$pb.TagNumber(2) + void clearMessagesSaved() => $_clearField(2); + + /// + /// Maximum number of messages we will save + @$pb.TagNumber(3) + $core.int get messagesMax => $_getIZ(2); + @$pb.TagNumber(3) + set messagesMax($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasMessagesMax() => $_has(2); + @$pb.TagNumber(3) + void clearMessagesMax() => $_clearField(3); + + /// + /// Router uptime in seconds + @$pb.TagNumber(4) + $core.int get upTime => $_getIZ(3); + @$pb.TagNumber(4) + set upTime($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasUpTime() => $_has(3); + @$pb.TagNumber(4) + void clearUpTime() => $_clearField(4); + + /// + /// Number of times any client sent a request to the S&F. + @$pb.TagNumber(5) + $core.int get requests => $_getIZ(4); + @$pb.TagNumber(5) + set requests($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasRequests() => $_has(4); + @$pb.TagNumber(5) + void clearRequests() => $_clearField(5); + + /// + /// Number of times the history was requested. + @$pb.TagNumber(6) + $core.int get requestsHistory => $_getIZ(5); + @$pb.TagNumber(6) + set requestsHistory($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasRequestsHistory() => $_has(5); + @$pb.TagNumber(6) + void clearRequestsHistory() => $_clearField(6); + + /// + /// Is the heartbeat enabled on the server? + @$pb.TagNumber(7) + $core.bool get heartbeat => $_getBF(6); + @$pb.TagNumber(7) + set heartbeat($core.bool value) => $_setBool(6, value); + @$pb.TagNumber(7) + $core.bool hasHeartbeat() => $_has(6); + @$pb.TagNumber(7) + void clearHeartbeat() => $_clearField(7); + + /// + /// Maximum number of messages the server will return. + @$pb.TagNumber(8) + $core.int get returnMax => $_getIZ(7); + @$pb.TagNumber(8) + set returnMax($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasReturnMax() => $_has(7); + @$pb.TagNumber(8) + void clearReturnMax() => $_clearField(8); + + /// + /// Maximum history window in minutes the server will return messages from. + @$pb.TagNumber(9) + $core.int get returnWindow => $_getIZ(8); + @$pb.TagNumber(9) + set returnWindow($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasReturnWindow() => $_has(8); + @$pb.TagNumber(9) + void clearReturnWindow() => $_clearField(9); +} + +/// +/// TODO: REPLACE +class StoreAndForward_History extends $pb.GeneratedMessage { + factory StoreAndForward_History({ + $core.int? historyMessages, + $core.int? window, + $core.int? lastRequest, + }) { + final result = create(); + if (historyMessages != null) result.historyMessages = historyMessages; + if (window != null) result.window = window; + if (lastRequest != null) result.lastRequest = lastRequest; + return result; + } + + StoreAndForward_History._(); + + factory StoreAndForward_History.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StoreAndForward_History.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StoreAndForward.History', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'historyMessages', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'window', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'lastRequest', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward_History clone() => + StoreAndForward_History()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward_History copyWith( + void Function(StoreAndForward_History) updates) => + super.copyWith((message) => updates(message as StoreAndForward_History)) + as StoreAndForward_History; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StoreAndForward_History create() => StoreAndForward_History._(); + @$core.override + StoreAndForward_History createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StoreAndForward_History getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StoreAndForward_History? _defaultInstance; + + /// + /// Number of that will be sent to the client + @$pb.TagNumber(1) + $core.int get historyMessages => $_getIZ(0); + @$pb.TagNumber(1) + set historyMessages($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasHistoryMessages() => $_has(0); + @$pb.TagNumber(1) + void clearHistoryMessages() => $_clearField(1); + + /// + /// The window of messages that was used to filter the history client requested + @$pb.TagNumber(2) + $core.int get window => $_getIZ(1); + @$pb.TagNumber(2) + set window($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasWindow() => $_has(1); + @$pb.TagNumber(2) + void clearWindow() => $_clearField(2); + + /// + /// Index in the packet history of the last message sent in a previous request to the server. + /// Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client. + @$pb.TagNumber(3) + $core.int get lastRequest => $_getIZ(2); + @$pb.TagNumber(3) + set lastRequest($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasLastRequest() => $_has(2); + @$pb.TagNumber(3) + void clearLastRequest() => $_clearField(3); +} + +/// +/// TODO: REPLACE +class StoreAndForward_Heartbeat extends $pb.GeneratedMessage { + factory StoreAndForward_Heartbeat({ + $core.int? period, + $core.int? secondary, + }) { + final result = create(); + if (period != null) result.period = period; + if (secondary != null) result.secondary = secondary; + return result; + } + + StoreAndForward_Heartbeat._(); + + factory StoreAndForward_Heartbeat.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StoreAndForward_Heartbeat.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StoreAndForward.Heartbeat', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'period', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'secondary', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward_Heartbeat clone() => + StoreAndForward_Heartbeat()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward_Heartbeat copyWith( + void Function(StoreAndForward_Heartbeat) updates) => + super.copyWith((message) => updates(message as StoreAndForward_Heartbeat)) + as StoreAndForward_Heartbeat; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StoreAndForward_Heartbeat create() => StoreAndForward_Heartbeat._(); + @$core.override + StoreAndForward_Heartbeat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StoreAndForward_Heartbeat getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StoreAndForward_Heartbeat? _defaultInstance; + + /// + /// Period in seconds that the heartbeat is sent out that will be sent to the client + @$pb.TagNumber(1) + $core.int get period => $_getIZ(0); + @$pb.TagNumber(1) + set period($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPeriod() => $_has(0); + @$pb.TagNumber(1) + void clearPeriod() => $_clearField(1); + + /// + /// If set, this is not the primary Store & Forward router on the mesh + @$pb.TagNumber(2) + $core.int get secondary => $_getIZ(1); + @$pb.TagNumber(2) + set secondary($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasSecondary() => $_has(1); + @$pb.TagNumber(2) + void clearSecondary() => $_clearField(2); +} + +enum StoreAndForward_Variant { stats, history, heartbeat, text, notSet } + +/// +/// TODO: REPLACE +class StoreAndForward extends $pb.GeneratedMessage { + factory StoreAndForward({ + StoreAndForward_RequestResponse? rr, + StoreAndForward_Statistics? stats, + StoreAndForward_History? history, + StoreAndForward_Heartbeat? heartbeat, + $core.List<$core.int>? text, + }) { + final result = create(); + if (rr != null) result.rr = rr; + if (stats != null) result.stats = stats; + if (history != null) result.history = history; + if (heartbeat != null) result.heartbeat = heartbeat; + if (text != null) result.text = text; + return result; + } + + StoreAndForward._(); + + factory StoreAndForward.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StoreAndForward.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, StoreAndForward_Variant> + _StoreAndForward_VariantByTag = { + 2: StoreAndForward_Variant.stats, + 3: StoreAndForward_Variant.history, + 4: StoreAndForward_Variant.heartbeat, + 5: StoreAndForward_Variant.text, + 0: StoreAndForward_Variant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StoreAndForward', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [2, 3, 4, 5]) + ..e( + 1, _omitFieldNames ? '' : 'rr', $pb.PbFieldType.OE, + defaultOrMaker: StoreAndForward_RequestResponse.UNSET, + valueOf: StoreAndForward_RequestResponse.valueOf, + enumValues: StoreAndForward_RequestResponse.values) + ..aOM(2, _omitFieldNames ? '' : 'stats', + subBuilder: StoreAndForward_Statistics.create) + ..aOM(3, _omitFieldNames ? '' : 'history', + subBuilder: StoreAndForward_History.create) + ..aOM(4, _omitFieldNames ? '' : 'heartbeat', + subBuilder: StoreAndForward_Heartbeat.create) + ..a<$core.List<$core.int>>( + 5, _omitFieldNames ? '' : 'text', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward clone() => StoreAndForward()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StoreAndForward copyWith(void Function(StoreAndForward) updates) => + super.copyWith((message) => updates(message as StoreAndForward)) + as StoreAndForward; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StoreAndForward create() => StoreAndForward._(); + @$core.override + StoreAndForward createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StoreAndForward getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StoreAndForward? _defaultInstance; + + StoreAndForward_Variant whichVariant() => + _StoreAndForward_VariantByTag[$_whichOneof(0)]!; + void clearVariant() => $_clearField($_whichOneof(0)); + + /// + /// TODO: REPLACE + @$pb.TagNumber(1) + StoreAndForward_RequestResponse get rr => $_getN(0); + @$pb.TagNumber(1) + set rr(StoreAndForward_RequestResponse value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasRr() => $_has(0); + @$pb.TagNumber(1) + void clearRr() => $_clearField(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(2) + StoreAndForward_Statistics get stats => $_getN(1); + @$pb.TagNumber(2) + set stats(StoreAndForward_Statistics value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasStats() => $_has(1); + @$pb.TagNumber(2) + void clearStats() => $_clearField(2); + @$pb.TagNumber(2) + StoreAndForward_Statistics ensureStats() => $_ensure(1); + + /// + /// TODO: REPLACE + @$pb.TagNumber(3) + StoreAndForward_History get history => $_getN(2); + @$pb.TagNumber(3) + set history(StoreAndForward_History value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasHistory() => $_has(2); + @$pb.TagNumber(3) + void clearHistory() => $_clearField(3); + @$pb.TagNumber(3) + StoreAndForward_History ensureHistory() => $_ensure(2); + + /// + /// TODO: REPLACE + @$pb.TagNumber(4) + StoreAndForward_Heartbeat get heartbeat => $_getN(3); + @$pb.TagNumber(4) + set heartbeat(StoreAndForward_Heartbeat value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasHeartbeat() => $_has(3); + @$pb.TagNumber(4) + void clearHeartbeat() => $_clearField(4); + @$pb.TagNumber(4) + StoreAndForward_Heartbeat ensureHeartbeat() => $_ensure(3); + + /// + /// Text from history message. + @$pb.TagNumber(5) + $core.List<$core.int> get text => $_getN(4); + @$pb.TagNumber(5) + set text($core.List<$core.int> value) => $_setBytes(4, value); + @$pb.TagNumber(5) + $core.bool hasText() => $_has(4); + @$pb.TagNumber(5) + void clearText() => $_clearField(5); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/storeforward.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/storeforward.pbenum.dart new file mode 100644 index 000000000..f38c7188a --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/storeforward.pbenum.dart @@ -0,0 +1,144 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/storeforward.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// 001 - 063 = From Router +/// 064 - 127 = From Client +class StoreAndForward_RequestResponse extends $pb.ProtobufEnum { + /// + /// Unset/unused + static const StoreAndForward_RequestResponse UNSET = + StoreAndForward_RequestResponse._(0, _omitEnumNames ? '' : 'UNSET'); + + /// + /// Router is an in error state. + static const StoreAndForward_RequestResponse ROUTER_ERROR = + StoreAndForward_RequestResponse._( + 1, _omitEnumNames ? '' : 'ROUTER_ERROR'); + + /// + /// Router heartbeat + static const StoreAndForward_RequestResponse ROUTER_HEARTBEAT = + StoreAndForward_RequestResponse._( + 2, _omitEnumNames ? '' : 'ROUTER_HEARTBEAT'); + + /// + /// Router has requested the client respond. This can work as a + /// "are you there" message. + static const StoreAndForward_RequestResponse ROUTER_PING = + StoreAndForward_RequestResponse._(3, _omitEnumNames ? '' : 'ROUTER_PING'); + + /// + /// The response to a "Ping" + static const StoreAndForward_RequestResponse ROUTER_PONG = + StoreAndForward_RequestResponse._(4, _omitEnumNames ? '' : 'ROUTER_PONG'); + + /// + /// Router is currently busy. Please try again later. + static const StoreAndForward_RequestResponse ROUTER_BUSY = + StoreAndForward_RequestResponse._(5, _omitEnumNames ? '' : 'ROUTER_BUSY'); + + /// + /// Router is responding to a request for history. + static const StoreAndForward_RequestResponse ROUTER_HISTORY = + StoreAndForward_RequestResponse._( + 6, _omitEnumNames ? '' : 'ROUTER_HISTORY'); + + /// + /// Router is responding to a request for stats. + static const StoreAndForward_RequestResponse ROUTER_STATS = + StoreAndForward_RequestResponse._( + 7, _omitEnumNames ? '' : 'ROUTER_STATS'); + + /// + /// Router sends a text message from its history that was a direct message. + static const StoreAndForward_RequestResponse ROUTER_TEXT_DIRECT = + StoreAndForward_RequestResponse._( + 8, _omitEnumNames ? '' : 'ROUTER_TEXT_DIRECT'); + + /// + /// Router sends a text message from its history that was a broadcast. + static const StoreAndForward_RequestResponse ROUTER_TEXT_BROADCAST = + StoreAndForward_RequestResponse._( + 9, _omitEnumNames ? '' : 'ROUTER_TEXT_BROADCAST'); + + /// + /// Client is an in error state. + static const StoreAndForward_RequestResponse CLIENT_ERROR = + StoreAndForward_RequestResponse._( + 64, _omitEnumNames ? '' : 'CLIENT_ERROR'); + + /// + /// Client has requested a replay from the router. + static const StoreAndForward_RequestResponse CLIENT_HISTORY = + StoreAndForward_RequestResponse._( + 65, _omitEnumNames ? '' : 'CLIENT_HISTORY'); + + /// + /// Client has requested stats from the router. + static const StoreAndForward_RequestResponse CLIENT_STATS = + StoreAndForward_RequestResponse._( + 66, _omitEnumNames ? '' : 'CLIENT_STATS'); + + /// + /// Client has requested the router respond. This can work as a + /// "are you there" message. + static const StoreAndForward_RequestResponse CLIENT_PING = + StoreAndForward_RequestResponse._( + 67, _omitEnumNames ? '' : 'CLIENT_PING'); + + /// + /// The response to a "Ping" + static const StoreAndForward_RequestResponse CLIENT_PONG = + StoreAndForward_RequestResponse._( + 68, _omitEnumNames ? '' : 'CLIENT_PONG'); + + /// + /// Client has requested that the router abort processing the client's request + static const StoreAndForward_RequestResponse CLIENT_ABORT = + StoreAndForward_RequestResponse._( + 106, _omitEnumNames ? '' : 'CLIENT_ABORT'); + + static const $core.List values = + [ + UNSET, + ROUTER_ERROR, + ROUTER_HEARTBEAT, + ROUTER_PING, + ROUTER_PONG, + ROUTER_BUSY, + ROUTER_HISTORY, + ROUTER_STATS, + ROUTER_TEXT_DIRECT, + ROUTER_TEXT_BROADCAST, + CLIENT_ERROR, + CLIENT_HISTORY, + CLIENT_STATS, + CLIENT_PING, + CLIENT_PONG, + CLIENT_ABORT, + ]; + + static final $core.Map<$core.int, StoreAndForward_RequestResponse> _byValue = + $pb.ProtobufEnum.initByValue(values); + static StoreAndForward_RequestResponse? valueOf($core.int value) => + _byValue[value]; + + const StoreAndForward_RequestResponse._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/storeforward.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/storeforward.pbjson.dart new file mode 100644 index 000000000..28333dd0a --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/storeforward.pbjson.dart @@ -0,0 +1,149 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/storeforward.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use storeAndForwardDescriptor instead') +const StoreAndForward$json = { + '1': 'StoreAndForward', + '2': [ + { + '1': 'rr', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.StoreAndForward.RequestResponse', + '10': 'rr' + }, + { + '1': 'stats', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.StoreAndForward.Statistics', + '9': 0, + '10': 'stats' + }, + { + '1': 'history', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.StoreAndForward.History', + '9': 0, + '10': 'history' + }, + { + '1': 'heartbeat', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.StoreAndForward.Heartbeat', + '9': 0, + '10': 'heartbeat' + }, + {'1': 'text', '3': 5, '4': 1, '5': 12, '9': 0, '10': 'text'}, + ], + '3': [ + StoreAndForward_Statistics$json, + StoreAndForward_History$json, + StoreAndForward_Heartbeat$json + ], + '4': [StoreAndForward_RequestResponse$json], + '8': [ + {'1': 'variant'}, + ], +}; + +@$core.Deprecated('Use storeAndForwardDescriptor instead') +const StoreAndForward_Statistics$json = { + '1': 'Statistics', + '2': [ + {'1': 'messages_total', '3': 1, '4': 1, '5': 13, '10': 'messagesTotal'}, + {'1': 'messages_saved', '3': 2, '4': 1, '5': 13, '10': 'messagesSaved'}, + {'1': 'messages_max', '3': 3, '4': 1, '5': 13, '10': 'messagesMax'}, + {'1': 'up_time', '3': 4, '4': 1, '5': 13, '10': 'upTime'}, + {'1': 'requests', '3': 5, '4': 1, '5': 13, '10': 'requests'}, + {'1': 'requests_history', '3': 6, '4': 1, '5': 13, '10': 'requestsHistory'}, + {'1': 'heartbeat', '3': 7, '4': 1, '5': 8, '10': 'heartbeat'}, + {'1': 'return_max', '3': 8, '4': 1, '5': 13, '10': 'returnMax'}, + {'1': 'return_window', '3': 9, '4': 1, '5': 13, '10': 'returnWindow'}, + ], +}; + +@$core.Deprecated('Use storeAndForwardDescriptor instead') +const StoreAndForward_History$json = { + '1': 'History', + '2': [ + {'1': 'history_messages', '3': 1, '4': 1, '5': 13, '10': 'historyMessages'}, + {'1': 'window', '3': 2, '4': 1, '5': 13, '10': 'window'}, + {'1': 'last_request', '3': 3, '4': 1, '5': 13, '10': 'lastRequest'}, + ], +}; + +@$core.Deprecated('Use storeAndForwardDescriptor instead') +const StoreAndForward_Heartbeat$json = { + '1': 'Heartbeat', + '2': [ + {'1': 'period', '3': 1, '4': 1, '5': 13, '10': 'period'}, + {'1': 'secondary', '3': 2, '4': 1, '5': 13, '10': 'secondary'}, + ], +}; + +@$core.Deprecated('Use storeAndForwardDescriptor instead') +const StoreAndForward_RequestResponse$json = { + '1': 'RequestResponse', + '2': [ + {'1': 'UNSET', '2': 0}, + {'1': 'ROUTER_ERROR', '2': 1}, + {'1': 'ROUTER_HEARTBEAT', '2': 2}, + {'1': 'ROUTER_PING', '2': 3}, + {'1': 'ROUTER_PONG', '2': 4}, + {'1': 'ROUTER_BUSY', '2': 5}, + {'1': 'ROUTER_HISTORY', '2': 6}, + {'1': 'ROUTER_STATS', '2': 7}, + {'1': 'ROUTER_TEXT_DIRECT', '2': 8}, + {'1': 'ROUTER_TEXT_BROADCAST', '2': 9}, + {'1': 'CLIENT_ERROR', '2': 64}, + {'1': 'CLIENT_HISTORY', '2': 65}, + {'1': 'CLIENT_STATS', '2': 66}, + {'1': 'CLIENT_PING', '2': 67}, + {'1': 'CLIENT_PONG', '2': 68}, + {'1': 'CLIENT_ABORT', '2': 106}, + ], +}; + +/// Descriptor for `StoreAndForward`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List storeAndForwardDescriptor = $convert.base64Decode( + 'Cg9TdG9yZUFuZEZvcndhcmQSOwoCcnIYASABKA4yKy5tZXNodGFzdGljLlN0b3JlQW5kRm9yd2' + 'FyZC5SZXF1ZXN0UmVzcG9uc2VSAnJyEj4KBXN0YXRzGAIgASgLMiYubWVzaHRhc3RpYy5TdG9y' + 'ZUFuZEZvcndhcmQuU3RhdGlzdGljc0gAUgVzdGF0cxI/CgdoaXN0b3J5GAMgASgLMiMubWVzaH' + 'Rhc3RpYy5TdG9yZUFuZEZvcndhcmQuSGlzdG9yeUgAUgdoaXN0b3J5EkUKCWhlYXJ0YmVhdBgE' + 'IAEoCzIlLm1lc2h0YXN0aWMuU3RvcmVBbmRGb3J3YXJkLkhlYXJ0YmVhdEgAUgloZWFydGJlYX' + 'QSFAoEdGV4dBgFIAEoDEgAUgR0ZXh0Gr8CCgpTdGF0aXN0aWNzEiUKDm1lc3NhZ2VzX3RvdGFs' + 'GAEgASgNUg1tZXNzYWdlc1RvdGFsEiUKDm1lc3NhZ2VzX3NhdmVkGAIgASgNUg1tZXNzYWdlc1' + 'NhdmVkEiEKDG1lc3NhZ2VzX21heBgDIAEoDVILbWVzc2FnZXNNYXgSFwoHdXBfdGltZRgEIAEo' + 'DVIGdXBUaW1lEhoKCHJlcXVlc3RzGAUgASgNUghyZXF1ZXN0cxIpChByZXF1ZXN0c19oaXN0b3' + 'J5GAYgASgNUg9yZXF1ZXN0c0hpc3RvcnkSHAoJaGVhcnRiZWF0GAcgASgIUgloZWFydGJlYXQS' + 'HQoKcmV0dXJuX21heBgIIAEoDVIJcmV0dXJuTWF4EiMKDXJldHVybl93aW5kb3cYCSABKA1SDH' + 'JldHVybldpbmRvdxpvCgdIaXN0b3J5EikKEGhpc3RvcnlfbWVzc2FnZXMYASABKA1SD2hpc3Rv' + 'cnlNZXNzYWdlcxIWCgZ3aW5kb3cYAiABKA1SBndpbmRvdxIhCgxsYXN0X3JlcXVlc3QYAyABKA' + '1SC2xhc3RSZXF1ZXN0GkEKCUhlYXJ0YmVhdBIWCgZwZXJpb2QYASABKA1SBnBlcmlvZBIcCglz' + 'ZWNvbmRhcnkYAiABKA1SCXNlY29uZGFyeSK8AgoPUmVxdWVzdFJlc3BvbnNlEgkKBVVOU0VUEA' + 'ASEAoMUk9VVEVSX0VSUk9SEAESFAoQUk9VVEVSX0hFQVJUQkVBVBACEg8KC1JPVVRFUl9QSU5H' + 'EAMSDwoLUk9VVEVSX1BPTkcQBBIPCgtST1VURVJfQlVTWRAFEhIKDlJPVVRFUl9ISVNUT1JZEA' + 'YSEAoMUk9VVEVSX1NUQVRTEAcSFgoSUk9VVEVSX1RFWFRfRElSRUNUEAgSGQoVUk9VVEVSX1RF' + 'WFRfQlJPQURDQVNUEAkSEAoMQ0xJRU5UX0VSUk9SEEASEgoOQ0xJRU5UX0hJU1RPUlkQQRIQCg' + 'xDTElFTlRfU1RBVFMQQhIPCgtDTElFTlRfUElORxBDEg8KC0NMSUVOVF9QT05HEEQSEAoMQ0xJ' + 'RU5UX0FCT1JUEGpCCQoHdmFyaWFudA=='); diff --git a/third_party/meshtastic_flutter/lib/generated/telemetry.pb.dart b/third_party/meshtastic_flutter/lib/generated/telemetry.pb.dart new file mode 100644 index 000000000..a62d3e1e2 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/telemetry.pb.dart @@ -0,0 +1,2019 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/telemetry.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'telemetry.pbenum.dart'; + +/// +/// Key native device metrics such as battery level +class DeviceMetrics extends $pb.GeneratedMessage { + factory DeviceMetrics({ + $core.int? batteryLevel, + $core.double? voltage, + $core.double? channelUtilization, + $core.double? airUtilTx, + $core.int? uptimeSeconds, + }) { + final result = create(); + if (batteryLevel != null) result.batteryLevel = batteryLevel; + if (voltage != null) result.voltage = voltage; + if (channelUtilization != null) + result.channelUtilization = channelUtilization; + if (airUtilTx != null) result.airUtilTx = airUtilTx; + if (uptimeSeconds != null) result.uptimeSeconds = uptimeSeconds; + return result; + } + + DeviceMetrics._(); + + factory DeviceMetrics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeviceMetrics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeviceMetrics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'batteryLevel', $pb.PbFieldType.OU3) + ..a<$core.double>(2, _omitFieldNames ? '' : 'voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 3, _omitFieldNames ? '' : 'channelUtilization', $pb.PbFieldType.OF) + ..a<$core.double>(4, _omitFieldNames ? '' : 'airUtilTx', $pb.PbFieldType.OF) + ..a<$core.int>( + 5, _omitFieldNames ? '' : 'uptimeSeconds', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceMetrics clone() => DeviceMetrics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeviceMetrics copyWith(void Function(DeviceMetrics) updates) => + super.copyWith((message) => updates(message as DeviceMetrics)) + as DeviceMetrics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceMetrics create() => DeviceMetrics._(); + @$core.override + DeviceMetrics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceMetrics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeviceMetrics? _defaultInstance; + + /// + /// 0-100 (>100 means powered) + @$pb.TagNumber(1) + $core.int get batteryLevel => $_getIZ(0); + @$pb.TagNumber(1) + set batteryLevel($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasBatteryLevel() => $_has(0); + @$pb.TagNumber(1) + void clearBatteryLevel() => $_clearField(1); + + /// + /// Voltage measured + @$pb.TagNumber(2) + $core.double get voltage => $_getN(1); + @$pb.TagNumber(2) + set voltage($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasVoltage() => $_has(1); + @$pb.TagNumber(2) + void clearVoltage() => $_clearField(2); + + /// + /// Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). + @$pb.TagNumber(3) + $core.double get channelUtilization => $_getN(2); + @$pb.TagNumber(3) + set channelUtilization($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasChannelUtilization() => $_has(2); + @$pb.TagNumber(3) + void clearChannelUtilization() => $_clearField(3); + + /// + /// Percent of airtime for transmission used within the last hour. + @$pb.TagNumber(4) + $core.double get airUtilTx => $_getN(3); + @$pb.TagNumber(4) + set airUtilTx($core.double value) => $_setFloat(3, value); + @$pb.TagNumber(4) + $core.bool hasAirUtilTx() => $_has(3); + @$pb.TagNumber(4) + void clearAirUtilTx() => $_clearField(4); + + /// + /// How long the device has been running since the last reboot (in seconds) + @$pb.TagNumber(5) + $core.int get uptimeSeconds => $_getIZ(4); + @$pb.TagNumber(5) + set uptimeSeconds($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasUptimeSeconds() => $_has(4); + @$pb.TagNumber(5) + void clearUptimeSeconds() => $_clearField(5); +} + +/// +/// Weather station or other environmental metrics +class EnvironmentMetrics extends $pb.GeneratedMessage { + factory EnvironmentMetrics({ + $core.double? temperature, + $core.double? relativeHumidity, + $core.double? barometricPressure, + $core.double? gasResistance, + $core.double? voltage, + $core.double? current, + $core.int? iaq, + $core.double? distance, + $core.double? lux, + $core.double? whiteLux, + $core.double? irLux, + $core.double? uvLux, + $core.int? windDirection, + $core.double? windSpeed, + $core.double? weight, + $core.double? windGust, + $core.double? windLull, + $core.double? radiation, + $core.double? rainfall1h, + $core.double? rainfall24h, + $core.int? soilMoisture, + $core.double? soilTemperature, + }) { + final result = create(); + if (temperature != null) result.temperature = temperature; + if (relativeHumidity != null) result.relativeHumidity = relativeHumidity; + if (barometricPressure != null) + result.barometricPressure = barometricPressure; + if (gasResistance != null) result.gasResistance = gasResistance; + if (voltage != null) result.voltage = voltage; + if (current != null) result.current = current; + if (iaq != null) result.iaq = iaq; + if (distance != null) result.distance = distance; + if (lux != null) result.lux = lux; + if (whiteLux != null) result.whiteLux = whiteLux; + if (irLux != null) result.irLux = irLux; + if (uvLux != null) result.uvLux = uvLux; + if (windDirection != null) result.windDirection = windDirection; + if (windSpeed != null) result.windSpeed = windSpeed; + if (weight != null) result.weight = weight; + if (windGust != null) result.windGust = windGust; + if (windLull != null) result.windLull = windLull; + if (radiation != null) result.radiation = radiation; + if (rainfall1h != null) result.rainfall1h = rainfall1h; + if (rainfall24h != null) result.rainfall24h = rainfall24h; + if (soilMoisture != null) result.soilMoisture = soilMoisture; + if (soilTemperature != null) result.soilTemperature = soilTemperature; + return result; + } + + EnvironmentMetrics._(); + + factory EnvironmentMetrics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory EnvironmentMetrics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'EnvironmentMetrics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.double>( + 1, _omitFieldNames ? '' : 'temperature', $pb.PbFieldType.OF) + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'relativeHumidity', $pb.PbFieldType.OF) + ..a<$core.double>( + 3, _omitFieldNames ? '' : 'barometricPressure', $pb.PbFieldType.OF) + ..a<$core.double>( + 4, _omitFieldNames ? '' : 'gasResistance', $pb.PbFieldType.OF) + ..a<$core.double>(5, _omitFieldNames ? '' : 'voltage', $pb.PbFieldType.OF) + ..a<$core.double>(6, _omitFieldNames ? '' : 'current', $pb.PbFieldType.OF) + ..a<$core.int>(7, _omitFieldNames ? '' : 'iaq', $pb.PbFieldType.OU3) + ..a<$core.double>(8, _omitFieldNames ? '' : 'distance', $pb.PbFieldType.OF) + ..a<$core.double>(9, _omitFieldNames ? '' : 'lux', $pb.PbFieldType.OF) + ..a<$core.double>(10, _omitFieldNames ? '' : 'whiteLux', $pb.PbFieldType.OF) + ..a<$core.double>(11, _omitFieldNames ? '' : 'irLux', $pb.PbFieldType.OF) + ..a<$core.double>(12, _omitFieldNames ? '' : 'uvLux', $pb.PbFieldType.OF) + ..a<$core.int>( + 13, _omitFieldNames ? '' : 'windDirection', $pb.PbFieldType.OU3) + ..a<$core.double>( + 14, _omitFieldNames ? '' : 'windSpeed', $pb.PbFieldType.OF) + ..a<$core.double>(15, _omitFieldNames ? '' : 'weight', $pb.PbFieldType.OF) + ..a<$core.double>(16, _omitFieldNames ? '' : 'windGust', $pb.PbFieldType.OF) + ..a<$core.double>(17, _omitFieldNames ? '' : 'windLull', $pb.PbFieldType.OF) + ..a<$core.double>( + 18, _omitFieldNames ? '' : 'radiation', $pb.PbFieldType.OF) + ..a<$core.double>( + 19, _omitFieldNames ? '' : 'rainfall1h', $pb.PbFieldType.OF, + protoName: 'rainfall_1h') + ..a<$core.double>( + 20, _omitFieldNames ? '' : 'rainfall24h', $pb.PbFieldType.OF, + protoName: 'rainfall_24h') + ..a<$core.int>( + 21, _omitFieldNames ? '' : 'soilMoisture', $pb.PbFieldType.OU3) + ..a<$core.double>( + 22, _omitFieldNames ? '' : 'soilTemperature', $pb.PbFieldType.OF) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EnvironmentMetrics clone() => EnvironmentMetrics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + EnvironmentMetrics copyWith(void Function(EnvironmentMetrics) updates) => + super.copyWith((message) => updates(message as EnvironmentMetrics)) + as EnvironmentMetrics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EnvironmentMetrics create() => EnvironmentMetrics._(); + @$core.override + EnvironmentMetrics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EnvironmentMetrics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static EnvironmentMetrics? _defaultInstance; + + /// + /// Temperature measured + @$pb.TagNumber(1) + $core.double get temperature => $_getN(0); + @$pb.TagNumber(1) + set temperature($core.double value) => $_setFloat(0, value); + @$pb.TagNumber(1) + $core.bool hasTemperature() => $_has(0); + @$pb.TagNumber(1) + void clearTemperature() => $_clearField(1); + + /// + /// Relative humidity percent measured + @$pb.TagNumber(2) + $core.double get relativeHumidity => $_getN(1); + @$pb.TagNumber(2) + set relativeHumidity($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasRelativeHumidity() => $_has(1); + @$pb.TagNumber(2) + void clearRelativeHumidity() => $_clearField(2); + + /// + /// Barometric pressure in hPA measured + @$pb.TagNumber(3) + $core.double get barometricPressure => $_getN(2); + @$pb.TagNumber(3) + set barometricPressure($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasBarometricPressure() => $_has(2); + @$pb.TagNumber(3) + void clearBarometricPressure() => $_clearField(3); + + /// + /// Gas resistance in MOhm measured + @$pb.TagNumber(4) + $core.double get gasResistance => $_getN(3); + @$pb.TagNumber(4) + set gasResistance($core.double value) => $_setFloat(3, value); + @$pb.TagNumber(4) + $core.bool hasGasResistance() => $_has(3); + @$pb.TagNumber(4) + void clearGasResistance() => $_clearField(4); + + /// + /// Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) + @$pb.TagNumber(5) + $core.double get voltage => $_getN(4); + @$pb.TagNumber(5) + set voltage($core.double value) => $_setFloat(4, value); + @$pb.TagNumber(5) + $core.bool hasVoltage() => $_has(4); + @$pb.TagNumber(5) + void clearVoltage() => $_clearField(5); + + /// + /// Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) + @$pb.TagNumber(6) + $core.double get current => $_getN(5); + @$pb.TagNumber(6) + set current($core.double value) => $_setFloat(5, value); + @$pb.TagNumber(6) + $core.bool hasCurrent() => $_has(5); + @$pb.TagNumber(6) + void clearCurrent() => $_clearField(6); + + /// + /// relative scale IAQ value as measured by Bosch BME680 . value 0-500. + /// Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. + @$pb.TagNumber(7) + $core.int get iaq => $_getIZ(6); + @$pb.TagNumber(7) + set iaq($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasIaq() => $_has(6); + @$pb.TagNumber(7) + void clearIaq() => $_clearField(7); + + /// + /// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. + @$pb.TagNumber(8) + $core.double get distance => $_getN(7); + @$pb.TagNumber(8) + set distance($core.double value) => $_setFloat(7, value); + @$pb.TagNumber(8) + $core.bool hasDistance() => $_has(7); + @$pb.TagNumber(8) + void clearDistance() => $_clearField(8); + + /// + /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. + @$pb.TagNumber(9) + $core.double get lux => $_getN(8); + @$pb.TagNumber(9) + set lux($core.double value) => $_setFloat(8, value); + @$pb.TagNumber(9) + $core.bool hasLux() => $_has(8); + @$pb.TagNumber(9) + void clearLux() => $_clearField(9); + + /// + /// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. + @$pb.TagNumber(10) + $core.double get whiteLux => $_getN(9); + @$pb.TagNumber(10) + set whiteLux($core.double value) => $_setFloat(9, value); + @$pb.TagNumber(10) + $core.bool hasWhiteLux() => $_has(9); + @$pb.TagNumber(10) + void clearWhiteLux() => $_clearField(10); + + /// + /// Infrared lux + @$pb.TagNumber(11) + $core.double get irLux => $_getN(10); + @$pb.TagNumber(11) + set irLux($core.double value) => $_setFloat(10, value); + @$pb.TagNumber(11) + $core.bool hasIrLux() => $_has(10); + @$pb.TagNumber(11) + void clearIrLux() => $_clearField(11); + + /// + /// Ultraviolet lux + @$pb.TagNumber(12) + $core.double get uvLux => $_getN(11); + @$pb.TagNumber(12) + set uvLux($core.double value) => $_setFloat(11, value); + @$pb.TagNumber(12) + $core.bool hasUvLux() => $_has(11); + @$pb.TagNumber(12) + void clearUvLux() => $_clearField(12); + + /// + /// Wind direction in degrees + /// 0 degrees = North, 90 = East, etc... + @$pb.TagNumber(13) + $core.int get windDirection => $_getIZ(12); + @$pb.TagNumber(13) + set windDirection($core.int value) => $_setUnsignedInt32(12, value); + @$pb.TagNumber(13) + $core.bool hasWindDirection() => $_has(12); + @$pb.TagNumber(13) + void clearWindDirection() => $_clearField(13); + + /// + /// Wind speed in m/s + @$pb.TagNumber(14) + $core.double get windSpeed => $_getN(13); + @$pb.TagNumber(14) + set windSpeed($core.double value) => $_setFloat(13, value); + @$pb.TagNumber(14) + $core.bool hasWindSpeed() => $_has(13); + @$pb.TagNumber(14) + void clearWindSpeed() => $_clearField(14); + + /// + /// Weight in KG + @$pb.TagNumber(15) + $core.double get weight => $_getN(14); + @$pb.TagNumber(15) + set weight($core.double value) => $_setFloat(14, value); + @$pb.TagNumber(15) + $core.bool hasWeight() => $_has(14); + @$pb.TagNumber(15) + void clearWeight() => $_clearField(15); + + /// + /// Wind gust in m/s + @$pb.TagNumber(16) + $core.double get windGust => $_getN(15); + @$pb.TagNumber(16) + set windGust($core.double value) => $_setFloat(15, value); + @$pb.TagNumber(16) + $core.bool hasWindGust() => $_has(15); + @$pb.TagNumber(16) + void clearWindGust() => $_clearField(16); + + /// + /// Wind lull in m/s + @$pb.TagNumber(17) + $core.double get windLull => $_getN(16); + @$pb.TagNumber(17) + set windLull($core.double value) => $_setFloat(16, value); + @$pb.TagNumber(17) + $core.bool hasWindLull() => $_has(16); + @$pb.TagNumber(17) + void clearWindLull() => $_clearField(17); + + /// + /// Radiation in µR/h + @$pb.TagNumber(18) + $core.double get radiation => $_getN(17); + @$pb.TagNumber(18) + set radiation($core.double value) => $_setFloat(17, value); + @$pb.TagNumber(18) + $core.bool hasRadiation() => $_has(17); + @$pb.TagNumber(18) + void clearRadiation() => $_clearField(18); + + /// + /// Rainfall in the last hour in mm + @$pb.TagNumber(19) + $core.double get rainfall1h => $_getN(18); + @$pb.TagNumber(19) + set rainfall1h($core.double value) => $_setFloat(18, value); + @$pb.TagNumber(19) + $core.bool hasRainfall1h() => $_has(18); + @$pb.TagNumber(19) + void clearRainfall1h() => $_clearField(19); + + /// + /// Rainfall in the last 24 hours in mm + @$pb.TagNumber(20) + $core.double get rainfall24h => $_getN(19); + @$pb.TagNumber(20) + set rainfall24h($core.double value) => $_setFloat(19, value); + @$pb.TagNumber(20) + $core.bool hasRainfall24h() => $_has(19); + @$pb.TagNumber(20) + void clearRainfall24h() => $_clearField(20); + + /// + /// Soil moisture measured (% 1-100) + @$pb.TagNumber(21) + $core.int get soilMoisture => $_getIZ(20); + @$pb.TagNumber(21) + set soilMoisture($core.int value) => $_setUnsignedInt32(20, value); + @$pb.TagNumber(21) + $core.bool hasSoilMoisture() => $_has(20); + @$pb.TagNumber(21) + void clearSoilMoisture() => $_clearField(21); + + /// + /// Soil temperature measured (*C) + @$pb.TagNumber(22) + $core.double get soilTemperature => $_getN(21); + @$pb.TagNumber(22) + set soilTemperature($core.double value) => $_setFloat(21, value); + @$pb.TagNumber(22) + $core.bool hasSoilTemperature() => $_has(21); + @$pb.TagNumber(22) + void clearSoilTemperature() => $_clearField(22); +} + +/// +/// Power Metrics (voltage / current / etc) +class PowerMetrics extends $pb.GeneratedMessage { + factory PowerMetrics({ + $core.double? ch1Voltage, + $core.double? ch1Current, + $core.double? ch2Voltage, + $core.double? ch2Current, + $core.double? ch3Voltage, + $core.double? ch3Current, + $core.double? ch4Voltage, + $core.double? ch4Current, + $core.double? ch5Voltage, + $core.double? ch5Current, + $core.double? ch6Voltage, + $core.double? ch6Current, + $core.double? ch7Voltage, + $core.double? ch7Current, + $core.double? ch8Voltage, + $core.double? ch8Current, + }) { + final result = create(); + if (ch1Voltage != null) result.ch1Voltage = ch1Voltage; + if (ch1Current != null) result.ch1Current = ch1Current; + if (ch2Voltage != null) result.ch2Voltage = ch2Voltage; + if (ch2Current != null) result.ch2Current = ch2Current; + if (ch3Voltage != null) result.ch3Voltage = ch3Voltage; + if (ch3Current != null) result.ch3Current = ch3Current; + if (ch4Voltage != null) result.ch4Voltage = ch4Voltage; + if (ch4Current != null) result.ch4Current = ch4Current; + if (ch5Voltage != null) result.ch5Voltage = ch5Voltage; + if (ch5Current != null) result.ch5Current = ch5Current; + if (ch6Voltage != null) result.ch6Voltage = ch6Voltage; + if (ch6Current != null) result.ch6Current = ch6Current; + if (ch7Voltage != null) result.ch7Voltage = ch7Voltage; + if (ch7Current != null) result.ch7Current = ch7Current; + if (ch8Voltage != null) result.ch8Voltage = ch8Voltage; + if (ch8Current != null) result.ch8Current = ch8Current; + return result; + } + + PowerMetrics._(); + + factory PowerMetrics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory PowerMetrics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'PowerMetrics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.double>( + 1, _omitFieldNames ? '' : 'ch1Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'ch1Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 3, _omitFieldNames ? '' : 'ch2Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 4, _omitFieldNames ? '' : 'ch2Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 5, _omitFieldNames ? '' : 'ch3Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 6, _omitFieldNames ? '' : 'ch3Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 7, _omitFieldNames ? '' : 'ch4Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 8, _omitFieldNames ? '' : 'ch4Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 9, _omitFieldNames ? '' : 'ch5Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 10, _omitFieldNames ? '' : 'ch5Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 11, _omitFieldNames ? '' : 'ch6Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 12, _omitFieldNames ? '' : 'ch6Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 13, _omitFieldNames ? '' : 'ch7Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 14, _omitFieldNames ? '' : 'ch7Current', $pb.PbFieldType.OF) + ..a<$core.double>( + 15, _omitFieldNames ? '' : 'ch8Voltage', $pb.PbFieldType.OF) + ..a<$core.double>( + 16, _omitFieldNames ? '' : 'ch8Current', $pb.PbFieldType.OF) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PowerMetrics clone() => PowerMetrics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + PowerMetrics copyWith(void Function(PowerMetrics) updates) => + super.copyWith((message) => updates(message as PowerMetrics)) + as PowerMetrics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PowerMetrics create() => PowerMetrics._(); + @$core.override + PowerMetrics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PowerMetrics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static PowerMetrics? _defaultInstance; + + /// + /// Voltage (Ch1) + @$pb.TagNumber(1) + $core.double get ch1Voltage => $_getN(0); + @$pb.TagNumber(1) + set ch1Voltage($core.double value) => $_setFloat(0, value); + @$pb.TagNumber(1) + $core.bool hasCh1Voltage() => $_has(0); + @$pb.TagNumber(1) + void clearCh1Voltage() => $_clearField(1); + + /// + /// Current (Ch1) + @$pb.TagNumber(2) + $core.double get ch1Current => $_getN(1); + @$pb.TagNumber(2) + set ch1Current($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasCh1Current() => $_has(1); + @$pb.TagNumber(2) + void clearCh1Current() => $_clearField(2); + + /// + /// Voltage (Ch2) + @$pb.TagNumber(3) + $core.double get ch2Voltage => $_getN(2); + @$pb.TagNumber(3) + set ch2Voltage($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasCh2Voltage() => $_has(2); + @$pb.TagNumber(3) + void clearCh2Voltage() => $_clearField(3); + + /// + /// Current (Ch2) + @$pb.TagNumber(4) + $core.double get ch2Current => $_getN(3); + @$pb.TagNumber(4) + set ch2Current($core.double value) => $_setFloat(3, value); + @$pb.TagNumber(4) + $core.bool hasCh2Current() => $_has(3); + @$pb.TagNumber(4) + void clearCh2Current() => $_clearField(4); + + /// + /// Voltage (Ch3) + @$pb.TagNumber(5) + $core.double get ch3Voltage => $_getN(4); + @$pb.TagNumber(5) + set ch3Voltage($core.double value) => $_setFloat(4, value); + @$pb.TagNumber(5) + $core.bool hasCh3Voltage() => $_has(4); + @$pb.TagNumber(5) + void clearCh3Voltage() => $_clearField(5); + + /// + /// Current (Ch3) + @$pb.TagNumber(6) + $core.double get ch3Current => $_getN(5); + @$pb.TagNumber(6) + set ch3Current($core.double value) => $_setFloat(5, value); + @$pb.TagNumber(6) + $core.bool hasCh3Current() => $_has(5); + @$pb.TagNumber(6) + void clearCh3Current() => $_clearField(6); + + /// + /// Voltage (Ch4) + @$pb.TagNumber(7) + $core.double get ch4Voltage => $_getN(6); + @$pb.TagNumber(7) + set ch4Voltage($core.double value) => $_setFloat(6, value); + @$pb.TagNumber(7) + $core.bool hasCh4Voltage() => $_has(6); + @$pb.TagNumber(7) + void clearCh4Voltage() => $_clearField(7); + + /// + /// Current (Ch4) + @$pb.TagNumber(8) + $core.double get ch4Current => $_getN(7); + @$pb.TagNumber(8) + set ch4Current($core.double value) => $_setFloat(7, value); + @$pb.TagNumber(8) + $core.bool hasCh4Current() => $_has(7); + @$pb.TagNumber(8) + void clearCh4Current() => $_clearField(8); + + /// + /// Voltage (Ch5) + @$pb.TagNumber(9) + $core.double get ch5Voltage => $_getN(8); + @$pb.TagNumber(9) + set ch5Voltage($core.double value) => $_setFloat(8, value); + @$pb.TagNumber(9) + $core.bool hasCh5Voltage() => $_has(8); + @$pb.TagNumber(9) + void clearCh5Voltage() => $_clearField(9); + + /// + /// Current (Ch5) + @$pb.TagNumber(10) + $core.double get ch5Current => $_getN(9); + @$pb.TagNumber(10) + set ch5Current($core.double value) => $_setFloat(9, value); + @$pb.TagNumber(10) + $core.bool hasCh5Current() => $_has(9); + @$pb.TagNumber(10) + void clearCh5Current() => $_clearField(10); + + /// + /// Voltage (Ch6) + @$pb.TagNumber(11) + $core.double get ch6Voltage => $_getN(10); + @$pb.TagNumber(11) + set ch6Voltage($core.double value) => $_setFloat(10, value); + @$pb.TagNumber(11) + $core.bool hasCh6Voltage() => $_has(10); + @$pb.TagNumber(11) + void clearCh6Voltage() => $_clearField(11); + + /// + /// Current (Ch6) + @$pb.TagNumber(12) + $core.double get ch6Current => $_getN(11); + @$pb.TagNumber(12) + set ch6Current($core.double value) => $_setFloat(11, value); + @$pb.TagNumber(12) + $core.bool hasCh6Current() => $_has(11); + @$pb.TagNumber(12) + void clearCh6Current() => $_clearField(12); + + /// + /// Voltage (Ch7) + @$pb.TagNumber(13) + $core.double get ch7Voltage => $_getN(12); + @$pb.TagNumber(13) + set ch7Voltage($core.double value) => $_setFloat(12, value); + @$pb.TagNumber(13) + $core.bool hasCh7Voltage() => $_has(12); + @$pb.TagNumber(13) + void clearCh7Voltage() => $_clearField(13); + + /// + /// Current (Ch7) + @$pb.TagNumber(14) + $core.double get ch7Current => $_getN(13); + @$pb.TagNumber(14) + set ch7Current($core.double value) => $_setFloat(13, value); + @$pb.TagNumber(14) + $core.bool hasCh7Current() => $_has(13); + @$pb.TagNumber(14) + void clearCh7Current() => $_clearField(14); + + /// + /// Voltage (Ch8) + @$pb.TagNumber(15) + $core.double get ch8Voltage => $_getN(14); + @$pb.TagNumber(15) + set ch8Voltage($core.double value) => $_setFloat(14, value); + @$pb.TagNumber(15) + $core.bool hasCh8Voltage() => $_has(14); + @$pb.TagNumber(15) + void clearCh8Voltage() => $_clearField(15); + + /// + /// Current (Ch8) + @$pb.TagNumber(16) + $core.double get ch8Current => $_getN(15); + @$pb.TagNumber(16) + set ch8Current($core.double value) => $_setFloat(15, value); + @$pb.TagNumber(16) + $core.bool hasCh8Current() => $_has(15); + @$pb.TagNumber(16) + void clearCh8Current() => $_clearField(16); +} + +/// +/// Air quality metrics +class AirQualityMetrics extends $pb.GeneratedMessage { + factory AirQualityMetrics({ + $core.int? pm10Standard, + $core.int? pm25Standard, + $core.int? pm100Standard, + $core.int? pm10Environmental, + $core.int? pm25Environmental, + $core.int? pm100Environmental, + $core.int? particles03um, + $core.int? particles05um, + $core.int? particles10um, + $core.int? particles25um, + $core.int? particles50um, + $core.int? particles100um, + $core.int? co2, + $core.double? co2Temperature, + $core.double? co2Humidity, + $core.double? formFormaldehyde, + $core.double? formHumidity, + $core.double? formTemperature, + $core.int? pm40Standard, + $core.int? particles40um, + $core.double? pmTemperature, + $core.double? pmHumidity, + $core.double? pmVocIdx, + $core.double? pmNoxIdx, + $core.double? particlesTps, + }) { + final result = create(); + if (pm10Standard != null) result.pm10Standard = pm10Standard; + if (pm25Standard != null) result.pm25Standard = pm25Standard; + if (pm100Standard != null) result.pm100Standard = pm100Standard; + if (pm10Environmental != null) result.pm10Environmental = pm10Environmental; + if (pm25Environmental != null) result.pm25Environmental = pm25Environmental; + if (pm100Environmental != null) + result.pm100Environmental = pm100Environmental; + if (particles03um != null) result.particles03um = particles03um; + if (particles05um != null) result.particles05um = particles05um; + if (particles10um != null) result.particles10um = particles10um; + if (particles25um != null) result.particles25um = particles25um; + if (particles50um != null) result.particles50um = particles50um; + if (particles100um != null) result.particles100um = particles100um; + if (co2 != null) result.co2 = co2; + if (co2Temperature != null) result.co2Temperature = co2Temperature; + if (co2Humidity != null) result.co2Humidity = co2Humidity; + if (formFormaldehyde != null) result.formFormaldehyde = formFormaldehyde; + if (formHumidity != null) result.formHumidity = formHumidity; + if (formTemperature != null) result.formTemperature = formTemperature; + if (pm40Standard != null) result.pm40Standard = pm40Standard; + if (particles40um != null) result.particles40um = particles40um; + if (pmTemperature != null) result.pmTemperature = pmTemperature; + if (pmHumidity != null) result.pmHumidity = pmHumidity; + if (pmVocIdx != null) result.pmVocIdx = pmVocIdx; + if (pmNoxIdx != null) result.pmNoxIdx = pmNoxIdx; + if (particlesTps != null) result.particlesTps = particlesTps; + return result; + } + + AirQualityMetrics._(); + + factory AirQualityMetrics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory AirQualityMetrics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'AirQualityMetrics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'pm10Standard', $pb.PbFieldType.OU3) + ..a<$core.int>( + 2, _omitFieldNames ? '' : 'pm25Standard', $pb.PbFieldType.OU3) + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'pm100Standard', $pb.PbFieldType.OU3) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'pm10Environmental', $pb.PbFieldType.OU3) + ..a<$core.int>( + 5, _omitFieldNames ? '' : 'pm25Environmental', $pb.PbFieldType.OU3) + ..a<$core.int>( + 6, _omitFieldNames ? '' : 'pm100Environmental', $pb.PbFieldType.OU3) + ..a<$core.int>( + 7, _omitFieldNames ? '' : 'particles03um', $pb.PbFieldType.OU3, + protoName: 'particles_03um') + ..a<$core.int>( + 8, _omitFieldNames ? '' : 'particles05um', $pb.PbFieldType.OU3, + protoName: 'particles_05um') + ..a<$core.int>( + 9, _omitFieldNames ? '' : 'particles10um', $pb.PbFieldType.OU3, + protoName: 'particles_10um') + ..a<$core.int>( + 10, _omitFieldNames ? '' : 'particles25um', $pb.PbFieldType.OU3, + protoName: 'particles_25um') + ..a<$core.int>( + 11, _omitFieldNames ? '' : 'particles50um', $pb.PbFieldType.OU3, + protoName: 'particles_50um') + ..a<$core.int>( + 12, _omitFieldNames ? '' : 'particles100um', $pb.PbFieldType.OU3, + protoName: 'particles_100um') + ..a<$core.int>(13, _omitFieldNames ? '' : 'co2', $pb.PbFieldType.OU3) + ..a<$core.double>( + 14, _omitFieldNames ? '' : 'co2Temperature', $pb.PbFieldType.OF) + ..a<$core.double>( + 15, _omitFieldNames ? '' : 'co2Humidity', $pb.PbFieldType.OF) + ..a<$core.double>( + 16, _omitFieldNames ? '' : 'formFormaldehyde', $pb.PbFieldType.OF) + ..a<$core.double>( + 17, _omitFieldNames ? '' : 'formHumidity', $pb.PbFieldType.OF) + ..a<$core.double>( + 18, _omitFieldNames ? '' : 'formTemperature', $pb.PbFieldType.OF) + ..a<$core.int>( + 19, _omitFieldNames ? '' : 'pm40Standard', $pb.PbFieldType.OU3) + ..a<$core.int>( + 20, _omitFieldNames ? '' : 'particles40um', $pb.PbFieldType.OU3, + protoName: 'particles_40um') + ..a<$core.double>( + 21, _omitFieldNames ? '' : 'pmTemperature', $pb.PbFieldType.OF) + ..a<$core.double>( + 22, _omitFieldNames ? '' : 'pmHumidity', $pb.PbFieldType.OF) + ..a<$core.double>(23, _omitFieldNames ? '' : 'pmVocIdx', $pb.PbFieldType.OF) + ..a<$core.double>(24, _omitFieldNames ? '' : 'pmNoxIdx', $pb.PbFieldType.OF) + ..a<$core.double>( + 25, _omitFieldNames ? '' : 'particlesTps', $pb.PbFieldType.OF) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AirQualityMetrics clone() => AirQualityMetrics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AirQualityMetrics copyWith(void Function(AirQualityMetrics) updates) => + super.copyWith((message) => updates(message as AirQualityMetrics)) + as AirQualityMetrics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AirQualityMetrics create() => AirQualityMetrics._(); + @$core.override + AirQualityMetrics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AirQualityMetrics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static AirQualityMetrics? _defaultInstance; + + /// + /// Concentration Units Standard PM1.0 in ug/m3 + @$pb.TagNumber(1) + $core.int get pm10Standard => $_getIZ(0); + @$pb.TagNumber(1) + set pm10Standard($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasPm10Standard() => $_has(0); + @$pb.TagNumber(1) + void clearPm10Standard() => $_clearField(1); + + /// + /// Concentration Units Standard PM2.5 in ug/m3 + @$pb.TagNumber(2) + $core.int get pm25Standard => $_getIZ(1); + @$pb.TagNumber(2) + set pm25Standard($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasPm25Standard() => $_has(1); + @$pb.TagNumber(2) + void clearPm25Standard() => $_clearField(2); + + /// + /// Concentration Units Standard PM10.0 in ug/m3 + @$pb.TagNumber(3) + $core.int get pm100Standard => $_getIZ(2); + @$pb.TagNumber(3) + set pm100Standard($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasPm100Standard() => $_has(2); + @$pb.TagNumber(3) + void clearPm100Standard() => $_clearField(3); + + /// + /// Concentration Units Environmental PM1.0 in ug/m3 + @$pb.TagNumber(4) + $core.int get pm10Environmental => $_getIZ(3); + @$pb.TagNumber(4) + set pm10Environmental($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasPm10Environmental() => $_has(3); + @$pb.TagNumber(4) + void clearPm10Environmental() => $_clearField(4); + + /// + /// Concentration Units Environmental PM2.5 in ug/m3 + @$pb.TagNumber(5) + $core.int get pm25Environmental => $_getIZ(4); + @$pb.TagNumber(5) + set pm25Environmental($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasPm25Environmental() => $_has(4); + @$pb.TagNumber(5) + void clearPm25Environmental() => $_clearField(5); + + /// + /// Concentration Units Environmental PM10.0 in ug/m3 + @$pb.TagNumber(6) + $core.int get pm100Environmental => $_getIZ(5); + @$pb.TagNumber(6) + set pm100Environmental($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasPm100Environmental() => $_has(5); + @$pb.TagNumber(6) + void clearPm100Environmental() => $_clearField(6); + + /// + /// 0.3um Particle Count in #/0.1l + @$pb.TagNumber(7) + $core.int get particles03um => $_getIZ(6); + @$pb.TagNumber(7) + set particles03um($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasParticles03um() => $_has(6); + @$pb.TagNumber(7) + void clearParticles03um() => $_clearField(7); + + /// + /// 0.5um Particle Count in #/0.1l + @$pb.TagNumber(8) + $core.int get particles05um => $_getIZ(7); + @$pb.TagNumber(8) + set particles05um($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasParticles05um() => $_has(7); + @$pb.TagNumber(8) + void clearParticles05um() => $_clearField(8); + + /// + /// 1.0um Particle Count in #/0.1l + @$pb.TagNumber(9) + $core.int get particles10um => $_getIZ(8); + @$pb.TagNumber(9) + set particles10um($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasParticles10um() => $_has(8); + @$pb.TagNumber(9) + void clearParticles10um() => $_clearField(9); + + /// + /// 2.5um Particle Count in #/0.1l + @$pb.TagNumber(10) + $core.int get particles25um => $_getIZ(9); + @$pb.TagNumber(10) + set particles25um($core.int value) => $_setUnsignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasParticles25um() => $_has(9); + @$pb.TagNumber(10) + void clearParticles25um() => $_clearField(10); + + /// + /// 5.0um Particle Count in #/0.1l + @$pb.TagNumber(11) + $core.int get particles50um => $_getIZ(10); + @$pb.TagNumber(11) + set particles50um($core.int value) => $_setUnsignedInt32(10, value); + @$pb.TagNumber(11) + $core.bool hasParticles50um() => $_has(10); + @$pb.TagNumber(11) + void clearParticles50um() => $_clearField(11); + + /// + /// 10.0um Particle Count in #/0.1l + @$pb.TagNumber(12) + $core.int get particles100um => $_getIZ(11); + @$pb.TagNumber(12) + set particles100um($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasParticles100um() => $_has(11); + @$pb.TagNumber(12) + void clearParticles100um() => $_clearField(12); + + /// + /// CO2 concentration in ppm + @$pb.TagNumber(13) + $core.int get co2 => $_getIZ(12); + @$pb.TagNumber(13) + set co2($core.int value) => $_setUnsignedInt32(12, value); + @$pb.TagNumber(13) + $core.bool hasCo2() => $_has(12); + @$pb.TagNumber(13) + void clearCo2() => $_clearField(13); + + /// + /// CO2 sensor temperature in degC + @$pb.TagNumber(14) + $core.double get co2Temperature => $_getN(13); + @$pb.TagNumber(14) + set co2Temperature($core.double value) => $_setFloat(13, value); + @$pb.TagNumber(14) + $core.bool hasCo2Temperature() => $_has(13); + @$pb.TagNumber(14) + void clearCo2Temperature() => $_clearField(14); + + /// + /// CO2 sensor relative humidity in % + @$pb.TagNumber(15) + $core.double get co2Humidity => $_getN(14); + @$pb.TagNumber(15) + set co2Humidity($core.double value) => $_setFloat(14, value); + @$pb.TagNumber(15) + $core.bool hasCo2Humidity() => $_has(14); + @$pb.TagNumber(15) + void clearCo2Humidity() => $_clearField(15); + + /// + /// Formaldehyde sensor formaldehyde concentration in ppb + @$pb.TagNumber(16) + $core.double get formFormaldehyde => $_getN(15); + @$pb.TagNumber(16) + set formFormaldehyde($core.double value) => $_setFloat(15, value); + @$pb.TagNumber(16) + $core.bool hasFormFormaldehyde() => $_has(15); + @$pb.TagNumber(16) + void clearFormFormaldehyde() => $_clearField(16); + + /// + /// Formaldehyde sensor relative humidity in %RH + @$pb.TagNumber(17) + $core.double get formHumidity => $_getN(16); + @$pb.TagNumber(17) + set formHumidity($core.double value) => $_setFloat(16, value); + @$pb.TagNumber(17) + $core.bool hasFormHumidity() => $_has(16); + @$pb.TagNumber(17) + void clearFormHumidity() => $_clearField(17); + + /// + /// Formaldehyde sensor temperature in degrees Celsius + @$pb.TagNumber(18) + $core.double get formTemperature => $_getN(17); + @$pb.TagNumber(18) + set formTemperature($core.double value) => $_setFloat(17, value); + @$pb.TagNumber(18) + $core.bool hasFormTemperature() => $_has(17); + @$pb.TagNumber(18) + void clearFormTemperature() => $_clearField(18); + + /// + /// Concentration Units Standard PM4.0 in ug/m3 + @$pb.TagNumber(19) + $core.int get pm40Standard => $_getIZ(18); + @$pb.TagNumber(19) + set pm40Standard($core.int value) => $_setUnsignedInt32(18, value); + @$pb.TagNumber(19) + $core.bool hasPm40Standard() => $_has(18); + @$pb.TagNumber(19) + void clearPm40Standard() => $_clearField(19); + + /// + /// 4.0um Particle Count in #/0.1l + @$pb.TagNumber(20) + $core.int get particles40um => $_getIZ(19); + @$pb.TagNumber(20) + set particles40um($core.int value) => $_setUnsignedInt32(19, value); + @$pb.TagNumber(20) + $core.bool hasParticles40um() => $_has(19); + @$pb.TagNumber(20) + void clearParticles40um() => $_clearField(20); + + /// + /// PM Sensor Temperature + @$pb.TagNumber(21) + $core.double get pmTemperature => $_getN(20); + @$pb.TagNumber(21) + set pmTemperature($core.double value) => $_setFloat(20, value); + @$pb.TagNumber(21) + $core.bool hasPmTemperature() => $_has(20); + @$pb.TagNumber(21) + void clearPmTemperature() => $_clearField(21); + + /// + /// PM Sensor humidity + @$pb.TagNumber(22) + $core.double get pmHumidity => $_getN(21); + @$pb.TagNumber(22) + set pmHumidity($core.double value) => $_setFloat(21, value); + @$pb.TagNumber(22) + $core.bool hasPmHumidity() => $_has(21); + @$pb.TagNumber(22) + void clearPmHumidity() => $_clearField(22); + + /// + /// PM Sensor VOC Index + @$pb.TagNumber(23) + $core.double get pmVocIdx => $_getN(22); + @$pb.TagNumber(23) + set pmVocIdx($core.double value) => $_setFloat(22, value); + @$pb.TagNumber(23) + $core.bool hasPmVocIdx() => $_has(22); + @$pb.TagNumber(23) + void clearPmVocIdx() => $_clearField(23); + + /// + /// PM Sensor NOx Index + @$pb.TagNumber(24) + $core.double get pmNoxIdx => $_getN(23); + @$pb.TagNumber(24) + set pmNoxIdx($core.double value) => $_setFloat(23, value); + @$pb.TagNumber(24) + $core.bool hasPmNoxIdx() => $_has(23); + @$pb.TagNumber(24) + void clearPmNoxIdx() => $_clearField(24); + + /// + /// Typical Particle Size in um + @$pb.TagNumber(25) + $core.double get particlesTps => $_getN(24); + @$pb.TagNumber(25) + set particlesTps($core.double value) => $_setFloat(24, value); + @$pb.TagNumber(25) + $core.bool hasParticlesTps() => $_has(24); + @$pb.TagNumber(25) + void clearParticlesTps() => $_clearField(25); +} + +/// +/// Local device mesh statistics +class LocalStats extends $pb.GeneratedMessage { + factory LocalStats({ + $core.int? uptimeSeconds, + $core.double? channelUtilization, + $core.double? airUtilTx, + $core.int? numPacketsTx, + $core.int? numPacketsRx, + $core.int? numPacketsRxBad, + $core.int? numOnlineNodes, + $core.int? numTotalNodes, + $core.int? numRxDupe, + $core.int? numTxRelay, + $core.int? numTxRelayCanceled, + $core.int? heapTotalBytes, + $core.int? heapFreeBytes, + }) { + final result = create(); + if (uptimeSeconds != null) result.uptimeSeconds = uptimeSeconds; + if (channelUtilization != null) + result.channelUtilization = channelUtilization; + if (airUtilTx != null) result.airUtilTx = airUtilTx; + if (numPacketsTx != null) result.numPacketsTx = numPacketsTx; + if (numPacketsRx != null) result.numPacketsRx = numPacketsRx; + if (numPacketsRxBad != null) result.numPacketsRxBad = numPacketsRxBad; + if (numOnlineNodes != null) result.numOnlineNodes = numOnlineNodes; + if (numTotalNodes != null) result.numTotalNodes = numTotalNodes; + if (numRxDupe != null) result.numRxDupe = numRxDupe; + if (numTxRelay != null) result.numTxRelay = numTxRelay; + if (numTxRelayCanceled != null) + result.numTxRelayCanceled = numTxRelayCanceled; + if (heapTotalBytes != null) result.heapTotalBytes = heapTotalBytes; + if (heapFreeBytes != null) result.heapFreeBytes = heapFreeBytes; + return result; + } + + LocalStats._(); + + factory LocalStats.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory LocalStats.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'LocalStats', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'uptimeSeconds', $pb.PbFieldType.OU3) + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'channelUtilization', $pb.PbFieldType.OF) + ..a<$core.double>(3, _omitFieldNames ? '' : 'airUtilTx', $pb.PbFieldType.OF) + ..a<$core.int>( + 4, _omitFieldNames ? '' : 'numPacketsTx', $pb.PbFieldType.OU3) + ..a<$core.int>( + 5, _omitFieldNames ? '' : 'numPacketsRx', $pb.PbFieldType.OU3) + ..a<$core.int>( + 6, _omitFieldNames ? '' : 'numPacketsRxBad', $pb.PbFieldType.OU3) + ..a<$core.int>( + 7, _omitFieldNames ? '' : 'numOnlineNodes', $pb.PbFieldType.OU3) + ..a<$core.int>( + 8, _omitFieldNames ? '' : 'numTotalNodes', $pb.PbFieldType.OU3) + ..a<$core.int>(9, _omitFieldNames ? '' : 'numRxDupe', $pb.PbFieldType.OU3) + ..a<$core.int>(10, _omitFieldNames ? '' : 'numTxRelay', $pb.PbFieldType.OU3) + ..a<$core.int>( + 11, _omitFieldNames ? '' : 'numTxRelayCanceled', $pb.PbFieldType.OU3) + ..a<$core.int>( + 12, _omitFieldNames ? '' : 'heapTotalBytes', $pb.PbFieldType.OU3) + ..a<$core.int>( + 13, _omitFieldNames ? '' : 'heapFreeBytes', $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LocalStats clone() => LocalStats()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + LocalStats copyWith(void Function(LocalStats) updates) => + super.copyWith((message) => updates(message as LocalStats)) as LocalStats; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LocalStats create() => LocalStats._(); + @$core.override + LocalStats createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LocalStats getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static LocalStats? _defaultInstance; + + /// + /// How long the device has been running since the last reboot (in seconds) + @$pb.TagNumber(1) + $core.int get uptimeSeconds => $_getIZ(0); + @$pb.TagNumber(1) + set uptimeSeconds($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasUptimeSeconds() => $_has(0); + @$pb.TagNumber(1) + void clearUptimeSeconds() => $_clearField(1); + + /// + /// Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). + @$pb.TagNumber(2) + $core.double get channelUtilization => $_getN(1); + @$pb.TagNumber(2) + set channelUtilization($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasChannelUtilization() => $_has(1); + @$pb.TagNumber(2) + void clearChannelUtilization() => $_clearField(2); + + /// + /// Percent of airtime for transmission used within the last hour. + @$pb.TagNumber(3) + $core.double get airUtilTx => $_getN(2); + @$pb.TagNumber(3) + set airUtilTx($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasAirUtilTx() => $_has(2); + @$pb.TagNumber(3) + void clearAirUtilTx() => $_clearField(3); + + /// + /// Number of packets sent + @$pb.TagNumber(4) + $core.int get numPacketsTx => $_getIZ(3); + @$pb.TagNumber(4) + set numPacketsTx($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasNumPacketsTx() => $_has(3); + @$pb.TagNumber(4) + void clearNumPacketsTx() => $_clearField(4); + + /// + /// Number of packets received (both good and bad) + @$pb.TagNumber(5) + $core.int get numPacketsRx => $_getIZ(4); + @$pb.TagNumber(5) + set numPacketsRx($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasNumPacketsRx() => $_has(4); + @$pb.TagNumber(5) + void clearNumPacketsRx() => $_clearField(5); + + /// + /// Number of packets received that are malformed or violate the protocol + @$pb.TagNumber(6) + $core.int get numPacketsRxBad => $_getIZ(5); + @$pb.TagNumber(6) + set numPacketsRxBad($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasNumPacketsRxBad() => $_has(5); + @$pb.TagNumber(6) + void clearNumPacketsRxBad() => $_clearField(6); + + /// + /// Number of nodes online (in the past 2 hours) + @$pb.TagNumber(7) + $core.int get numOnlineNodes => $_getIZ(6); + @$pb.TagNumber(7) + set numOnlineNodes($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasNumOnlineNodes() => $_has(6); + @$pb.TagNumber(7) + void clearNumOnlineNodes() => $_clearField(7); + + /// + /// Number of nodes total + @$pb.TagNumber(8) + $core.int get numTotalNodes => $_getIZ(7); + @$pb.TagNumber(8) + set numTotalNodes($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasNumTotalNodes() => $_has(7); + @$pb.TagNumber(8) + void clearNumTotalNodes() => $_clearField(8); + + /// + /// Number of received packets that were duplicates (due to multiple nodes relaying). + /// If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role. + @$pb.TagNumber(9) + $core.int get numRxDupe => $_getIZ(8); + @$pb.TagNumber(9) + set numRxDupe($core.int value) => $_setUnsignedInt32(8, value); + @$pb.TagNumber(9) + $core.bool hasNumRxDupe() => $_has(8); + @$pb.TagNumber(9) + void clearNumRxDupe() => $_clearField(9); + + /// + /// Number of packets we transmitted that were a relay for others (not originating from ourselves). + @$pb.TagNumber(10) + $core.int get numTxRelay => $_getIZ(9); + @$pb.TagNumber(10) + set numTxRelay($core.int value) => $_setUnsignedInt32(9, value); + @$pb.TagNumber(10) + $core.bool hasNumTxRelay() => $_has(9); + @$pb.TagNumber(10) + void clearNumTxRelay() => $_clearField(10); + + /// + /// Number of times we canceled a packet to be relayed, because someone else did it before us. + /// This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. + @$pb.TagNumber(11) + $core.int get numTxRelayCanceled => $_getIZ(10); + @$pb.TagNumber(11) + set numTxRelayCanceled($core.int value) => $_setUnsignedInt32(10, value); + @$pb.TagNumber(11) + $core.bool hasNumTxRelayCanceled() => $_has(10); + @$pb.TagNumber(11) + void clearNumTxRelayCanceled() => $_clearField(11); + + /// + /// Number of bytes used in the heap + @$pb.TagNumber(12) + $core.int get heapTotalBytes => $_getIZ(11); + @$pb.TagNumber(12) + set heapTotalBytes($core.int value) => $_setUnsignedInt32(11, value); + @$pb.TagNumber(12) + $core.bool hasHeapTotalBytes() => $_has(11); + @$pb.TagNumber(12) + void clearHeapTotalBytes() => $_clearField(12); + + /// + /// Number of bytes free in the heap + @$pb.TagNumber(13) + $core.int get heapFreeBytes => $_getIZ(12); + @$pb.TagNumber(13) + set heapFreeBytes($core.int value) => $_setUnsignedInt32(12, value); + @$pb.TagNumber(13) + $core.bool hasHeapFreeBytes() => $_has(12); + @$pb.TagNumber(13) + void clearHeapFreeBytes() => $_clearField(13); +} + +/// +/// Health telemetry metrics +class HealthMetrics extends $pb.GeneratedMessage { + factory HealthMetrics({ + $core.int? heartBpm, + $core.int? spO2, + $core.double? temperature, + }) { + final result = create(); + if (heartBpm != null) result.heartBpm = heartBpm; + if (spO2 != null) result.spO2 = spO2; + if (temperature != null) result.temperature = temperature; + return result; + } + + HealthMetrics._(); + + factory HealthMetrics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory HealthMetrics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'HealthMetrics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'heartBpm', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'spO2', $pb.PbFieldType.OU3, + protoName: 'spO2') + ..a<$core.double>( + 3, _omitFieldNames ? '' : 'temperature', $pb.PbFieldType.OF) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HealthMetrics clone() => HealthMetrics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HealthMetrics copyWith(void Function(HealthMetrics) updates) => + super.copyWith((message) => updates(message as HealthMetrics)) + as HealthMetrics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HealthMetrics create() => HealthMetrics._(); + @$core.override + HealthMetrics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static HealthMetrics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static HealthMetrics? _defaultInstance; + + /// + /// Heart rate (beats per minute) + @$pb.TagNumber(1) + $core.int get heartBpm => $_getIZ(0); + @$pb.TagNumber(1) + set heartBpm($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasHeartBpm() => $_has(0); + @$pb.TagNumber(1) + void clearHeartBpm() => $_clearField(1); + + /// + /// SpO2 (blood oxygen saturation) level + @$pb.TagNumber(2) + $core.int get spO2 => $_getIZ(1); + @$pb.TagNumber(2) + set spO2($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasSpO2() => $_has(1); + @$pb.TagNumber(2) + void clearSpO2() => $_clearField(2); + + /// + /// Body temperature in degrees Celsius + @$pb.TagNumber(3) + $core.double get temperature => $_getN(2); + @$pb.TagNumber(3) + set temperature($core.double value) => $_setFloat(2, value); + @$pb.TagNumber(3) + $core.bool hasTemperature() => $_has(2); + @$pb.TagNumber(3) + void clearTemperature() => $_clearField(3); +} + +/// +/// Linux host metrics +class HostMetrics extends $pb.GeneratedMessage { + factory HostMetrics({ + $core.int? uptimeSeconds, + $fixnum.Int64? freememBytes, + $fixnum.Int64? diskfree1Bytes, + $fixnum.Int64? diskfree2Bytes, + $fixnum.Int64? diskfree3Bytes, + $core.int? load1, + $core.int? load5, + $core.int? load15, + $core.String? userString, + }) { + final result = create(); + if (uptimeSeconds != null) result.uptimeSeconds = uptimeSeconds; + if (freememBytes != null) result.freememBytes = freememBytes; + if (diskfree1Bytes != null) result.diskfree1Bytes = diskfree1Bytes; + if (diskfree2Bytes != null) result.diskfree2Bytes = diskfree2Bytes; + if (diskfree3Bytes != null) result.diskfree3Bytes = diskfree3Bytes; + if (load1 != null) result.load1 = load1; + if (load5 != null) result.load5 = load5; + if (load15 != null) result.load15 = load15; + if (userString != null) result.userString = userString; + return result; + } + + HostMetrics._(); + + factory HostMetrics.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory HostMetrics.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'HostMetrics', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>( + 1, _omitFieldNames ? '' : 'uptimeSeconds', $pb.PbFieldType.OU3) + ..a<$fixnum.Int64>( + 2, _omitFieldNames ? '' : 'freememBytes', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>( + 3, _omitFieldNames ? '' : 'diskfree1Bytes', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>( + 4, _omitFieldNames ? '' : 'diskfree2Bytes', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>( + 5, _omitFieldNames ? '' : 'diskfree3Bytes', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.int>(6, _omitFieldNames ? '' : 'load1', $pb.PbFieldType.OU3) + ..a<$core.int>(7, _omitFieldNames ? '' : 'load5', $pb.PbFieldType.OU3) + ..a<$core.int>(8, _omitFieldNames ? '' : 'load15', $pb.PbFieldType.OU3) + ..aOS(9, _omitFieldNames ? '' : 'userString') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HostMetrics clone() => HostMetrics()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + HostMetrics copyWith(void Function(HostMetrics) updates) => + super.copyWith((message) => updates(message as HostMetrics)) + as HostMetrics; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HostMetrics create() => HostMetrics._(); + @$core.override + HostMetrics createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static HostMetrics getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static HostMetrics? _defaultInstance; + + /// + /// Host system uptime + @$pb.TagNumber(1) + $core.int get uptimeSeconds => $_getIZ(0); + @$pb.TagNumber(1) + set uptimeSeconds($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasUptimeSeconds() => $_has(0); + @$pb.TagNumber(1) + void clearUptimeSeconds() => $_clearField(1); + + /// + /// Host system free memory + @$pb.TagNumber(2) + $fixnum.Int64 get freememBytes => $_getI64(1); + @$pb.TagNumber(2) + set freememBytes($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasFreememBytes() => $_has(1); + @$pb.TagNumber(2) + void clearFreememBytes() => $_clearField(2); + + /// + /// Host system disk space free for / + @$pb.TagNumber(3) + $fixnum.Int64 get diskfree1Bytes => $_getI64(2); + @$pb.TagNumber(3) + set diskfree1Bytes($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasDiskfree1Bytes() => $_has(2); + @$pb.TagNumber(3) + void clearDiskfree1Bytes() => $_clearField(3); + + /// + /// Secondary system disk space free + @$pb.TagNumber(4) + $fixnum.Int64 get diskfree2Bytes => $_getI64(3); + @$pb.TagNumber(4) + set diskfree2Bytes($fixnum.Int64 value) => $_setInt64(3, value); + @$pb.TagNumber(4) + $core.bool hasDiskfree2Bytes() => $_has(3); + @$pb.TagNumber(4) + void clearDiskfree2Bytes() => $_clearField(4); + + /// + /// Tertiary disk space free + @$pb.TagNumber(5) + $fixnum.Int64 get diskfree3Bytes => $_getI64(4); + @$pb.TagNumber(5) + set diskfree3Bytes($fixnum.Int64 value) => $_setInt64(4, value); + @$pb.TagNumber(5) + $core.bool hasDiskfree3Bytes() => $_has(4); + @$pb.TagNumber(5) + void clearDiskfree3Bytes() => $_clearField(5); + + /// + /// Host system one minute load in 1/100ths + @$pb.TagNumber(6) + $core.int get load1 => $_getIZ(5); + @$pb.TagNumber(6) + set load1($core.int value) => $_setUnsignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasLoad1() => $_has(5); + @$pb.TagNumber(6) + void clearLoad1() => $_clearField(6); + + /// + /// Host system five minute load in 1/100ths + @$pb.TagNumber(7) + $core.int get load5 => $_getIZ(6); + @$pb.TagNumber(7) + set load5($core.int value) => $_setUnsignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasLoad5() => $_has(6); + @$pb.TagNumber(7) + void clearLoad5() => $_clearField(7); + + /// + /// Host system fifteen minute load in 1/100ths + @$pb.TagNumber(8) + $core.int get load15 => $_getIZ(7); + @$pb.TagNumber(8) + set load15($core.int value) => $_setUnsignedInt32(7, value); + @$pb.TagNumber(8) + $core.bool hasLoad15() => $_has(7); + @$pb.TagNumber(8) + void clearLoad15() => $_clearField(8); + + /// + /// Optional User-provided string for arbitrary host system information + /// that doesn't make sense as a dedicated entry. + @$pb.TagNumber(9) + $core.String get userString => $_getSZ(8); + @$pb.TagNumber(9) + set userString($core.String value) => $_setString(8, value); + @$pb.TagNumber(9) + $core.bool hasUserString() => $_has(8); + @$pb.TagNumber(9) + void clearUserString() => $_clearField(9); +} + +enum Telemetry_Variant { + deviceMetrics, + environmentMetrics, + airQualityMetrics, + powerMetrics, + localStats, + healthMetrics, + hostMetrics, + notSet +} + +/// +/// Types of Measurements the telemetry module is equipped to handle +class Telemetry extends $pb.GeneratedMessage { + factory Telemetry({ + $core.int? time, + DeviceMetrics? deviceMetrics, + EnvironmentMetrics? environmentMetrics, + AirQualityMetrics? airQualityMetrics, + PowerMetrics? powerMetrics, + LocalStats? localStats, + HealthMetrics? healthMetrics, + HostMetrics? hostMetrics, + }) { + final result = create(); + if (time != null) result.time = time; + if (deviceMetrics != null) result.deviceMetrics = deviceMetrics; + if (environmentMetrics != null) + result.environmentMetrics = environmentMetrics; + if (airQualityMetrics != null) result.airQualityMetrics = airQualityMetrics; + if (powerMetrics != null) result.powerMetrics = powerMetrics; + if (localStats != null) result.localStats = localStats; + if (healthMetrics != null) result.healthMetrics = healthMetrics; + if (hostMetrics != null) result.hostMetrics = hostMetrics; + return result; + } + + Telemetry._(); + + factory Telemetry.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Telemetry.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static const $core.Map<$core.int, Telemetry_Variant> _Telemetry_VariantByTag = + { + 2: Telemetry_Variant.deviceMetrics, + 3: Telemetry_Variant.environmentMetrics, + 4: Telemetry_Variant.airQualityMetrics, + 5: Telemetry_Variant.powerMetrics, + 6: Telemetry_Variant.localStats, + 7: Telemetry_Variant.healthMetrics, + 8: Telemetry_Variant.hostMetrics, + 0: Telemetry_Variant.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Telemetry', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..oo(0, [2, 3, 4, 5, 6, 7, 8]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'time', $pb.PbFieldType.OF3) + ..aOM(2, _omitFieldNames ? '' : 'deviceMetrics', + subBuilder: DeviceMetrics.create) + ..aOM(3, _omitFieldNames ? '' : 'environmentMetrics', + subBuilder: EnvironmentMetrics.create) + ..aOM(4, _omitFieldNames ? '' : 'airQualityMetrics', + subBuilder: AirQualityMetrics.create) + ..aOM(5, _omitFieldNames ? '' : 'powerMetrics', + subBuilder: PowerMetrics.create) + ..aOM(6, _omitFieldNames ? '' : 'localStats', + subBuilder: LocalStats.create) + ..aOM(7, _omitFieldNames ? '' : 'healthMetrics', + subBuilder: HealthMetrics.create) + ..aOM(8, _omitFieldNames ? '' : 'hostMetrics', + subBuilder: HostMetrics.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Telemetry clone() => Telemetry()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Telemetry copyWith(void Function(Telemetry) updates) => + super.copyWith((message) => updates(message as Telemetry)) as Telemetry; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Telemetry create() => Telemetry._(); + @$core.override + Telemetry createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Telemetry getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Telemetry? _defaultInstance; + + Telemetry_Variant whichVariant() => _Telemetry_VariantByTag[$_whichOneof(0)]!; + void clearVariant() => $_clearField($_whichOneof(0)); + + /// + /// Seconds since 1970 - or 0 for unknown/unset + @$pb.TagNumber(1) + $core.int get time => $_getIZ(0); + @$pb.TagNumber(1) + set time($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasTime() => $_has(0); + @$pb.TagNumber(1) + void clearTime() => $_clearField(1); + + /// + /// Key native device metrics such as battery level + @$pb.TagNumber(2) + DeviceMetrics get deviceMetrics => $_getN(1); + @$pb.TagNumber(2) + set deviceMetrics(DeviceMetrics value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasDeviceMetrics() => $_has(1); + @$pb.TagNumber(2) + void clearDeviceMetrics() => $_clearField(2); + @$pb.TagNumber(2) + DeviceMetrics ensureDeviceMetrics() => $_ensure(1); + + /// + /// Weather station or other environmental metrics + @$pb.TagNumber(3) + EnvironmentMetrics get environmentMetrics => $_getN(2); + @$pb.TagNumber(3) + set environmentMetrics(EnvironmentMetrics value) => $_setField(3, value); + @$pb.TagNumber(3) + $core.bool hasEnvironmentMetrics() => $_has(2); + @$pb.TagNumber(3) + void clearEnvironmentMetrics() => $_clearField(3); + @$pb.TagNumber(3) + EnvironmentMetrics ensureEnvironmentMetrics() => $_ensure(2); + + /// + /// Air quality metrics + @$pb.TagNumber(4) + AirQualityMetrics get airQualityMetrics => $_getN(3); + @$pb.TagNumber(4) + set airQualityMetrics(AirQualityMetrics value) => $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasAirQualityMetrics() => $_has(3); + @$pb.TagNumber(4) + void clearAirQualityMetrics() => $_clearField(4); + @$pb.TagNumber(4) + AirQualityMetrics ensureAirQualityMetrics() => $_ensure(3); + + /// + /// Power Metrics + @$pb.TagNumber(5) + PowerMetrics get powerMetrics => $_getN(4); + @$pb.TagNumber(5) + set powerMetrics(PowerMetrics value) => $_setField(5, value); + @$pb.TagNumber(5) + $core.bool hasPowerMetrics() => $_has(4); + @$pb.TagNumber(5) + void clearPowerMetrics() => $_clearField(5); + @$pb.TagNumber(5) + PowerMetrics ensurePowerMetrics() => $_ensure(4); + + /// + /// Local device mesh statistics + @$pb.TagNumber(6) + LocalStats get localStats => $_getN(5); + @$pb.TagNumber(6) + set localStats(LocalStats value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasLocalStats() => $_has(5); + @$pb.TagNumber(6) + void clearLocalStats() => $_clearField(6); + @$pb.TagNumber(6) + LocalStats ensureLocalStats() => $_ensure(5); + + /// + /// Health telemetry metrics + @$pb.TagNumber(7) + HealthMetrics get healthMetrics => $_getN(6); + @$pb.TagNumber(7) + set healthMetrics(HealthMetrics value) => $_setField(7, value); + @$pb.TagNumber(7) + $core.bool hasHealthMetrics() => $_has(6); + @$pb.TagNumber(7) + void clearHealthMetrics() => $_clearField(7); + @$pb.TagNumber(7) + HealthMetrics ensureHealthMetrics() => $_ensure(6); + + /// + /// Linux host metrics + @$pb.TagNumber(8) + HostMetrics get hostMetrics => $_getN(7); + @$pb.TagNumber(8) + set hostMetrics(HostMetrics value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasHostMetrics() => $_has(7); + @$pb.TagNumber(8) + void clearHostMetrics() => $_clearField(8); + @$pb.TagNumber(8) + HostMetrics ensureHostMetrics() => $_ensure(7); +} + +/// +/// NAU7802 Telemetry configuration, for saving to flash +class Nau7802Config extends $pb.GeneratedMessage { + factory Nau7802Config({ + $core.int? zeroOffset, + $core.double? calibrationFactor, + }) { + final result = create(); + if (zeroOffset != null) result.zeroOffset = zeroOffset; + if (calibrationFactor != null) result.calibrationFactor = calibrationFactor; + return result; + } + + Nau7802Config._(); + + factory Nau7802Config.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Nau7802Config.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Nau7802Config', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'zeroOffset', $pb.PbFieldType.O3, + protoName: 'zeroOffset') + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'calibrationFactor', $pb.PbFieldType.OF, + protoName: 'calibrationFactor') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Nau7802Config clone() => Nau7802Config()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Nau7802Config copyWith(void Function(Nau7802Config) updates) => + super.copyWith((message) => updates(message as Nau7802Config)) + as Nau7802Config; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Nau7802Config create() => Nau7802Config._(); + @$core.override + Nau7802Config createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Nau7802Config getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Nau7802Config? _defaultInstance; + + /// + /// The offset setting for the NAU7802 + @$pb.TagNumber(1) + $core.int get zeroOffset => $_getIZ(0); + @$pb.TagNumber(1) + set zeroOffset($core.int value) => $_setSignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasZeroOffset() => $_has(0); + @$pb.TagNumber(1) + void clearZeroOffset() => $_clearField(1); + + /// + /// The calibration factor for the NAU7802 + @$pb.TagNumber(2) + $core.double get calibrationFactor => $_getN(1); + @$pb.TagNumber(2) + set calibrationFactor($core.double value) => $_setFloat(1, value); + @$pb.TagNumber(2) + $core.bool hasCalibrationFactor() => $_has(1); + @$pb.TagNumber(2) + void clearCalibrationFactor() => $_clearField(2); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/telemetry.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/telemetry.pbenum.dart new file mode 100644 index 000000000..0f317df44 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/telemetry.pbenum.dart @@ -0,0 +1,296 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/telemetry.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// +/// Supported I2C Sensors for telemetry in Meshtastic +class TelemetrySensorType extends $pb.ProtobufEnum { + /// + /// No external telemetry sensor explicitly set + static const TelemetrySensorType SENSOR_UNSET = + TelemetrySensorType._(0, _omitEnumNames ? '' : 'SENSOR_UNSET'); + + /// + /// High accuracy temperature, pressure, humidity + static const TelemetrySensorType BME280 = + TelemetrySensorType._(1, _omitEnumNames ? '' : 'BME280'); + + /// + /// High accuracy temperature, pressure, humidity, and air resistance + static const TelemetrySensorType BME680 = + TelemetrySensorType._(2, _omitEnumNames ? '' : 'BME680'); + + /// + /// Very high accuracy temperature + static const TelemetrySensorType MCP9808 = + TelemetrySensorType._(3, _omitEnumNames ? '' : 'MCP9808'); + + /// + /// Moderate accuracy current and voltage + static const TelemetrySensorType INA260 = + TelemetrySensorType._(4, _omitEnumNames ? '' : 'INA260'); + + /// + /// Moderate accuracy current and voltage + static const TelemetrySensorType INA219 = + TelemetrySensorType._(5, _omitEnumNames ? '' : 'INA219'); + + /// + /// High accuracy temperature and pressure + static const TelemetrySensorType BMP280 = + TelemetrySensorType._(6, _omitEnumNames ? '' : 'BMP280'); + + /// + /// High accuracy temperature and humidity + static const TelemetrySensorType SHTC3 = + TelemetrySensorType._(7, _omitEnumNames ? '' : 'SHTC3'); + + /// + /// High accuracy pressure + static const TelemetrySensorType LPS22 = + TelemetrySensorType._(8, _omitEnumNames ? '' : 'LPS22'); + + /// + /// 3-Axis magnetic sensor + static const TelemetrySensorType QMC6310 = + TelemetrySensorType._(9, _omitEnumNames ? '' : 'QMC6310'); + + /// + /// 6-Axis inertial measurement sensor + static const TelemetrySensorType QMI8658 = + TelemetrySensorType._(10, _omitEnumNames ? '' : 'QMI8658'); + + /// + /// 3-Axis magnetic sensor + static const TelemetrySensorType QMC5883L = + TelemetrySensorType._(11, _omitEnumNames ? '' : 'QMC5883L'); + + /// + /// High accuracy temperature and humidity + static const TelemetrySensorType SHT31 = + TelemetrySensorType._(12, _omitEnumNames ? '' : 'SHT31'); + + /// + /// PM2.5 air quality sensor + static const TelemetrySensorType PMSA003I = + TelemetrySensorType._(13, _omitEnumNames ? '' : 'PMSA003I'); + + /// + /// INA3221 3 Channel Voltage / Current Sensor + static const TelemetrySensorType INA3221 = + TelemetrySensorType._(14, _omitEnumNames ? '' : 'INA3221'); + + /// + /// BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) + static const TelemetrySensorType BMP085 = + TelemetrySensorType._(15, _omitEnumNames ? '' : 'BMP085'); + + /// + /// RCWL-9620 Doppler Radar Distance Sensor, used for water level detection + static const TelemetrySensorType RCWL9620 = + TelemetrySensorType._(16, _omitEnumNames ? '' : 'RCWL9620'); + + /// + /// Sensirion High accuracy temperature and humidity + static const TelemetrySensorType SHT4X = + TelemetrySensorType._(17, _omitEnumNames ? '' : 'SHT4X'); + + /// + /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. + static const TelemetrySensorType VEML7700 = + TelemetrySensorType._(18, _omitEnumNames ? '' : 'VEML7700'); + + /// + /// MLX90632 non-contact IR temperature sensor. + static const TelemetrySensorType MLX90632 = + TelemetrySensorType._(19, _omitEnumNames ? '' : 'MLX90632'); + + /// + /// TI OPT3001 Ambient Light Sensor + static const TelemetrySensorType OPT3001 = + TelemetrySensorType._(20, _omitEnumNames ? '' : 'OPT3001'); + + /// + /// Lite On LTR-390UV-01 UV Light Sensor + static const TelemetrySensorType LTR390UV = + TelemetrySensorType._(21, _omitEnumNames ? '' : 'LTR390UV'); + + /// + /// AMS TSL25911FN RGB Light Sensor + static const TelemetrySensorType TSL25911FN = + TelemetrySensorType._(22, _omitEnumNames ? '' : 'TSL25911FN'); + + /// + /// AHT10 Integrated temperature and humidity sensor + static const TelemetrySensorType AHT10 = + TelemetrySensorType._(23, _omitEnumNames ? '' : 'AHT10'); + + /// + /// DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction) + static const TelemetrySensorType DFROBOT_LARK = + TelemetrySensorType._(24, _omitEnumNames ? '' : 'DFROBOT_LARK'); + + /// + /// NAU7802 Scale Chip or compatible + static const TelemetrySensorType NAU7802 = + TelemetrySensorType._(25, _omitEnumNames ? '' : 'NAU7802'); + + /// + /// BMP3XX High accuracy temperature and pressure + static const TelemetrySensorType BMP3XX = + TelemetrySensorType._(26, _omitEnumNames ? '' : 'BMP3XX'); + + /// + /// ICM-20948 9-Axis digital motion processor + static const TelemetrySensorType ICM20948 = + TelemetrySensorType._(27, _omitEnumNames ? '' : 'ICM20948'); + + /// + /// MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) + static const TelemetrySensorType MAX17048 = + TelemetrySensorType._(28, _omitEnumNames ? '' : 'MAX17048'); + + /// + /// Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor + static const TelemetrySensorType CUSTOM_SENSOR = + TelemetrySensorType._(29, _omitEnumNames ? '' : 'CUSTOM_SENSOR'); + + /// + /// MAX30102 Pulse Oximeter and Heart-Rate Sensor + static const TelemetrySensorType MAX30102 = + TelemetrySensorType._(30, _omitEnumNames ? '' : 'MAX30102'); + + /// + /// MLX90614 non-contact IR temperature sensor + static const TelemetrySensorType MLX90614 = + TelemetrySensorType._(31, _omitEnumNames ? '' : 'MLX90614'); + + /// + /// SCD40/SCD41 CO2, humidity, temperature sensor + static const TelemetrySensorType SCD4X = + TelemetrySensorType._(32, _omitEnumNames ? '' : 'SCD4X'); + + /// + /// ClimateGuard RadSens, radiation, Geiger-Muller Tube + static const TelemetrySensorType RADSENS = + TelemetrySensorType._(33, _omitEnumNames ? '' : 'RADSENS'); + + /// + /// High accuracy current and voltage + static const TelemetrySensorType INA226 = + TelemetrySensorType._(34, _omitEnumNames ? '' : 'INA226'); + + /// + /// DFRobot Gravity tipping bucket rain gauge + static const TelemetrySensorType DFROBOT_RAIN = + TelemetrySensorType._(35, _omitEnumNames ? '' : 'DFROBOT_RAIN'); + + /// + /// Infineon DPS310 High accuracy pressure and temperature + static const TelemetrySensorType DPS310 = + TelemetrySensorType._(36, _omitEnumNames ? '' : 'DPS310'); + + /// + /// RAKWireless RAK12035 Soil Moisture Sensor Module + static const TelemetrySensorType RAK12035 = + TelemetrySensorType._(37, _omitEnumNames ? '' : 'RAK12035'); + + /// + /// MAX17261 lipo battery gauge + static const TelemetrySensorType MAX17261 = + TelemetrySensorType._(38, _omitEnumNames ? '' : 'MAX17261'); + + /// + /// PCT2075 Temperature Sensor + static const TelemetrySensorType PCT2075 = + TelemetrySensorType._(39, _omitEnumNames ? '' : 'PCT2075'); + + /// + /// ADS1X15 ADC + static const TelemetrySensorType ADS1X15 = + TelemetrySensorType._(40, _omitEnumNames ? '' : 'ADS1X15'); + + /// + /// ADS1X15 ADC_ALT + static const TelemetrySensorType ADS1X15_ALT = + TelemetrySensorType._(41, _omitEnumNames ? '' : 'ADS1X15_ALT'); + + /// + /// Sensirion SFA30 Formaldehyde sensor + static const TelemetrySensorType SFA30 = + TelemetrySensorType._(42, _omitEnumNames ? '' : 'SFA30'); + + /// + /// SEN5X PM SENSORS + static const TelemetrySensorType SEN5X = + TelemetrySensorType._(43, _omitEnumNames ? '' : 'SEN5X'); + + static const $core.List values = [ + SENSOR_UNSET, + BME280, + BME680, + MCP9808, + INA260, + INA219, + BMP280, + SHTC3, + LPS22, + QMC6310, + QMI8658, + QMC5883L, + SHT31, + PMSA003I, + INA3221, + BMP085, + RCWL9620, + SHT4X, + VEML7700, + MLX90632, + OPT3001, + LTR390UV, + TSL25911FN, + AHT10, + DFROBOT_LARK, + NAU7802, + BMP3XX, + ICM20948, + MAX17048, + CUSTOM_SENSOR, + MAX30102, + MLX90614, + SCD4X, + RADSENS, + INA226, + DFROBOT_RAIN, + DPS310, + RAK12035, + MAX17261, + PCT2075, + ADS1X15, + ADS1X15_ALT, + SFA30, + SEN5X, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 43); + static TelemetrySensorType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const TelemetrySensorType._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/telemetry.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/telemetry.pbjson.dart new file mode 100644 index 000000000..f7c36e871 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/telemetry.pbjson.dart @@ -0,0 +1,1106 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/telemetry.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use telemetrySensorTypeDescriptor instead') +const TelemetrySensorType$json = { + '1': 'TelemetrySensorType', + '2': [ + {'1': 'SENSOR_UNSET', '2': 0}, + {'1': 'BME280', '2': 1}, + {'1': 'BME680', '2': 2}, + {'1': 'MCP9808', '2': 3}, + {'1': 'INA260', '2': 4}, + {'1': 'INA219', '2': 5}, + {'1': 'BMP280', '2': 6}, + {'1': 'SHTC3', '2': 7}, + {'1': 'LPS22', '2': 8}, + {'1': 'QMC6310', '2': 9}, + {'1': 'QMI8658', '2': 10}, + {'1': 'QMC5883L', '2': 11}, + {'1': 'SHT31', '2': 12}, + {'1': 'PMSA003I', '2': 13}, + {'1': 'INA3221', '2': 14}, + {'1': 'BMP085', '2': 15}, + {'1': 'RCWL9620', '2': 16}, + {'1': 'SHT4X', '2': 17}, + {'1': 'VEML7700', '2': 18}, + {'1': 'MLX90632', '2': 19}, + {'1': 'OPT3001', '2': 20}, + {'1': 'LTR390UV', '2': 21}, + {'1': 'TSL25911FN', '2': 22}, + {'1': 'AHT10', '2': 23}, + {'1': 'DFROBOT_LARK', '2': 24}, + {'1': 'NAU7802', '2': 25}, + {'1': 'BMP3XX', '2': 26}, + {'1': 'ICM20948', '2': 27}, + {'1': 'MAX17048', '2': 28}, + {'1': 'CUSTOM_SENSOR', '2': 29}, + {'1': 'MAX30102', '2': 30}, + {'1': 'MLX90614', '2': 31}, + {'1': 'SCD4X', '2': 32}, + {'1': 'RADSENS', '2': 33}, + {'1': 'INA226', '2': 34}, + {'1': 'DFROBOT_RAIN', '2': 35}, + {'1': 'DPS310', '2': 36}, + {'1': 'RAK12035', '2': 37}, + {'1': 'MAX17261', '2': 38}, + {'1': 'PCT2075', '2': 39}, + {'1': 'ADS1X15', '2': 40}, + {'1': 'ADS1X15_ALT', '2': 41}, + {'1': 'SFA30', '2': 42}, + {'1': 'SEN5X', '2': 43}, + ], +}; + +/// Descriptor for `TelemetrySensorType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List telemetrySensorTypeDescriptor = $convert.base64Decode( + 'ChNUZWxlbWV0cnlTZW5zb3JUeXBlEhAKDFNFTlNPUl9VTlNFVBAAEgoKBkJNRTI4MBABEgoKBk' + 'JNRTY4MBACEgsKB01DUDk4MDgQAxIKCgZJTkEyNjAQBBIKCgZJTkEyMTkQBRIKCgZCTVAyODAQ' + 'BhIJCgVTSFRDMxAHEgkKBUxQUzIyEAgSCwoHUU1DNjMxMBAJEgsKB1FNSTg2NTgQChIMCghRTU' + 'M1ODgzTBALEgkKBVNIVDMxEAwSDAoIUE1TQTAwM0kQDRILCgdJTkEzMjIxEA4SCgoGQk1QMDg1' + 'EA8SDAoIUkNXTDk2MjAQEBIJCgVTSFQ0WBAREgwKCFZFTUw3NzAwEBISDAoITUxYOTA2MzIQEx' + 'ILCgdPUFQzMDAxEBQSDAoITFRSMzkwVVYQFRIOCgpUU0wyNTkxMUZOEBYSCQoFQUhUMTAQFxIQ' + 'CgxERlJPQk9UX0xBUksQGBILCgdOQVU3ODAyEBkSCgoGQk1QM1hYEBoSDAoISUNNMjA5NDgQGx' + 'IMCghNQVgxNzA0OBAcEhEKDUNVU1RPTV9TRU5TT1IQHRIMCghNQVgzMDEwMhAeEgwKCE1MWDkw' + 'NjE0EB8SCQoFU0NENFgQIBILCgdSQURTRU5TECESCgoGSU5BMjI2ECISEAoMREZST0JPVF9SQU' + 'lOECMSCgoGRFBTMzEwECQSDAoIUkFLMTIwMzUQJRIMCghNQVgxNzI2MRAmEgsKB1BDVDIwNzUQ' + 'JxILCgdBRFMxWDE1ECgSDwoLQURTMVgxNV9BTFQQKRIJCgVTRkEzMBAqEgkKBVNFTjVYECs='); + +@$core.Deprecated('Use deviceMetricsDescriptor instead') +const DeviceMetrics$json = { + '1': 'DeviceMetrics', + '2': [ + { + '1': 'battery_level', + '3': 1, + '4': 1, + '5': 13, + '9': 0, + '10': 'batteryLevel', + '17': true + }, + { + '1': 'voltage', + '3': 2, + '4': 1, + '5': 2, + '9': 1, + '10': 'voltage', + '17': true + }, + { + '1': 'channel_utilization', + '3': 3, + '4': 1, + '5': 2, + '9': 2, + '10': 'channelUtilization', + '17': true + }, + { + '1': 'air_util_tx', + '3': 4, + '4': 1, + '5': 2, + '9': 3, + '10': 'airUtilTx', + '17': true + }, + { + '1': 'uptime_seconds', + '3': 5, + '4': 1, + '5': 13, + '9': 4, + '10': 'uptimeSeconds', + '17': true + }, + ], + '8': [ + {'1': '_battery_level'}, + {'1': '_voltage'}, + {'1': '_channel_utilization'}, + {'1': '_air_util_tx'}, + {'1': '_uptime_seconds'}, + ], +}; + +/// Descriptor for `DeviceMetrics`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceMetricsDescriptor = $convert.base64Decode( + 'Cg1EZXZpY2VNZXRyaWNzEigKDWJhdHRlcnlfbGV2ZWwYASABKA1IAFIMYmF0dGVyeUxldmVsiA' + 'EBEh0KB3ZvbHRhZ2UYAiABKAJIAVIHdm9sdGFnZYgBARI0ChNjaGFubmVsX3V0aWxpemF0aW9u' + 'GAMgASgCSAJSEmNoYW5uZWxVdGlsaXphdGlvbogBARIjCgthaXJfdXRpbF90eBgEIAEoAkgDUg' + 'lhaXJVdGlsVHiIAQESKgoOdXB0aW1lX3NlY29uZHMYBSABKA1IBFINdXB0aW1lU2Vjb25kc4gB' + 'AUIQCg5fYmF0dGVyeV9sZXZlbEIKCghfdm9sdGFnZUIWChRfY2hhbm5lbF91dGlsaXphdGlvbk' + 'IOCgxfYWlyX3V0aWxfdHhCEQoPX3VwdGltZV9zZWNvbmRz'); + +@$core.Deprecated('Use environmentMetricsDescriptor instead') +const EnvironmentMetrics$json = { + '1': 'EnvironmentMetrics', + '2': [ + { + '1': 'temperature', + '3': 1, + '4': 1, + '5': 2, + '9': 0, + '10': 'temperature', + '17': true + }, + { + '1': 'relative_humidity', + '3': 2, + '4': 1, + '5': 2, + '9': 1, + '10': 'relativeHumidity', + '17': true + }, + { + '1': 'barometric_pressure', + '3': 3, + '4': 1, + '5': 2, + '9': 2, + '10': 'barometricPressure', + '17': true + }, + { + '1': 'gas_resistance', + '3': 4, + '4': 1, + '5': 2, + '9': 3, + '10': 'gasResistance', + '17': true + }, + { + '1': 'voltage', + '3': 5, + '4': 1, + '5': 2, + '9': 4, + '10': 'voltage', + '17': true + }, + { + '1': 'current', + '3': 6, + '4': 1, + '5': 2, + '9': 5, + '10': 'current', + '17': true + }, + {'1': 'iaq', '3': 7, '4': 1, '5': 13, '9': 6, '10': 'iaq', '17': true}, + { + '1': 'distance', + '3': 8, + '4': 1, + '5': 2, + '9': 7, + '10': 'distance', + '17': true + }, + {'1': 'lux', '3': 9, '4': 1, '5': 2, '9': 8, '10': 'lux', '17': true}, + { + '1': 'white_lux', + '3': 10, + '4': 1, + '5': 2, + '9': 9, + '10': 'whiteLux', + '17': true + }, + { + '1': 'ir_lux', + '3': 11, + '4': 1, + '5': 2, + '9': 10, + '10': 'irLux', + '17': true + }, + { + '1': 'uv_lux', + '3': 12, + '4': 1, + '5': 2, + '9': 11, + '10': 'uvLux', + '17': true + }, + { + '1': 'wind_direction', + '3': 13, + '4': 1, + '5': 13, + '9': 12, + '10': 'windDirection', + '17': true + }, + { + '1': 'wind_speed', + '3': 14, + '4': 1, + '5': 2, + '9': 13, + '10': 'windSpeed', + '17': true + }, + { + '1': 'weight', + '3': 15, + '4': 1, + '5': 2, + '9': 14, + '10': 'weight', + '17': true + }, + { + '1': 'wind_gust', + '3': 16, + '4': 1, + '5': 2, + '9': 15, + '10': 'windGust', + '17': true + }, + { + '1': 'wind_lull', + '3': 17, + '4': 1, + '5': 2, + '9': 16, + '10': 'windLull', + '17': true + }, + { + '1': 'radiation', + '3': 18, + '4': 1, + '5': 2, + '9': 17, + '10': 'radiation', + '17': true + }, + { + '1': 'rainfall_1h', + '3': 19, + '4': 1, + '5': 2, + '9': 18, + '10': 'rainfall1h', + '17': true + }, + { + '1': 'rainfall_24h', + '3': 20, + '4': 1, + '5': 2, + '9': 19, + '10': 'rainfall24h', + '17': true + }, + { + '1': 'soil_moisture', + '3': 21, + '4': 1, + '5': 13, + '9': 20, + '10': 'soilMoisture', + '17': true + }, + { + '1': 'soil_temperature', + '3': 22, + '4': 1, + '5': 2, + '9': 21, + '10': 'soilTemperature', + '17': true + }, + ], + '8': [ + {'1': '_temperature'}, + {'1': '_relative_humidity'}, + {'1': '_barometric_pressure'}, + {'1': '_gas_resistance'}, + {'1': '_voltage'}, + {'1': '_current'}, + {'1': '_iaq'}, + {'1': '_distance'}, + {'1': '_lux'}, + {'1': '_white_lux'}, + {'1': '_ir_lux'}, + {'1': '_uv_lux'}, + {'1': '_wind_direction'}, + {'1': '_wind_speed'}, + {'1': '_weight'}, + {'1': '_wind_gust'}, + {'1': '_wind_lull'}, + {'1': '_radiation'}, + {'1': '_rainfall_1h'}, + {'1': '_rainfall_24h'}, + {'1': '_soil_moisture'}, + {'1': '_soil_temperature'}, + ], +}; + +/// Descriptor for `EnvironmentMetrics`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List environmentMetricsDescriptor = $convert.base64Decode( + 'ChJFbnZpcm9ubWVudE1ldHJpY3MSJQoLdGVtcGVyYXR1cmUYASABKAJIAFILdGVtcGVyYXR1cm' + 'WIAQESMAoRcmVsYXRpdmVfaHVtaWRpdHkYAiABKAJIAVIQcmVsYXRpdmVIdW1pZGl0eYgBARI0' + 'ChNiYXJvbWV0cmljX3ByZXNzdXJlGAMgASgCSAJSEmJhcm9tZXRyaWNQcmVzc3VyZYgBARIqCg' + '5nYXNfcmVzaXN0YW5jZRgEIAEoAkgDUg1nYXNSZXNpc3RhbmNliAEBEh0KB3ZvbHRhZ2UYBSAB' + 'KAJIBFIHdm9sdGFnZYgBARIdCgdjdXJyZW50GAYgASgCSAVSB2N1cnJlbnSIAQESFQoDaWFxGA' + 'cgASgNSAZSA2lhcYgBARIfCghkaXN0YW5jZRgIIAEoAkgHUghkaXN0YW5jZYgBARIVCgNsdXgY' + 'CSABKAJICFIDbHV4iAEBEiAKCXdoaXRlX2x1eBgKIAEoAkgJUgh3aGl0ZUx1eIgBARIaCgZpcl' + '9sdXgYCyABKAJIClIFaXJMdXiIAQESGgoGdXZfbHV4GAwgASgCSAtSBXV2THV4iAEBEioKDndp' + 'bmRfZGlyZWN0aW9uGA0gASgNSAxSDXdpbmREaXJlY3Rpb26IAQESIgoKd2luZF9zcGVlZBgOIA' + 'EoAkgNUgl3aW5kU3BlZWSIAQESGwoGd2VpZ2h0GA8gASgCSA5SBndlaWdodIgBARIgCgl3aW5k' + 'X2d1c3QYECABKAJID1IId2luZEd1c3SIAQESIAoJd2luZF9sdWxsGBEgASgCSBBSCHdpbmRMdW' + 'xsiAEBEiEKCXJhZGlhdGlvbhgSIAEoAkgRUglyYWRpYXRpb26IAQESJAoLcmFpbmZhbGxfMWgY' + 'EyABKAJIElIKcmFpbmZhbGwxaIgBARImCgxyYWluZmFsbF8yNGgYFCABKAJIE1ILcmFpbmZhbG' + 'wyNGiIAQESKAoNc29pbF9tb2lzdHVyZRgVIAEoDUgUUgxzb2lsTW9pc3R1cmWIAQESLgoQc29p' + 'bF90ZW1wZXJhdHVyZRgWIAEoAkgVUg9zb2lsVGVtcGVyYXR1cmWIAQFCDgoMX3RlbXBlcmF0dX' + 'JlQhQKEl9yZWxhdGl2ZV9odW1pZGl0eUIWChRfYmFyb21ldHJpY19wcmVzc3VyZUIRCg9fZ2Fz' + 'X3Jlc2lzdGFuY2VCCgoIX3ZvbHRhZ2VCCgoIX2N1cnJlbnRCBgoEX2lhcUILCglfZGlzdGFuY2' + 'VCBgoEX2x1eEIMCgpfd2hpdGVfbHV4QgkKB19pcl9sdXhCCQoHX3V2X2x1eEIRCg9fd2luZF9k' + 'aXJlY3Rpb25CDQoLX3dpbmRfc3BlZWRCCQoHX3dlaWdodEIMCgpfd2luZF9ndXN0QgwKCl93aW' + '5kX2x1bGxCDAoKX3JhZGlhdGlvbkIOCgxfcmFpbmZhbGxfMWhCDwoNX3JhaW5mYWxsXzI0aEIQ' + 'Cg5fc29pbF9tb2lzdHVyZUITChFfc29pbF90ZW1wZXJhdHVyZQ=='); + +@$core.Deprecated('Use powerMetricsDescriptor instead') +const PowerMetrics$json = { + '1': 'PowerMetrics', + '2': [ + { + '1': 'ch1_voltage', + '3': 1, + '4': 1, + '5': 2, + '9': 0, + '10': 'ch1Voltage', + '17': true + }, + { + '1': 'ch1_current', + '3': 2, + '4': 1, + '5': 2, + '9': 1, + '10': 'ch1Current', + '17': true + }, + { + '1': 'ch2_voltage', + '3': 3, + '4': 1, + '5': 2, + '9': 2, + '10': 'ch2Voltage', + '17': true + }, + { + '1': 'ch2_current', + '3': 4, + '4': 1, + '5': 2, + '9': 3, + '10': 'ch2Current', + '17': true + }, + { + '1': 'ch3_voltage', + '3': 5, + '4': 1, + '5': 2, + '9': 4, + '10': 'ch3Voltage', + '17': true + }, + { + '1': 'ch3_current', + '3': 6, + '4': 1, + '5': 2, + '9': 5, + '10': 'ch3Current', + '17': true + }, + { + '1': 'ch4_voltage', + '3': 7, + '4': 1, + '5': 2, + '9': 6, + '10': 'ch4Voltage', + '17': true + }, + { + '1': 'ch4_current', + '3': 8, + '4': 1, + '5': 2, + '9': 7, + '10': 'ch4Current', + '17': true + }, + { + '1': 'ch5_voltage', + '3': 9, + '4': 1, + '5': 2, + '9': 8, + '10': 'ch5Voltage', + '17': true + }, + { + '1': 'ch5_current', + '3': 10, + '4': 1, + '5': 2, + '9': 9, + '10': 'ch5Current', + '17': true + }, + { + '1': 'ch6_voltage', + '3': 11, + '4': 1, + '5': 2, + '9': 10, + '10': 'ch6Voltage', + '17': true + }, + { + '1': 'ch6_current', + '3': 12, + '4': 1, + '5': 2, + '9': 11, + '10': 'ch6Current', + '17': true + }, + { + '1': 'ch7_voltage', + '3': 13, + '4': 1, + '5': 2, + '9': 12, + '10': 'ch7Voltage', + '17': true + }, + { + '1': 'ch7_current', + '3': 14, + '4': 1, + '5': 2, + '9': 13, + '10': 'ch7Current', + '17': true + }, + { + '1': 'ch8_voltage', + '3': 15, + '4': 1, + '5': 2, + '9': 14, + '10': 'ch8Voltage', + '17': true + }, + { + '1': 'ch8_current', + '3': 16, + '4': 1, + '5': 2, + '9': 15, + '10': 'ch8Current', + '17': true + }, + ], + '8': [ + {'1': '_ch1_voltage'}, + {'1': '_ch1_current'}, + {'1': '_ch2_voltage'}, + {'1': '_ch2_current'}, + {'1': '_ch3_voltage'}, + {'1': '_ch3_current'}, + {'1': '_ch4_voltage'}, + {'1': '_ch4_current'}, + {'1': '_ch5_voltage'}, + {'1': '_ch5_current'}, + {'1': '_ch6_voltage'}, + {'1': '_ch6_current'}, + {'1': '_ch7_voltage'}, + {'1': '_ch7_current'}, + {'1': '_ch8_voltage'}, + {'1': '_ch8_current'}, + ], +}; + +/// Descriptor for `PowerMetrics`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List powerMetricsDescriptor = $convert.base64Decode( + 'CgxQb3dlck1ldHJpY3MSJAoLY2gxX3ZvbHRhZ2UYASABKAJIAFIKY2gxVm9sdGFnZYgBARIkCg' + 'tjaDFfY3VycmVudBgCIAEoAkgBUgpjaDFDdXJyZW50iAEBEiQKC2NoMl92b2x0YWdlGAMgASgC' + 'SAJSCmNoMlZvbHRhZ2WIAQESJAoLY2gyX2N1cnJlbnQYBCABKAJIA1IKY2gyQ3VycmVudIgBAR' + 'IkCgtjaDNfdm9sdGFnZRgFIAEoAkgEUgpjaDNWb2x0YWdliAEBEiQKC2NoM19jdXJyZW50GAYg' + 'ASgCSAVSCmNoM0N1cnJlbnSIAQESJAoLY2g0X3ZvbHRhZ2UYByABKAJIBlIKY2g0Vm9sdGFnZY' + 'gBARIkCgtjaDRfY3VycmVudBgIIAEoAkgHUgpjaDRDdXJyZW50iAEBEiQKC2NoNV92b2x0YWdl' + 'GAkgASgCSAhSCmNoNVZvbHRhZ2WIAQESJAoLY2g1X2N1cnJlbnQYCiABKAJICVIKY2g1Q3Vycm' + 'VudIgBARIkCgtjaDZfdm9sdGFnZRgLIAEoAkgKUgpjaDZWb2x0YWdliAEBEiQKC2NoNl9jdXJy' + 'ZW50GAwgASgCSAtSCmNoNkN1cnJlbnSIAQESJAoLY2g3X3ZvbHRhZ2UYDSABKAJIDFIKY2g3Vm' + '9sdGFnZYgBARIkCgtjaDdfY3VycmVudBgOIAEoAkgNUgpjaDdDdXJyZW50iAEBEiQKC2NoOF92' + 'b2x0YWdlGA8gASgCSA5SCmNoOFZvbHRhZ2WIAQESJAoLY2g4X2N1cnJlbnQYECABKAJID1IKY2' + 'g4Q3VycmVudIgBAUIOCgxfY2gxX3ZvbHRhZ2VCDgoMX2NoMV9jdXJyZW50Qg4KDF9jaDJfdm9s' + 'dGFnZUIOCgxfY2gyX2N1cnJlbnRCDgoMX2NoM192b2x0YWdlQg4KDF9jaDNfY3VycmVudEIOCg' + 'xfY2g0X3ZvbHRhZ2VCDgoMX2NoNF9jdXJyZW50Qg4KDF9jaDVfdm9sdGFnZUIOCgxfY2g1X2N1' + 'cnJlbnRCDgoMX2NoNl92b2x0YWdlQg4KDF9jaDZfY3VycmVudEIOCgxfY2g3X3ZvbHRhZ2VCDg' + 'oMX2NoN19jdXJyZW50Qg4KDF9jaDhfdm9sdGFnZUIOCgxfY2g4X2N1cnJlbnQ='); + +@$core.Deprecated('Use airQualityMetricsDescriptor instead') +const AirQualityMetrics$json = { + '1': 'AirQualityMetrics', + '2': [ + { + '1': 'pm10_standard', + '3': 1, + '4': 1, + '5': 13, + '9': 0, + '10': 'pm10Standard', + '17': true + }, + { + '1': 'pm25_standard', + '3': 2, + '4': 1, + '5': 13, + '9': 1, + '10': 'pm25Standard', + '17': true + }, + { + '1': 'pm100_standard', + '3': 3, + '4': 1, + '5': 13, + '9': 2, + '10': 'pm100Standard', + '17': true + }, + { + '1': 'pm10_environmental', + '3': 4, + '4': 1, + '5': 13, + '9': 3, + '10': 'pm10Environmental', + '17': true + }, + { + '1': 'pm25_environmental', + '3': 5, + '4': 1, + '5': 13, + '9': 4, + '10': 'pm25Environmental', + '17': true + }, + { + '1': 'pm100_environmental', + '3': 6, + '4': 1, + '5': 13, + '9': 5, + '10': 'pm100Environmental', + '17': true + }, + { + '1': 'particles_03um', + '3': 7, + '4': 1, + '5': 13, + '9': 6, + '10': 'particles03um', + '17': true + }, + { + '1': 'particles_05um', + '3': 8, + '4': 1, + '5': 13, + '9': 7, + '10': 'particles05um', + '17': true + }, + { + '1': 'particles_10um', + '3': 9, + '4': 1, + '5': 13, + '9': 8, + '10': 'particles10um', + '17': true + }, + { + '1': 'particles_25um', + '3': 10, + '4': 1, + '5': 13, + '9': 9, + '10': 'particles25um', + '17': true + }, + { + '1': 'particles_50um', + '3': 11, + '4': 1, + '5': 13, + '9': 10, + '10': 'particles50um', + '17': true + }, + { + '1': 'particles_100um', + '3': 12, + '4': 1, + '5': 13, + '9': 11, + '10': 'particles100um', + '17': true + }, + {'1': 'co2', '3': 13, '4': 1, '5': 13, '9': 12, '10': 'co2', '17': true}, + { + '1': 'co2_temperature', + '3': 14, + '4': 1, + '5': 2, + '9': 13, + '10': 'co2Temperature', + '17': true + }, + { + '1': 'co2_humidity', + '3': 15, + '4': 1, + '5': 2, + '9': 14, + '10': 'co2Humidity', + '17': true + }, + { + '1': 'form_formaldehyde', + '3': 16, + '4': 1, + '5': 2, + '9': 15, + '10': 'formFormaldehyde', + '17': true + }, + { + '1': 'form_humidity', + '3': 17, + '4': 1, + '5': 2, + '9': 16, + '10': 'formHumidity', + '17': true + }, + { + '1': 'form_temperature', + '3': 18, + '4': 1, + '5': 2, + '9': 17, + '10': 'formTemperature', + '17': true + }, + { + '1': 'pm40_standard', + '3': 19, + '4': 1, + '5': 13, + '9': 18, + '10': 'pm40Standard', + '17': true + }, + { + '1': 'particles_40um', + '3': 20, + '4': 1, + '5': 13, + '9': 19, + '10': 'particles40um', + '17': true + }, + { + '1': 'pm_temperature', + '3': 21, + '4': 1, + '5': 2, + '9': 20, + '10': 'pmTemperature', + '17': true + }, + { + '1': 'pm_humidity', + '3': 22, + '4': 1, + '5': 2, + '9': 21, + '10': 'pmHumidity', + '17': true + }, + { + '1': 'pm_voc_idx', + '3': 23, + '4': 1, + '5': 2, + '9': 22, + '10': 'pmVocIdx', + '17': true + }, + { + '1': 'pm_nox_idx', + '3': 24, + '4': 1, + '5': 2, + '9': 23, + '10': 'pmNoxIdx', + '17': true + }, + { + '1': 'particles_tps', + '3': 25, + '4': 1, + '5': 2, + '9': 24, + '10': 'particlesTps', + '17': true + }, + ], + '8': [ + {'1': '_pm10_standard'}, + {'1': '_pm25_standard'}, + {'1': '_pm100_standard'}, + {'1': '_pm10_environmental'}, + {'1': '_pm25_environmental'}, + {'1': '_pm100_environmental'}, + {'1': '_particles_03um'}, + {'1': '_particles_05um'}, + {'1': '_particles_10um'}, + {'1': '_particles_25um'}, + {'1': '_particles_50um'}, + {'1': '_particles_100um'}, + {'1': '_co2'}, + {'1': '_co2_temperature'}, + {'1': '_co2_humidity'}, + {'1': '_form_formaldehyde'}, + {'1': '_form_humidity'}, + {'1': '_form_temperature'}, + {'1': '_pm40_standard'}, + {'1': '_particles_40um'}, + {'1': '_pm_temperature'}, + {'1': '_pm_humidity'}, + {'1': '_pm_voc_idx'}, + {'1': '_pm_nox_idx'}, + {'1': '_particles_tps'}, + ], +}; + +/// Descriptor for `AirQualityMetrics`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List airQualityMetricsDescriptor = $convert.base64Decode( + 'ChFBaXJRdWFsaXR5TWV0cmljcxIoCg1wbTEwX3N0YW5kYXJkGAEgASgNSABSDHBtMTBTdGFuZG' + 'FyZIgBARIoCg1wbTI1X3N0YW5kYXJkGAIgASgNSAFSDHBtMjVTdGFuZGFyZIgBARIqCg5wbTEw' + 'MF9zdGFuZGFyZBgDIAEoDUgCUg1wbTEwMFN0YW5kYXJkiAEBEjIKEnBtMTBfZW52aXJvbm1lbn' + 'RhbBgEIAEoDUgDUhFwbTEwRW52aXJvbm1lbnRhbIgBARIyChJwbTI1X2Vudmlyb25tZW50YWwY' + 'BSABKA1IBFIRcG0yNUVudmlyb25tZW50YWyIAQESNAoTcG0xMDBfZW52aXJvbm1lbnRhbBgGIA' + 'EoDUgFUhJwbTEwMEVudmlyb25tZW50YWyIAQESKgoOcGFydGljbGVzXzAzdW0YByABKA1IBlIN' + 'cGFydGljbGVzMDN1bYgBARIqCg5wYXJ0aWNsZXNfMDV1bRgIIAEoDUgHUg1wYXJ0aWNsZXMwNX' + 'VtiAEBEioKDnBhcnRpY2xlc18xMHVtGAkgASgNSAhSDXBhcnRpY2xlczEwdW2IAQESKgoOcGFy' + 'dGljbGVzXzI1dW0YCiABKA1ICVINcGFydGljbGVzMjV1bYgBARIqCg5wYXJ0aWNsZXNfNTB1bR' + 'gLIAEoDUgKUg1wYXJ0aWNsZXM1MHVtiAEBEiwKD3BhcnRpY2xlc18xMDB1bRgMIAEoDUgLUg5w' + 'YXJ0aWNsZXMxMDB1bYgBARIVCgNjbzIYDSABKA1IDFIDY28yiAEBEiwKD2NvMl90ZW1wZXJhdH' + 'VyZRgOIAEoAkgNUg5jbzJUZW1wZXJhdHVyZYgBARImCgxjbzJfaHVtaWRpdHkYDyABKAJIDlIL' + 'Y28ySHVtaWRpdHmIAQESMAoRZm9ybV9mb3JtYWxkZWh5ZGUYECABKAJID1IQZm9ybUZvcm1hbG' + 'RlaHlkZYgBARIoCg1mb3JtX2h1bWlkaXR5GBEgASgCSBBSDGZvcm1IdW1pZGl0eYgBARIuChBm' + 'b3JtX3RlbXBlcmF0dXJlGBIgASgCSBFSD2Zvcm1UZW1wZXJhdHVyZYgBARIoCg1wbTQwX3N0YW' + '5kYXJkGBMgASgNSBJSDHBtNDBTdGFuZGFyZIgBARIqCg5wYXJ0aWNsZXNfNDB1bRgUIAEoDUgT' + 'Ug1wYXJ0aWNsZXM0MHVtiAEBEioKDnBtX3RlbXBlcmF0dXJlGBUgASgCSBRSDXBtVGVtcGVyYX' + 'R1cmWIAQESJAoLcG1faHVtaWRpdHkYFiABKAJIFVIKcG1IdW1pZGl0eYgBARIhCgpwbV92b2Nf' + 'aWR4GBcgASgCSBZSCHBtVm9jSWR4iAEBEiEKCnBtX25veF9pZHgYGCABKAJIF1IIcG1Ob3hJZH' + 'iIAQESKAoNcGFydGljbGVzX3RwcxgZIAEoAkgYUgxwYXJ0aWNsZXNUcHOIAQFCEAoOX3BtMTBf' + 'c3RhbmRhcmRCEAoOX3BtMjVfc3RhbmRhcmRCEQoPX3BtMTAwX3N0YW5kYXJkQhUKE19wbTEwX2' + 'Vudmlyb25tZW50YWxCFQoTX3BtMjVfZW52aXJvbm1lbnRhbEIWChRfcG0xMDBfZW52aXJvbm1l' + 'bnRhbEIRCg9fcGFydGljbGVzXzAzdW1CEQoPX3BhcnRpY2xlc18wNXVtQhEKD19wYXJ0aWNsZX' + 'NfMTB1bUIRCg9fcGFydGljbGVzXzI1dW1CEQoPX3BhcnRpY2xlc181MHVtQhIKEF9wYXJ0aWNs' + 'ZXNfMTAwdW1CBgoEX2NvMkISChBfY28yX3RlbXBlcmF0dXJlQg8KDV9jbzJfaHVtaWRpdHlCFA' + 'oSX2Zvcm1fZm9ybWFsZGVoeWRlQhAKDl9mb3JtX2h1bWlkaXR5QhMKEV9mb3JtX3RlbXBlcmF0' + 'dXJlQhAKDl9wbTQwX3N0YW5kYXJkQhEKD19wYXJ0aWNsZXNfNDB1bUIRCg9fcG1fdGVtcGVyYX' + 'R1cmVCDgoMX3BtX2h1bWlkaXR5Qg0KC19wbV92b2NfaWR4Qg0KC19wbV9ub3hfaWR4QhAKDl9w' + 'YXJ0aWNsZXNfdHBz'); + +@$core.Deprecated('Use localStatsDescriptor instead') +const LocalStats$json = { + '1': 'LocalStats', + '2': [ + {'1': 'uptime_seconds', '3': 1, '4': 1, '5': 13, '10': 'uptimeSeconds'}, + { + '1': 'channel_utilization', + '3': 2, + '4': 1, + '5': 2, + '10': 'channelUtilization' + }, + {'1': 'air_util_tx', '3': 3, '4': 1, '5': 2, '10': 'airUtilTx'}, + {'1': 'num_packets_tx', '3': 4, '4': 1, '5': 13, '10': 'numPacketsTx'}, + {'1': 'num_packets_rx', '3': 5, '4': 1, '5': 13, '10': 'numPacketsRx'}, + { + '1': 'num_packets_rx_bad', + '3': 6, + '4': 1, + '5': 13, + '10': 'numPacketsRxBad' + }, + {'1': 'num_online_nodes', '3': 7, '4': 1, '5': 13, '10': 'numOnlineNodes'}, + {'1': 'num_total_nodes', '3': 8, '4': 1, '5': 13, '10': 'numTotalNodes'}, + {'1': 'num_rx_dupe', '3': 9, '4': 1, '5': 13, '10': 'numRxDupe'}, + {'1': 'num_tx_relay', '3': 10, '4': 1, '5': 13, '10': 'numTxRelay'}, + { + '1': 'num_tx_relay_canceled', + '3': 11, + '4': 1, + '5': 13, + '10': 'numTxRelayCanceled' + }, + {'1': 'heap_total_bytes', '3': 12, '4': 1, '5': 13, '10': 'heapTotalBytes'}, + {'1': 'heap_free_bytes', '3': 13, '4': 1, '5': 13, '10': 'heapFreeBytes'}, + ], +}; + +/// Descriptor for `LocalStats`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List localStatsDescriptor = $convert.base64Decode( + 'CgpMb2NhbFN0YXRzEiUKDnVwdGltZV9zZWNvbmRzGAEgASgNUg11cHRpbWVTZWNvbmRzEi8KE2' + 'NoYW5uZWxfdXRpbGl6YXRpb24YAiABKAJSEmNoYW5uZWxVdGlsaXphdGlvbhIeCgthaXJfdXRp' + 'bF90eBgDIAEoAlIJYWlyVXRpbFR4EiQKDm51bV9wYWNrZXRzX3R4GAQgASgNUgxudW1QYWNrZX' + 'RzVHgSJAoObnVtX3BhY2tldHNfcngYBSABKA1SDG51bVBhY2tldHNSeBIrChJudW1fcGFja2V0' + 'c19yeF9iYWQYBiABKA1SD251bVBhY2tldHNSeEJhZBIoChBudW1fb25saW5lX25vZGVzGAcgAS' + 'gNUg5udW1PbmxpbmVOb2RlcxImCg9udW1fdG90YWxfbm9kZXMYCCABKA1SDW51bVRvdGFsTm9k' + 'ZXMSHgoLbnVtX3J4X2R1cGUYCSABKA1SCW51bVJ4RHVwZRIgCgxudW1fdHhfcmVsYXkYCiABKA' + '1SCm51bVR4UmVsYXkSMQoVbnVtX3R4X3JlbGF5X2NhbmNlbGVkGAsgASgNUhJudW1UeFJlbGF5' + 'Q2FuY2VsZWQSKAoQaGVhcF90b3RhbF9ieXRlcxgMIAEoDVIOaGVhcFRvdGFsQnl0ZXMSJgoPaG' + 'VhcF9mcmVlX2J5dGVzGA0gASgNUg1oZWFwRnJlZUJ5dGVz'); + +@$core.Deprecated('Use healthMetricsDescriptor instead') +const HealthMetrics$json = { + '1': 'HealthMetrics', + '2': [ + { + '1': 'heart_bpm', + '3': 1, + '4': 1, + '5': 13, + '9': 0, + '10': 'heartBpm', + '17': true + }, + {'1': 'spO2', '3': 2, '4': 1, '5': 13, '9': 1, '10': 'spO2', '17': true}, + { + '1': 'temperature', + '3': 3, + '4': 1, + '5': 2, + '9': 2, + '10': 'temperature', + '17': true + }, + ], + '8': [ + {'1': '_heart_bpm'}, + {'1': '_spO2'}, + {'1': '_temperature'}, + ], +}; + +/// Descriptor for `HealthMetrics`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List healthMetricsDescriptor = $convert.base64Decode( + 'Cg1IZWFsdGhNZXRyaWNzEiAKCWhlYXJ0X2JwbRgBIAEoDUgAUghoZWFydEJwbYgBARIXCgRzcE' + '8yGAIgASgNSAFSBHNwTzKIAQESJQoLdGVtcGVyYXR1cmUYAyABKAJIAlILdGVtcGVyYXR1cmWI' + 'AQFCDAoKX2hlYXJ0X2JwbUIHCgVfc3BPMkIOCgxfdGVtcGVyYXR1cmU='); + +@$core.Deprecated('Use hostMetricsDescriptor instead') +const HostMetrics$json = { + '1': 'HostMetrics', + '2': [ + {'1': 'uptime_seconds', '3': 1, '4': 1, '5': 13, '10': 'uptimeSeconds'}, + {'1': 'freemem_bytes', '3': 2, '4': 1, '5': 4, '10': 'freememBytes'}, + {'1': 'diskfree1_bytes', '3': 3, '4': 1, '5': 4, '10': 'diskfree1Bytes'}, + { + '1': 'diskfree2_bytes', + '3': 4, + '4': 1, + '5': 4, + '9': 0, + '10': 'diskfree2Bytes', + '17': true + }, + { + '1': 'diskfree3_bytes', + '3': 5, + '4': 1, + '5': 4, + '9': 1, + '10': 'diskfree3Bytes', + '17': true + }, + {'1': 'load1', '3': 6, '4': 1, '5': 13, '10': 'load1'}, + {'1': 'load5', '3': 7, '4': 1, '5': 13, '10': 'load5'}, + {'1': 'load15', '3': 8, '4': 1, '5': 13, '10': 'load15'}, + { + '1': 'user_string', + '3': 9, + '4': 1, + '5': 9, + '9': 2, + '10': 'userString', + '17': true + }, + ], + '8': [ + {'1': '_diskfree2_bytes'}, + {'1': '_diskfree3_bytes'}, + {'1': '_user_string'}, + ], +}; + +/// Descriptor for `HostMetrics`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List hostMetricsDescriptor = $convert.base64Decode( + 'CgtIb3N0TWV0cmljcxIlCg51cHRpbWVfc2Vjb25kcxgBIAEoDVINdXB0aW1lU2Vjb25kcxIjCg' + '1mcmVlbWVtX2J5dGVzGAIgASgEUgxmcmVlbWVtQnl0ZXMSJwoPZGlza2ZyZWUxX2J5dGVzGAMg' + 'ASgEUg5kaXNrZnJlZTFCeXRlcxIsCg9kaXNrZnJlZTJfYnl0ZXMYBCABKARIAFIOZGlza2ZyZW' + 'UyQnl0ZXOIAQESLAoPZGlza2ZyZWUzX2J5dGVzGAUgASgESAFSDmRpc2tmcmVlM0J5dGVziAEB' + 'EhQKBWxvYWQxGAYgASgNUgVsb2FkMRIUCgVsb2FkNRgHIAEoDVIFbG9hZDUSFgoGbG9hZDE1GA' + 'ggASgNUgZsb2FkMTUSJAoLdXNlcl9zdHJpbmcYCSABKAlIAlIKdXNlclN0cmluZ4gBAUISChBf' + 'ZGlza2ZyZWUyX2J5dGVzQhIKEF9kaXNrZnJlZTNfYnl0ZXNCDgoMX3VzZXJfc3RyaW5n'); + +@$core.Deprecated('Use telemetryDescriptor instead') +const Telemetry$json = { + '1': 'Telemetry', + '2': [ + {'1': 'time', '3': 1, '4': 1, '5': 7, '10': 'time'}, + { + '1': 'device_metrics', + '3': 2, + '4': 1, + '5': 11, + '6': '.meshtastic.DeviceMetrics', + '9': 0, + '10': 'deviceMetrics' + }, + { + '1': 'environment_metrics', + '3': 3, + '4': 1, + '5': 11, + '6': '.meshtastic.EnvironmentMetrics', + '9': 0, + '10': 'environmentMetrics' + }, + { + '1': 'air_quality_metrics', + '3': 4, + '4': 1, + '5': 11, + '6': '.meshtastic.AirQualityMetrics', + '9': 0, + '10': 'airQualityMetrics' + }, + { + '1': 'power_metrics', + '3': 5, + '4': 1, + '5': 11, + '6': '.meshtastic.PowerMetrics', + '9': 0, + '10': 'powerMetrics' + }, + { + '1': 'local_stats', + '3': 6, + '4': 1, + '5': 11, + '6': '.meshtastic.LocalStats', + '9': 0, + '10': 'localStats' + }, + { + '1': 'health_metrics', + '3': 7, + '4': 1, + '5': 11, + '6': '.meshtastic.HealthMetrics', + '9': 0, + '10': 'healthMetrics' + }, + { + '1': 'host_metrics', + '3': 8, + '4': 1, + '5': 11, + '6': '.meshtastic.HostMetrics', + '9': 0, + '10': 'hostMetrics' + }, + ], + '8': [ + {'1': 'variant'}, + ], +}; + +/// Descriptor for `Telemetry`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List telemetryDescriptor = $convert.base64Decode( + 'CglUZWxlbWV0cnkSEgoEdGltZRgBIAEoB1IEdGltZRJCCg5kZXZpY2VfbWV0cmljcxgCIAEoCz' + 'IZLm1lc2h0YXN0aWMuRGV2aWNlTWV0cmljc0gAUg1kZXZpY2VNZXRyaWNzElEKE2Vudmlyb25t' + 'ZW50X21ldHJpY3MYAyABKAsyHi5tZXNodGFzdGljLkVudmlyb25tZW50TWV0cmljc0gAUhJlbn' + 'Zpcm9ubWVudE1ldHJpY3MSTwoTYWlyX3F1YWxpdHlfbWV0cmljcxgEIAEoCzIdLm1lc2h0YXN0' + 'aWMuQWlyUXVhbGl0eU1ldHJpY3NIAFIRYWlyUXVhbGl0eU1ldHJpY3MSPwoNcG93ZXJfbWV0cm' + 'ljcxgFIAEoCzIYLm1lc2h0YXN0aWMuUG93ZXJNZXRyaWNzSABSDHBvd2VyTWV0cmljcxI5Cgts' + 'b2NhbF9zdGF0cxgGIAEoCzIWLm1lc2h0YXN0aWMuTG9jYWxTdGF0c0gAUgpsb2NhbFN0YXRzEk' + 'IKDmhlYWx0aF9tZXRyaWNzGAcgASgLMhkubWVzaHRhc3RpYy5IZWFsdGhNZXRyaWNzSABSDWhl' + 'YWx0aE1ldHJpY3MSPAoMaG9zdF9tZXRyaWNzGAggASgLMhcubWVzaHRhc3RpYy5Ib3N0TWV0cm' + 'ljc0gAUgtob3N0TWV0cmljc0IJCgd2YXJpYW50'); + +@$core.Deprecated('Use nau7802ConfigDescriptor instead') +const Nau7802Config$json = { + '1': 'Nau7802Config', + '2': [ + {'1': 'zeroOffset', '3': 1, '4': 1, '5': 5, '10': 'zeroOffset'}, + { + '1': 'calibrationFactor', + '3': 2, + '4': 1, + '5': 2, + '10': 'calibrationFactor' + }, + ], +}; + +/// Descriptor for `Nau7802Config`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nau7802ConfigDescriptor = $convert.base64Decode( + 'Cg1OYXU3ODAyQ29uZmlnEh4KCnplcm9PZmZzZXQYASABKAVSCnplcm9PZmZzZXQSLAoRY2FsaW' + 'JyYXRpb25GYWN0b3IYAiABKAJSEWNhbGlicmF0aW9uRmFjdG9y'); diff --git a/third_party/meshtastic_flutter/lib/generated/xmodem.pb.dart b/third_party/meshtastic_flutter/lib/generated/xmodem.pb.dart new file mode 100644 index 000000000..43de1f251 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/xmodem.pb.dart @@ -0,0 +1,120 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/xmodem.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'xmodem.pbenum.dart'; + +export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; + +export 'xmodem.pbenum.dart'; + +class XModem extends $pb.GeneratedMessage { + factory XModem({ + XModem_Control? control, + $core.int? seq, + $core.int? crc16, + $core.List<$core.int>? buffer, + }) { + final result = create(); + if (control != null) result.control = control; + if (seq != null) result.seq = seq; + if (crc16 != null) result.crc16 = crc16; + if (buffer != null) result.buffer = buffer; + return result; + } + + XModem._(); + + factory XModem.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory XModem.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'XModem', + package: const $pb.PackageName(_omitMessageNames ? '' : 'meshtastic'), + createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'control', $pb.PbFieldType.OE, + defaultOrMaker: XModem_Control.NUL, + valueOf: XModem_Control.valueOf, + enumValues: XModem_Control.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'crc16', $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'buffer', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + XModem clone() => XModem()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + XModem copyWith(void Function(XModem) updates) => + super.copyWith((message) => updates(message as XModem)) as XModem; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static XModem create() => XModem._(); + @$core.override + XModem createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static XModem getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static XModem? _defaultInstance; + + @$pb.TagNumber(1) + XModem_Control get control => $_getN(0); + @$pb.TagNumber(1) + set control(XModem_Control value) => $_setField(1, value); + @$pb.TagNumber(1) + $core.bool hasControl() => $_has(0); + @$pb.TagNumber(1) + void clearControl() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get seq => $_getIZ(1); + @$pb.TagNumber(2) + set seq($core.int value) => $_setUnsignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasSeq() => $_has(1); + @$pb.TagNumber(2) + void clearSeq() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get crc16 => $_getIZ(2); + @$pb.TagNumber(3) + set crc16($core.int value) => $_setUnsignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasCrc16() => $_has(2); + @$pb.TagNumber(3) + void clearCrc16() => $_clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get buffer => $_getN(3); + @$pb.TagNumber(4) + set buffer($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasBuffer() => $_has(3); + @$pb.TagNumber(4) + void clearBuffer() => $_clearField(4); +} + +const $core.bool _omitFieldNames = + $core.bool.fromEnvironment('protobuf.omit_field_names'); +const $core.bool _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/xmodem.pbenum.dart b/third_party/meshtastic_flutter/lib/generated/xmodem.pbenum.dart new file mode 100644 index 000000000..6381ec5bb --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/xmodem.pbenum.dart @@ -0,0 +1,54 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/xmodem.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class XModem_Control extends $pb.ProtobufEnum { + static const XModem_Control NUL = + XModem_Control._(0, _omitEnumNames ? '' : 'NUL'); + static const XModem_Control SOH = + XModem_Control._(1, _omitEnumNames ? '' : 'SOH'); + static const XModem_Control STX = + XModem_Control._(2, _omitEnumNames ? '' : 'STX'); + static const XModem_Control EOT = + XModem_Control._(4, _omitEnumNames ? '' : 'EOT'); + static const XModem_Control ACK = + XModem_Control._(6, _omitEnumNames ? '' : 'ACK'); + static const XModem_Control NAK = + XModem_Control._(21, _omitEnumNames ? '' : 'NAK'); + static const XModem_Control CAN = + XModem_Control._(24, _omitEnumNames ? '' : 'CAN'); + static const XModem_Control CTRLZ = + XModem_Control._(26, _omitEnumNames ? '' : 'CTRLZ'); + + static const $core.List values = [ + NUL, + SOH, + STX, + EOT, + ACK, + NAK, + CAN, + CTRLZ, + ]; + + static final $core.Map<$core.int, XModem_Control> _byValue = + $pb.ProtobufEnum.initByValue(values); + static XModem_Control? valueOf($core.int value) => _byValue[value]; + + const XModem_Control._(super.value, super.name); +} + +const $core.bool _omitEnumNames = + $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/third_party/meshtastic_flutter/lib/generated/xmodem.pbjson.dart b/third_party/meshtastic_flutter/lib/generated/xmodem.pbjson.dart new file mode 100644 index 000000000..d2dd9b6fe --- /dev/null +++ b/third_party/meshtastic_flutter/lib/generated/xmodem.pbjson.dart @@ -0,0 +1,56 @@ +// This is a generated file - do not edit. +// +// Generated from meshtastic/xmodem.proto. + +// @dart = 3.3 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: curly_braces_in_flow_control_structures +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use xModemDescriptor instead') +const XModem$json = { + '1': 'XModem', + '2': [ + { + '1': 'control', + '3': 1, + '4': 1, + '5': 14, + '6': '.meshtastic.XModem.Control', + '10': 'control' + }, + {'1': 'seq', '3': 2, '4': 1, '5': 13, '10': 'seq'}, + {'1': 'crc16', '3': 3, '4': 1, '5': 13, '10': 'crc16'}, + {'1': 'buffer', '3': 4, '4': 1, '5': 12, '10': 'buffer'}, + ], + '4': [XModem_Control$json], +}; + +@$core.Deprecated('Use xModemDescriptor instead') +const XModem_Control$json = { + '1': 'Control', + '2': [ + {'1': 'NUL', '2': 0}, + {'1': 'SOH', '2': 1}, + {'1': 'STX', '2': 2}, + {'1': 'EOT', '2': 4}, + {'1': 'ACK', '2': 6}, + {'1': 'NAK', '2': 21}, + {'1': 'CAN', '2': 24}, + {'1': 'CTRLZ', '2': 26}, + ], +}; + +/// Descriptor for `XModem`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List xModemDescriptor = $convert.base64Decode( + 'CgZYTW9kZW0SNAoHY29udHJvbBgBIAEoDjIaLm1lc2h0YXN0aWMuWE1vZGVtLkNvbnRyb2xSB2' + 'NvbnRyb2wSEAoDc2VxGAIgASgNUgNzZXESFAoFY3JjMTYYAyABKA1SBWNyYzE2EhYKBmJ1ZmZl' + 'chgEIAEoDFIGYnVmZmVyIlMKB0NvbnRyb2wSBwoDTlVMEAASBwoDU09IEAESBwoDU1RYEAISBw' + 'oDRU9UEAQSBwoDQUNLEAYSBwoDTkFLEBUSBwoDQ0FOEBgSCQoFQ1RSTFoQGg=='); diff --git a/third_party/meshtastic_flutter/lib/meshtastic_flutter.dart b/third_party/meshtastic_flutter/lib/meshtastic_flutter.dart new file mode 100644 index 000000000..fcfd2a9b5 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/meshtastic_flutter.dart @@ -0,0 +1,14 @@ +export 'src/meshtastic_client.dart'; +export 'src/models/connection_state.dart'; +export 'src/models/mesh_packet_wrapper.dart'; +export 'src/models/node_info.dart'; +export 'src/models/meshtastic_config.dart'; +export 'src/exceptions/meshtastic_exceptions.dart'; +export 'generated/admin.pb.dart'; +export 'generated/channel.pb.dart'; +export 'generated/config.pb.dart'; +export 'generated/config.pbenum.dart'; +export 'generated/mesh.pb.dart'; +export 'generated/mesh.pbenum.dart'; +export 'generated/module_config.pb.dart'; +export 'generated/portnums.pbenum.dart'; diff --git a/third_party/meshtastic_flutter/lib/src/exceptions/meshtastic_exceptions.dart b/third_party/meshtastic_flutter/lib/src/exceptions/meshtastic_exceptions.dart new file mode 100644 index 000000000..ae85b1c5c --- /dev/null +++ b/third_party/meshtastic_flutter/lib/src/exceptions/meshtastic_exceptions.dart @@ -0,0 +1,65 @@ +/// Base exception for all Meshtastic-related errors +abstract class MeshtasticException implements Exception { + final String message; + final dynamic cause; + + const MeshtasticException(this.message, [this.cause]); + + @override + String toString() => + 'MeshtasticException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} + +/// Exception thrown when Bluetooth operations fail +class BluetoothException extends MeshtasticException { + const BluetoothException(super.message, [super.cause]); + + @override + String toString() => + 'BluetoothException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} + +/// Exception thrown when connection to device fails or is lost +class ConnectionException extends MeshtasticException { + const ConnectionException(super.message, [super.cause]); + + @override + String toString() => + 'ConnectionException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} + +/// Exception thrown when protobuf parsing fails +class ProtocolException extends MeshtasticException { + const ProtocolException(super.message, [super.cause]); + + @override + String toString() => + 'ProtocolException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} + +/// Exception thrown when device configuration is invalid +class ConfigurationException extends MeshtasticException { + const ConfigurationException(super.message, [super.cause]); + + @override + String toString() => + 'ConfigurationException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} + +/// Exception thrown when permissions are not granted +class PermissionException extends MeshtasticException { + const PermissionException(super.message, [super.cause]); + + @override + String toString() => + 'PermissionException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} + +/// Exception thrown when operation times out +class TimeoutException extends MeshtasticException { + const TimeoutException(super.message, [super.cause]); + + @override + String toString() => + 'TimeoutException: $message${cause != null ? ' (caused by: $cause)' : ''}'; +} diff --git a/third_party/meshtastic_flutter/lib/src/meshtastic_client.dart b/third_party/meshtastic_flutter/lib/src/meshtastic_client.dart new file mode 100644 index 000000000..000b7281f --- /dev/null +++ b/third_party/meshtastic_flutter/lib/src/meshtastic_client.dart @@ -0,0 +1,875 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:logging/logging.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../generated/admin.pb.dart'; +import '../generated/mesh.pb.dart'; +import '../generated/config.pb.dart'; +import '../generated/module_config.pb.dart'; +import '../generated/channel.pb.dart'; +import '../generated/portnums.pb.dart'; +import '../generated/telemetry.pb.dart'; +import 'models/connection_state.dart'; +import 'models/mesh_packet_wrapper.dart'; +import 'models/node_info.dart'; +import 'models/meshtastic_config.dart'; +import 'exceptions/meshtastic_exceptions.dart'; + +/// Main client for communicating with Meshtastic devices over BLE +class MeshtasticClient { + static final Logger _logger = Logger('MeshtasticClient'); + + // Meshtastic BLE Service UUID + static const String _serviceUuid = '6ba1b218-15a8-461f-9fa8-5dcae273eafd'; + + // Characteristic UUIDs + static const String _toRadioUuid = 'f75c76d2-129e-4dad-a1dd-7866124401e7'; + static const String _fromRadioUuid = '2c55e69e-4993-11ed-b878-0242ac120002'; + static const String _fromNumUuid = 'ed9da18c-a800-4f66-a670-aa7547e34453'; + + // Maximum packet size + static const int _maxPacketSize = 512; + + // The radio refills `fromradio` asynchronously, so a single empty read does + // not always mean "the mailbox is drained". The reference Python client + // retries the same way before giving up. + static const int _configEmptyReadRetries = 3; + static const Duration _emptyReadBackoff = Duration(milliseconds: 100); + + // Private fields + BluetoothDevice? _device; + BluetoothCharacteristic? _toRadioChar; + BluetoothCharacteristic? _fromRadioChar; + BluetoothCharacteristic? _fromNumChar; + + StreamSubscription? _connectionSubscription; + StreamSubscription>? _fromNumSubscription; + + final StreamController _connectionController = + StreamController.broadcast(); + final StreamController _packetController = + StreamController.broadcast(); + final StreamController _nodeController = + StreamController.broadcast(); + final StreamController _adminController = + StreamController.broadcast(); + + // Configuration and state + final Map _nodes = {}; + + /// Live device metrics per node, and when each arrived. + /// + /// Separate from the node DB because the DB entry is a *snapshot taken at + /// config-download time* — for the local radio it is often stale or missing + /// entirely. The truth is broadcast continuously on TELEMETRY_APP, including + /// by the attached radio about itself. + final Map _metrics = {}; + final Map _metricsAt = {}; + MyNodeInfo? _myNodeInfo; + Config? _config; + Config_LoRaConfig? _lora; + DeviceMetadata? _metadata; + ModuleConfig? _moduleConfig; + final List _channels = []; + User? _localUser; + + bool _configComplete = false; + + // Drain scheduling — see [_requestDrain]. + Future? _drainTask; + bool _drainRequested = false; + int _pendingEmptyRetries = 0; + + // Public streams + Stream get connectionStream => _connectionController.stream; + Stream get packetStream => _packetController.stream; + Stream get nodeStream => _nodeController.stream; + + /// Admin replies from the radio (channel/config reads, error responses). + Stream get adminStream => _adminController.stream; + + // Getters for current state + Map get nodes => Map.unmodifiable(_nodes); + MyNodeInfo? get myNodeInfo => _myNodeInfo; + + /// The local radio's node number, once the config download delivered it. + int? get myNodeNum => _myNodeInfo?.myNodeNum; + + /// The radio's own entry in the node DB — where its battery, uptime and + /// air-time live, since the firmware reports them like any other node's. + NodeInfoWrapper? get localNode { + final num = myNodeNum; + return num == null ? null : _nodes[num]; + } + + /// Firmware version, hardware model and capability flags, as sent once + /// during the config download. + DeviceMetadata? get metadata => _metadata; + + /// The channel table as last read from the radio, index-ordered. + List get channels => List.unmodifiable(_channels); + + /// The freshest device metrics for [nodeNum] (battery, voltage, air time), + /// or null if that node has never reported any. + DeviceMetrics? metricsFor(int nodeNum) => _metrics[nodeNum]; + + /// When [metricsFor] last changed for [nodeNum] — a battery reading is only + /// meaningful next to its age. + DateTime? metricsAgeFor(int nodeNum) => _metricsAt[nodeNum]; + + /// Records a channel we just wrote, so the cached table doesn't keep + /// reporting the pre-write state for the rest of the session (which would + /// make a second provisioning pass rewrite the same slot). + void cacheChannel(Channel channel) { + while (_channels.length <= channel.index) { + _channels.add(Channel()); + } + _channels[channel.index] = channel; + } + + /// The radio's LoRa config (region, preset), or null before the download. + /// + /// Held separately from [_config] on purpose: `Config` carries its sections + /// in a **oneof**, and the radio sends one section per packet, so the last + /// packet of the download (bluetooth) would otherwise be the only section + /// left standing. + Config_LoRaConfig? get loraConfig => _lora; + MeshtasticConfigWrapper? get config => + _config != null && _moduleConfig != null + ? MeshtasticConfigWrapper( + config: _config!, + moduleConfig: _moduleConfig!, + channels: _channels, + ) + : null; + User? get localUser => _localUser; + bool get isConnected => _device?.isConnected ?? false; + bool get isConfigured => _configComplete; + + /// Initialize the client and request necessary permissions + Future initialize() async { + _logger.info('Initializing Meshtastic client'); + + // Check if Bluetooth is supported + if (await FlutterBluePlus.isSupported == false) { + throw const BluetoothException('Bluetooth not supported on this device'); + } + + // Request permissions + await _requestPermissions(); + + // Check if Bluetooth is enabled + final state = await FlutterBluePlus.adapterState.first; + if (state != BluetoothAdapterState.on) { + throw const BluetoothException('Bluetooth is not enabled'); + } + + _logger.info('Meshtastic client initialized successfully'); + } + + /// Request necessary permissions for BLE + Future _requestPermissions() async { + final permissions = [ + Permission.bluetooth, + Permission.bluetoothConnect, + Permission.bluetoothScan, + Permission.locationWhenInUse, + ]; + + for (final permission in permissions) { + final status = await permission.request(); + if (!status.isGranted) { + throw PermissionException('Permission denied: $permission'); + } + } + } + + /// Scan for nearby Meshtastic devices. The stream **always closes** when the + /// timeout elapses or the listener cancels. + /// + /// That guarantee is the whole point of the rewrite here. The obvious + /// `await for (results in FlutterBluePlus.scanResults)` shape cannot deliver + /// it: `scanResults` is a broadcast stream that is never closed, and + /// `stopScan` doesn't emit, so a loop that breaks on a flag only breaks when + /// the *next* advertisement arrives — and after `stopScan` there is no next + /// one. With no radio in range the generator hangs forever, taking with it + /// every caller awaiting the scan's end: the picker's spinner never stops, + /// and a reconnect that falls back to a scan never finishes, never retries, + /// and never resumes. + Stream scanForDevices({ + Duration timeout = const Duration(seconds: 10), + }) { + final controller = StreamController(); + StreamSubscription>? results; + Timer? deadline; + var closing = false; + + Future finish() async { + if (closing) return; + closing = true; + deadline?.cancel(); + await results?.cancel(); + try { + await FlutterBluePlus.stopScan(); + } catch (e) { + _logger.warning('Error stopping scan: $e'); + } + if (!controller.isClosed) await controller.close(); + } + + controller.onListen = () async { + _logger.info('Scanning for Meshtastic devices'); + results = FlutterBluePlus.scanResults.listen( + (batch) { + for (final result in batch) { + final device = result.device; + if (device.platformName.isNotEmpty || + result.advertisementData.serviceUuids.contains( + Guid(_serviceUuid), + )) { + if (!controller.isClosed) controller.add(device); + } + } + }, + onError: (Object error, StackTrace stackTrace) { + if (!controller.isClosed) controller.addError(error, stackTrace); + }, + ); + deadline = Timer(timeout, finish); + try { + await FlutterBluePlus.startScan( + withServices: [Guid(_serviceUuid)], + timeout: timeout, + ); + } catch (error, stackTrace) { + if (!controller.isClosed) controller.addError(error, stackTrace); + await finish(); + } + }; + controller.onCancel = finish; + return controller.stream; + } + + /// Connect to a radio by its BLE id, without a preceding scan. + /// + /// How a reconnect works after an app restart: the id is the Android MAC / + /// iOS peripheral UUID, and both platforms can open a link to a known id + /// directly. On iOS the UUID is only meaningful to this app (and only while + /// the system still remembers the peripheral), so a caller must be ready for + /// this to fail and fall back to a scan. + Future connectToId(String remoteId) => + connectToDevice(BluetoothDevice.fromId(remoteId)); + + /// Connect to a specific Meshtastic device + Future connectToDevice(BluetoothDevice device) async { + _logger.info( + 'Connecting to device: ${device.platformName} (${device.remoteId})', + ); + + try { + _emitConnectionState(MeshtasticConnectionState.connecting); + + // Disconnect from any existing device + await disconnect(); + + _device = device; + + // Listen for connection state changes + _connectionSubscription = device.connectionState.listen((state) { + if (state == BluetoothConnectionState.disconnected) { + _handleDisconnection(); + } + }); + + // `connect` already negotiates a 512-byte MTU on Android (and ignores + // the request on iOS, where CoreBluetooth negotiates it itself), so no + // separate requestMtu — that only bought a second round trip. + await device.connect(timeout: const Duration(seconds: 30)); + + // Discover services + final services = await device.discoverServices(); + final meshtasticService = services.firstWhere( + (service) => + service.uuid.toString().toLowerCase() == _serviceUuid.toLowerCase(), + orElse: () => + throw const ConnectionException('Meshtastic service not found'), + ); + + // Get characteristics + _toRadioChar = meshtasticService.characteristics.firstWhere( + (char) => + char.uuid.toString().toLowerCase() == _toRadioUuid.toLowerCase(), + orElse: () => + throw const ConnectionException('ToRadio characteristic not found'), + ); + + _fromRadioChar = meshtasticService.characteristics.firstWhere( + (char) => + char.uuid.toString().toLowerCase() == _fromRadioUuid.toLowerCase(), + orElse: () => throw const ConnectionException( + 'FromRadio characteristic not found', + ), + ); + + _fromNumChar = meshtasticService.characteristics.firstWhere( + (char) => + char.uuid.toString().toLowerCase() == _fromNumUuid.toLowerCase(), + orElse: () => + throw const ConnectionException('FromNum characteristic not found'), + ); + + // Log characteristic properties for debugging + _logger.info( + 'ToRadio properties: write=${_toRadioChar!.properties.write}, ' + 'writeWithoutResponse=${_toRadioChar!.properties.writeWithoutResponse}', + ); + _logger.info( + 'FromRadio properties: read=${_fromRadioChar!.properties.read}, ' + 'notify=${_fromRadioChar!.properties.notify}', + ); + _logger.info( + 'FromNum properties: read=${_fromNumChar!.properties.read}, ' + 'notify=${_fromNumChar!.properties.notify}', + ); + + // Enable notifications on FromNum. + // + // `lastValueStream` (not `onValueReceived`) on purpose: it replays the + // cached value on subscribe, so a notification that lands between + // `setNotifyValue` and this `listen` can't be missed. Every emission + // just schedules a drain, so the replayed value costs one empty read. + await _fromNumChar!.setNotifyValue(true); + _fromNumSubscription = _fromNumChar!.lastValueStream.listen( + _handleFromNumNotification, + ); + + _emitConnectionState(MeshtasticConnectionState.configuring); + + // Start configuration process + await _startConfiguration(); + + _logger.info('Successfully connected to device'); + } catch (e) { + _logger.severe('Failed to connect to device: $e'); + _emitConnectionState( + MeshtasticConnectionState.error, + errorMessage: e.toString(), + ); + rethrow; + } + } + + /// Disconnect from the current device + Future disconnect() async { + _logger.info('Disconnecting from device'); + + await _fromNumSubscription?.cancel(); + _fromNumSubscription = null; + + await _connectionSubscription?.cancel(); + _connectionSubscription = null; + + if (_device?.isConnected == true) { + await _device!.disconnect(); + } + + _device = null; + _toRadioChar = null; + _fromRadioChar = null; + _fromNumChar = null; + + _configComplete = false; + _drainRequested = false; + _pendingEmptyRetries = 0; + _nodes.clear(); + _metrics.clear(); + _metricsAt.clear(); + _myNodeInfo = null; + _config = null; + _lora = null; + _metadata = null; + _moduleConfig = null; + _channels.clear(); + _localUser = null; + + _emitConnectionState(MeshtasticConnectionState.disconnected); + } + + /// Send a text message to a specific node or broadcast + Future sendTextMessage( + String message, { + int? destinationId, + int channel = 0, + }) async { + if (!isConnected) { + throw const ConnectionException('Not connected to a device'); + } + + if (!isConfigured) { + throw const ConnectionException('Device configuration not complete'); + } + + // Generate a random packet ID + final packetId = DateTime.now().millisecondsSinceEpoch & 0xFFFFFFFF; + + final packet = MeshPacket( + from: _myNodeInfo?.myNodeNum ?? 0, // Set sender node ID + to: destinationId ?? 0xFFFFFFFF, // 0xFFFFFFFF for broadcast + channel: channel, + id: packetId, + decoded: Data( + portnum: PortNum.TEXT_MESSAGE_APP, + payload: utf8.encode(message), + ), + wantAck: destinationId != null, // Request ACK for direct messages + hopLimit: 3, + priority: MeshPacket_Priority.DEFAULT, + ); + + _logger.info( + 'Sending text message: "$message" from ${packet.from.toRadixString(16)} to ${packet.to.toRadixString(16)} on channel $channel', + ); + await _sendPacket(packet); + } + + /// Send a position update + Future sendPosition( + double latitude, + double longitude, { + int? altitude, + }) async { + if (!isConnected) { + throw const ConnectionException('Not connected to a device'); + } + + if (!isConfigured) { + throw const ConnectionException('Device configuration not complete'); + } + + final position = Position( + latitudeI: (latitude * 1e7).round(), + longitudeI: (longitude * 1e7).round(), + altitude: altitude, + time: DateTime.now().millisecondsSinceEpoch ~/ 1000, + ); + + // Generate a random packet ID + final packetId = DateTime.now().millisecondsSinceEpoch & 0xFFFFFFFF; + + final packet = MeshPacket( + from: _myNodeInfo?.myNodeNum ?? 0, // Set sender node ID + to: 0xFFFFFFFF, // Broadcast + id: packetId, + decoded: Data( + portnum: PortNum.POSITION_APP, + payload: position.writeToBuffer(), + ), + hopLimit: 3, + priority: MeshPacket_Priority.DEFAULT, + ); + + _logger.info( + 'Sending position: lat=$latitude, lon=$longitude, alt=$altitude', + ); + await _sendPacket(packet); + } + + /// Send an arbitrary payload on [portnum]. + /// + /// The general form behind [sendTextMessage]: any app port, any channel, + /// broadcast or direct. `from` is deliberately left unset — the firmware + /// overwrites it ("we don't let clients assign nodenums"), and a zero `from` + /// is what marks a packet as locally originated, which is what exempts + /// [sendAdmin] from the remote-admin session key. + Future sendData({ + required PortNum portnum, + required List payload, + int channel = 0, + int? destination, + bool wantAck = false, + bool wantResponse = false, + }) async { + if (!isConnected) { + throw const ConnectionException('Not connected to a device'); + } + if (!isConfigured) { + throw const ConnectionException('Device configuration not complete'); + } + final packet = MeshPacket( + to: destination ?? 0xFFFFFFFF, + channel: channel, + id: _nextPacketId(), + decoded: Data( + portnum: portnum, + payload: payload, + wantResponse: wantResponse, + ), + wantAck: wantAck, + hopLimit: 3, + priority: MeshPacket_Priority.DEFAULT, + ); + _logger.info( + 'Sending $portnum: ${payload.length} bytes on channel $channel', + ); + await _sendPacket(packet); + } + + /// Sends an [AdminMessage] to the attached radio itself. + /// + /// Local admin only — addressed to our own node so the firmware handles it + /// on the local path, where `mp.from == 0` skips the session-key check that + /// guards remote administration. + Future sendAdmin(AdminMessage message, {bool wantResponse = false}) { + final myNum = myNodeNum; + if (myNum == null) { + throw const ConnectionException('Node info not available yet'); + } + return sendData( + portnum: PortNum.ADMIN_APP, + payload: message.writeToBuffer(), + destination: myNum, + wantResponse: wantResponse, + ); + } + + /// Writes one `ToRadio` frame, using a long write where the platform needs + /// one. + /// + /// iOS is the reason this exists. A plain write is capped at the negotiated + /// ATT MTU minus 3 — 182 bytes on iOS against 509 on Android — so a + /// full-size payload (a max-length DPIP packet, a long CJK message) succeeds + /// on Android and fails on iOS with `data longer than allowed`. Asking for a + /// long write switches CoreBluetooth to queued `WriteWithResponse`, which + /// carries the full 512 bytes. It is mutually exclusive with + /// write-without-response, so it only applies when the characteristic + /// actually supports write-with-response (every Meshtastic radio does). + Future _writeToRadio(List data) { + final characteristic = _toRadioChar!; + if (characteristic.properties.write) { + return characteristic.write(data, allowLongWrite: true); + } + return characteristic.write(data, withoutResponse: true); + } + + /// Packet ids only need to be unique among in-flight packets. + int _nextPacketId() => DateTime.now().millisecondsSinceEpoch & 0xFFFFFFFF; + + /// Takes the device metrics out of a telemetry packet. + /// + /// This is where a live battery reading actually comes from — for the + /// attached radio as much as for anyone else on the mesh, because it + /// broadcasts its own telemetry like any other node. The node DB copy is + /// updated too, so a re-emitted node carries the new numbers. + void _absorbTelemetry(MeshPacketWrapper packet) { + final payload = packet.decoded?.payload; + if (payload == null || payload.isEmpty) return; + try { + final telemetry = Telemetry.fromBuffer(payload); + if (!telemetry.hasDeviceMetrics()) return; + final from = packet.from; + _metrics[from] = telemetry.deviceMetrics; + _metricsAt[from] = DateTime.now(); + final node = _nodes[from]; + if (node != null) { + node.original.deviceMetrics = telemetry.deviceMetrics; + _nodeController.add(node); + } + _logger.info( + 'Device metrics from ${from.toRadixString(16)}: ' + 'battery=${telemetry.deviceMetrics.batteryLevel}% ' + 'voltage=${telemetry.deviceMetrics.voltage}V', + ); + } catch (e) { + _logger.warning('Unreadable telemetry: $e'); + } + } + + void _emitAdminReply(MeshPacketWrapper packet) { + final payload = packet.decoded?.payload; + if (payload == null || payload.isEmpty) return; + try { + _adminController.add(AdminMessage.fromBuffer(payload)); + } catch (e) { + _logger.warning('Unreadable admin reply: $e'); + } + } + + /// Send a packet to the device + Future _sendPacket(MeshPacket packet) async { + if (_toRadioChar == null) { + throw const ConnectionException('ToRadio characteristic not available'); + } + + final toRadio = ToRadio(packet: packet); + final data = toRadio.writeToBuffer(); + + if (data.length > _maxPacketSize) { + throw const ProtocolException('Packet too large'); + } + + _logger.info( + 'Sending packet: from=${packet.from.toRadixString(16)}, to=${packet.to.toRadixString(16)}, ' + 'id=${packet.id}, portnum=${packet.decoded.portnum}, size=${data.length} bytes', + ); + + await _writeToRadio(data); + _logger.fine('Packet sent successfully'); + } + + /// Start the configuration process + Future _startConfiguration() async { + _logger.info('Starting configuration process'); + + // Send wantConfigId to start configuration download + await _writeToRadio(ToRadio(wantConfigId: 0).writeToBuffer()); + + // One drain covers both halves of the handshake: the config download, and + // — once the radio has sent `config_complete_id` and moved on to + // STATE_SEND_PACKETS — the backlog of packets it queued while no phone was + // connected. Stopping at `config_complete_id` would silently drop every + // message that arrived during the disconnected window. + final drained = await _requestDrain(emptyRetries: _configEmptyReadRetries); + + if (!_configComplete && drained) { + // Firmware that never sent `config_complete_id`: an empty mailbox is the + // end of the download. + _logger.warning('Config download ended without config_complete_id'); + _markConfigured(); + } + if (!_configComplete) { + throw const ConnectionException( + 'Configuration download failed — the radio stopped responding', + ); + } + } + + /// Mark the config handshake finished and report the device as connected. + void _markConfigured() { + if (_configComplete) return; + _configComplete = true; + _emitConnectionState(MeshtasticConnectionState.connected); + } + + /// Process incoming data from FromRadio characteristic + Future _processFromRadioData(List data) async { + try { + final fromRadio = FromRadio.fromBuffer(data); + _logger.fine('Received FromRadio: ${fromRadio.toString()}'); + + if (fromRadio.hasMyInfo()) { + _myNodeInfo = fromRadio.myInfo; + _logger.info( + 'Received MyNodeInfo: myNodeNum=${_myNodeInfo!.myNodeNum.toRadixString(16)}', + ); + } + + if (fromRadio.hasNodeInfo()) { + final nodeInfo = NodeInfoWrapper(fromRadio.nodeInfo); + _nodes[nodeInfo.num] = nodeInfo; + final stored = nodeInfo.deviceMetrics; + // Only as a starting point — a later telemetry packet overwrites it. + if (stored != null && !_metrics.containsKey(nodeInfo.num)) { + _metrics[nodeInfo.num] = stored; + } + _nodeController.add(nodeInfo); + _logger.info( + 'Received NodeInfo: num=${nodeInfo.num.toRadixString(16)}, ' + 'displayName=${nodeInfo.displayName}', + ); + + // Extract user info from the node info + if (nodeInfo.user != null && + _localUser == null && + _myNodeInfo != null && + nodeInfo.num == _myNodeInfo!.myNodeNum) { + _localUser = nodeInfo.user; + _logger.info( + 'Received local User: longName=${_localUser!.longName}, ' + 'shortName=${_localUser!.shortName}', + ); + } + } + + if (fromRadio.hasMetadata()) { + _metadata = fromRadio.metadata; + _logger.info( + 'Received DeviceMetadata: firmware=${_metadata!.firmwareVersion} ' + 'hw=${_metadata!.hwModel}', + ); + } + + if (fromRadio.hasConfig()) { + _config = fromRadio.config; + if (fromRadio.config.hasLora()) _lora = fromRadio.config.lora; + _logger.info('Received Config'); + } + + if (fromRadio.hasModuleConfig()) { + _moduleConfig = fromRadio.moduleConfig; + _logger.info('Received ModuleConfig'); + } + + if (fromRadio.hasChannel()) { + final channel = fromRadio.channel; + if (channel.index < _channels.length) { + _channels[channel.index] = channel; + } else { + while (_channels.length <= channel.index) { + _channels.add(Channel()); + } + _channels[channel.index] = channel; + } + _logger.info('Received Channel ${channel.index}'); + } + + if (fromRadio.hasPacket()) { + final packetWrapper = MeshPacketWrapper(fromRadio.packet); + _packetController.add(packetWrapper); + _logger.info('Received MeshPacket: ${packetWrapper.toString()}'); + if (packetWrapper.portnum == PortNum.ADMIN_APP) { + _emitAdminReply(packetWrapper); + } + if (packetWrapper.portnum == PortNum.TELEMETRY_APP) { + _absorbTelemetry(packetWrapper); + } + } + + if (fromRadio.hasConfigCompleteId()) { + _logger.info('Configuration complete'); + _markConfigured(); + + // Log summary of received configuration + _logger.info('Configuration summary:'); + _logger.info(' MyNodeInfo: ${_myNodeInfo != null ? "✓" : "✗"}'); + _logger.info(' Config: ${_config != null ? "✓" : "✗"}'); + _logger.info(' ModuleConfig: ${_moduleConfig != null ? "✓" : "✗"}'); + _logger.info(' Channels: ${_channels.length}'); + _logger.info(' Nodes: ${_nodes.length}'); + _logger.info(' LocalUser: ${_localUser != null ? "✓" : "✗"}'); + } + } catch (e) { + _logger.warning('Error processing FromRadio data: $e'); + throw ProtocolException('Failed to parse FromRadio data', e); + } + } + + /// Handle FromNum notifications + void _handleFromNumNotification(List data) { + if (data.length >= 4) { + final bytes = Uint8List.fromList(data); + final fromNum = ByteData.view(bytes.buffer).getUint32(0, Endian.little); + _logger.fine('FromNum notification: $fromNum'); + } + // The number itself is advisory: it is the radio's own packet counter and + // it restarts when the radio reboots, so gating reads on "greater than the + // last one seen" stops delivering packets after a reboot. The reference + // clients ignore the value and just drain the mailbox — so do we. + unawaited(_requestDrain()); + } + + /// Read `fromradio` until the radio's mailbox is empty. + /// + /// Single-flight: while a drain is running, a new request only raises a flag + /// (and its retry budget), so notifications never start a second read loop + /// interleaved with the first on the same characteristic. The returned + /// future completes once the requester's own pass has run. + /// + /// [emptyRetries] is how many empty reads to tolerate before declaring the + /// mailbox drained; the config handshake allows a few because the radio + /// refills `fromradio` asynchronously, a notification-driven drain does not + /// need to wait. + Future _requestDrain({int emptyRetries = 0}) { + if (emptyRetries > _pendingEmptyRetries) { + _pendingEmptyRetries = emptyRetries; + } + _drainRequested = true; + return _drainTask ??= _drainFromRadio().whenComplete( + () => _drainTask = null, + ); + } + + Future _drainFromRadio() async { + var clean = true; + while (_drainRequested) { + _drainRequested = false; + final retries = _pendingEmptyRetries; + _pendingEmptyRetries = 0; + clean = await _readUntilEmpty(retries); + if (!clean) break; + } + return clean; + } + + /// One read pass. Returns `false` if it stopped on a transport error rather + /// than on an empty mailbox. + Future _readUntilEmpty(int emptyRetries) async { + var retries = 0; + while (true) { + final char = _fromRadioChar; + if (char == null) return false; + + List data; + try { + data = await char.read(); + } catch (e) { + _logger.warning('Error reading from FromRadio: $e'); + return false; + } + + if (data.isEmpty) { + if (retries >= emptyRetries) return true; + retries++; + await Future.delayed(_emptyReadBackoff); + continue; + } + retries = 0; + + try { + await _processFromRadioData(data); + } catch (e) { + // One unparseable packet must not abandon the rest of the backlog. + _logger.warning('Skipping unreadable FromRadio packet: $e'); + } + } + } + + /// Handle disconnection + void _handleDisconnection() { + _logger.info('Device disconnected'); + // The next connection re-runs the handshake from scratch; leaving this set + // would make `isConfigured` lie and skip the config download (and with it + // the backlog replay) on reconnect. + _configComplete = false; + _emitConnectionState(MeshtasticConnectionState.disconnected); + } + + /// Emit connection state change + void _emitConnectionState( + MeshtasticConnectionState state, { + String? errorMessage, + }) { + final status = ConnectionStatus( + state: state, + deviceAddress: _device?.remoteId.toString(), + deviceName: _device?.platformName, + errorMessage: errorMessage, + timestamp: DateTime.now(), + ); + + _connectionController.add(status); + } + + /// Dispose of the client and clean up resources + void dispose() { + _logger.info('Disposing Meshtastic client'); + + disconnect(); + _connectionController.close(); + _packetController.close(); + _nodeController.close(); + _adminController.close(); + } +} diff --git a/third_party/meshtastic_flutter/lib/src/models/connection_state.dart b/third_party/meshtastic_flutter/lib/src/models/connection_state.dart new file mode 100644 index 000000000..158620bcb --- /dev/null +++ b/third_party/meshtastic_flutter/lib/src/models/connection_state.dart @@ -0,0 +1,74 @@ +/// Connection state for the Meshtastic device +enum MeshtasticConnectionState { + /// Not connected to any device + disconnected, + + /// Currently attempting to connect + connecting, + + /// Connected and receiving configuration + configuring, + + /// Connected and ready for communication + connected, + + /// Connection lost or error occurred + error, +} + +/// Represents the current connection state with additional metadata +class ConnectionStatus { + final MeshtasticConnectionState state; + final String? deviceAddress; + final String? deviceName; + final String? errorMessage; + final DateTime timestamp; + + const ConnectionStatus({ + required this.state, + this.deviceAddress, + this.deviceName, + this.errorMessage, + required this.timestamp, + }); + + ConnectionStatus copyWith({ + MeshtasticConnectionState? state, + String? deviceAddress, + String? deviceName, + String? errorMessage, + DateTime? timestamp, + }) { + return ConnectionStatus( + state: state ?? this.state, + deviceAddress: deviceAddress ?? this.deviceAddress, + deviceName: deviceName ?? this.deviceName, + errorMessage: errorMessage ?? this.errorMessage, + timestamp: timestamp ?? this.timestamp, + ); + } + + @override + String toString() { + return 'ConnectionStatus(state: $state, deviceAddress: $deviceAddress, ' + 'deviceName: $deviceName, errorMessage: $errorMessage, timestamp: $timestamp)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ConnectionStatus && + other.state == state && + other.deviceAddress == deviceAddress && + other.deviceName == deviceName && + other.errorMessage == errorMessage; + } + + @override + int get hashCode { + return state.hashCode ^ + deviceAddress.hashCode ^ + deviceName.hashCode ^ + errorMessage.hashCode; + } +} diff --git a/third_party/meshtastic_flutter/lib/src/models/mesh_packet_wrapper.dart b/third_party/meshtastic_flutter/lib/src/models/mesh_packet_wrapper.dart new file mode 100644 index 000000000..d44cde1f3 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/src/models/mesh_packet_wrapper.dart @@ -0,0 +1,158 @@ +import 'dart:convert'; + +import '../../generated/mesh.pb.dart'; +import '../../generated/portnums.pb.dart'; + +/// Wrapper class for MeshPacket with additional convenience methods +class MeshPacketWrapper { + final MeshPacket packet; + + const MeshPacketWrapper(this.packet); + + /// The original packet + MeshPacket get original => packet; + + /// Sender node ID + int get from => packet.from; + + /// Destination node ID (0 for broadcast) + int get to => packet.to; + + /// Channel this packet was sent on + int get channel => packet.channel; + + /// Packet ID for tracking + int get id => packet.id; + + /// Hop limit for routing + int get hopLimit => packet.hopLimit; + + /// Priority level + MeshPacket_Priority get priority => packet.priority; + + /// Whether this packet wants an ACK + bool get wantAck => packet.wantAck; + + /// Timestamp when packet was received + int get rxTime => packet.rxTime; + + /// Signal strength (RSSI) + int get rxRssi => packet.rxRssi; + + /// Signal to noise ratio + double get rxSnr => packet.rxSnr; + + /// The decoded data payload + Data? get decoded => packet.hasDecoded() ? packet.decoded : null; + + /// The encrypted payload (if not decoded) + List? get encrypted => packet.hasEncrypted() ? packet.encrypted : null; + + /// The port number indicating the app/service + PortNum? get portnum => decoded?.portnum; + + /// Whether this is a text message + bool get isTextMessage => portnum == PortNum.TEXT_MESSAGE_APP; + + /// Whether this is telemetry data + bool get isTelemetry => portnum == PortNum.TELEMETRY_APP; + + /// Whether this is a position update + bool get isPosition => portnum == PortNum.POSITION_APP; + + /// Whether this is a node info update + bool get isNodeInfo => portnum == PortNum.NODEINFO_APP; + + /// Whether this is a routing packet + bool get isRouting => portnum == PortNum.ROUTING_APP; + + /// Whether this is an admin packet + bool get isAdmin => portnum == PortNum.ADMIN_APP; + + /// Get the text message content (if this is a text message) + String? get textMessage { + if (!isTextMessage || decoded == null) return null; + try { + return utf8.decode(decoded!.payload, allowMalformed: true); + } catch (e) { + return null; + } + } + + /// Get the JSON payload as a string (if applicable) + String? get jsonPayload { + if (decoded == null) return null; + try { + return utf8.decode(decoded!.payload, allowMalformed: true); + } catch (e) { + return null; + } + } + + /// Whether this packet is encrypted + bool get isEncrypted => packet.hasEncrypted(); + + /// Whether this packet has been decoded + bool get isDecoded => packet.hasDecoded(); + + /// Whether this is a broadcast message (to == 0) + bool get isBroadcast => to == 0; + + /// Whether this is a direct message (to != 0) + bool get isDirectMessage => to != 0; + + /// Get a human-readable description of the packet type + String get packetTypeDescription { + if (portnum == null) return 'Unknown'; + + switch (portnum!) { + case PortNum.TEXT_MESSAGE_APP: + return 'Text Message'; + case PortNum.REMOTE_HARDWARE_APP: + return 'Remote Hardware'; + case PortNum.POSITION_APP: + return 'Position'; + case PortNum.NODEINFO_APP: + return 'Node Info'; + case PortNum.ROUTING_APP: + return 'Routing'; + case PortNum.ADMIN_APP: + return 'Admin'; + case PortNum.TELEMETRY_APP: + return 'Telemetry'; + case PortNum.ZPS_APP: + return 'ZPS'; + case PortNum.SIMULATOR_APP: + return 'Simulator'; + case PortNum.TRACEROUTE_APP: + return 'Traceroute'; + case PortNum.NEIGHBORINFO_APP: + return 'Neighbor Info'; + case PortNum.ATAK_PLUGIN: + return 'ATAK Plugin'; + case PortNum.MAP_REPORT_APP: + return 'Map Report'; + case PortNum.PRIVATE_APP: + return 'Private App'; + case PortNum.ATAK_FORWARDER: + return 'ATAK Forwarder'; + default: + return 'Unknown (${portnum!.value})'; + } + } + + @override + String toString() { + return 'MeshPacketWrapper(from: $from, to: $to, channel: $channel, ' + 'type: $packetTypeDescription, id: $id, encrypted: $isEncrypted)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is MeshPacketWrapper && other.packet == packet; + } + + @override + int get hashCode => packet.hashCode; +} diff --git a/third_party/meshtastic_flutter/lib/src/models/meshtastic_config.dart b/third_party/meshtastic_flutter/lib/src/models/meshtastic_config.dart new file mode 100644 index 000000000..30f6f12d8 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/src/models/meshtastic_config.dart @@ -0,0 +1,165 @@ +import '../../generated/config.pb.dart'; +import '../../generated/module_config.pb.dart'; +import '../../generated/channel.pb.dart'; + +/// Wrapper for Meshtastic device configuration +class MeshtasticConfigWrapper { + final Config config; + final ModuleConfig moduleConfig; + final List channels; + + const MeshtasticConfigWrapper({ + required this.config, + required this.moduleConfig, + required this.channels, + }); + + /// Device configuration + Config_DeviceConfig? get deviceConfig => + config.hasDevice() ? config.device : null; + + /// Position configuration + Config_PositionConfig? get positionConfig => + config.hasPosition() ? config.position : null; + + /// Power configuration + Config_PowerConfig? get powerConfig => + config.hasPower() ? config.power : null; + + /// Network configuration + Config_NetworkConfig? get networkConfig => + config.hasNetwork() ? config.network : null; + + /// Display configuration + Config_DisplayConfig? get displayConfig => + config.hasDisplay() ? config.display : null; + + /// LoRa configuration + Config_LoRaConfig? get loraConfig => config.hasLora() ? config.lora : null; + + /// Bluetooth configuration + Config_BluetoothConfig? get bluetoothConfig => + config.hasBluetooth() ? config.bluetooth : null; + + /// MQTT module configuration + ModuleConfig_MQTTConfig? get mqttConfig => + moduleConfig.hasMqtt() ? moduleConfig.mqtt : null; + + /// Serial module configuration + ModuleConfig_SerialConfig? get serialConfig => + moduleConfig.hasSerial() ? moduleConfig.serial : null; + + /// External notification configuration + ModuleConfig_ExternalNotificationConfig? get externalNotificationConfig => + moduleConfig.hasExternalNotification() + ? moduleConfig.externalNotification + : null; + + /// Store and forward configuration + ModuleConfig_StoreForwardConfig? get storeForwardConfig => + moduleConfig.hasStoreForward() ? moduleConfig.storeForward : null; + + /// Range test configuration + ModuleConfig_RangeTestConfig? get rangeTestConfig => + moduleConfig.hasRangeTest() ? moduleConfig.rangeTest : null; + + /// Telemetry configuration + ModuleConfig_TelemetryConfig? get telemetryConfig => + moduleConfig.hasTelemetry() ? moduleConfig.telemetry : null; + + /// Canned message configuration + ModuleConfig_CannedMessageConfig? get cannedMessageConfig => + moduleConfig.hasCannedMessage() ? moduleConfig.cannedMessage : null; + + /// Audio configuration + ModuleConfig_AudioConfig? get audioConfig => + moduleConfig.hasAudio() ? moduleConfig.audio : null; + + /// Remote hardware configuration + ModuleConfig_RemoteHardwareConfig? get remoteHardwareConfig => + moduleConfig.hasRemoteHardware() ? moduleConfig.remoteHardware : null; + + /// Neighbor info configuration + ModuleConfig_NeighborInfoConfig? get neighborInfoConfig => + moduleConfig.hasNeighborInfo() ? moduleConfig.neighborInfo : null; + + /// Ambient lighting configuration + ModuleConfig_AmbientLightingConfig? get ambientLightingConfig => + moduleConfig.hasAmbientLighting() ? moduleConfig.ambientLighting : null; + + /// Detection sensor configuration + ModuleConfig_DetectionSensorConfig? get detectionSensorConfig => + moduleConfig.hasDetectionSensor() ? moduleConfig.detectionSensor : null; + + /// Paxcounter configuration + ModuleConfig_PaxcounterConfig? get paxcounterConfig => + moduleConfig.hasPaxcounter() ? moduleConfig.paxcounter : null; + + /// Primary channel (index 0) + Channel? get primaryChannel => channels.isNotEmpty ? channels[0] : null; + + /// Secondary channels (index 1+) + List get secondaryChannels => + channels.length > 1 ? channels.sublist(1) : []; + + /// All channels that are enabled + List get enabledChannels => channels + .where((ch) => ch.hasSettings() && ch.settings.name.isNotEmpty) + .toList(); + + /// Device role + Config_DeviceConfig_Role? get deviceRole => deviceConfig?.role; + + /// Node info broadcast interval (seconds) + int? get nodeInfoBroadcastSecs => deviceConfig?.nodeInfoBroadcastSecs; + + /// Double tap as button press + bool get doubleTapAsButtonPress => + deviceConfig?.doubleTapAsButtonPress ?? false; + + /// GPS operation mode + Config_PositionConfig_GpsMode? get gpsMode => positionConfig?.gpsMode; + + /// GPS update interval (seconds) + int? get gpsUpdateInterval => positionConfig?.gpsUpdateInterval; + + /// Position broadcast interval (seconds) + int? get positionBroadcastSecs => positionConfig?.positionBroadcastSecs; + + /// Whether GPS is enabled + bool get gpsEnabled => gpsMode == Config_PositionConfig_GpsMode.ENABLED; + + /// LoRa region + Config_LoRaConfig_RegionCode? get region => loraConfig?.region; + + /// Hop limit + int? get hopLimit => loraConfig?.hopLimit; + + /// Transmit enabled + bool get txEnabled => loraConfig?.txEnabled ?? true; + + /// Transmit power level + int? get txPower => loraConfig?.txPower; + + /// Channel number + int? get channelNum => loraConfig?.channelNum; + + /// Override duty cycle limit + bool get overrideDutyCycle => loraConfig?.overrideDutyCycle ?? false; + + /// Whether Bluetooth is enabled + bool get bluetoothEnabled => bluetoothConfig?.enabled ?? true; + + /// Bluetooth mode + Config_BluetoothConfig_PairingMode? get bluetoothMode => + bluetoothConfig?.mode; + + /// Fixed PIN for Bluetooth pairing + int? get fixedPin => bluetoothConfig?.fixedPin; + + @override + String toString() { + return 'MeshtasticConfigWrapper(deviceRole: $deviceRole, region: $region, ' + 'channels: ${channels.length}, bluetoothEnabled: $bluetoothEnabled)'; + } +} diff --git a/third_party/meshtastic_flutter/lib/src/models/node_info.dart b/third_party/meshtastic_flutter/lib/src/models/node_info.dart new file mode 100644 index 000000000..fad8c8c46 --- /dev/null +++ b/third_party/meshtastic_flutter/lib/src/models/node_info.dart @@ -0,0 +1,196 @@ +import 'dart:math' as math; +import '../../generated/mesh.pb.dart'; +import '../../generated/config.pb.dart'; +import '../../generated/telemetry.pb.dart'; + +/// Enhanced wrapper for NodeInfo with convenience methods +class NodeInfoWrapper { + final NodeInfo nodeInfo; + + const NodeInfoWrapper(this.nodeInfo); + + /// The original NodeInfo + NodeInfo get original => nodeInfo; + + /// Node ID + int get num => nodeInfo.num; + + /// User information + User? get user => nodeInfo.hasUser() ? nodeInfo.user : null; + + /// Position information + Position? get position => nodeInfo.hasPosition() ? nodeInfo.position : null; + + /// Signal metrics + DeviceMetrics? get deviceMetrics { + if (!nodeInfo.hasDeviceMetrics()) return null; + final telemetry = nodeInfo.deviceMetrics; + // telemetry is already a DeviceMetrics object + return telemetry; + } + + /// Channel utilization + int get channel => nodeInfo.channel; + + /// Whether the node is online/reachable + bool get isOnline => + nodeInfo.hasLastHeard() && + (DateTime.now().millisecondsSinceEpoch - (nodeInfo.lastHeard * 1000)) < + (15 * 60 * 1000); // 15 minutes + + /// Last heard timestamp + DateTime? get lastHeard => nodeInfo.hasLastHeard() + ? DateTime.fromMillisecondsSinceEpoch(nodeInfo.lastHeard * 1000) + : null; + + /// SNR (Signal to Noise Ratio) + double get snr => nodeInfo.snr; + + /// User's long name + String? get longName => user?.longName; + + /// User's short name + String? get shortName => user?.shortName; + + /// Hardware model + HardwareModel? get hwModel => user?.hwModel; + + /// Whether this node is licensed amateur radio + bool get isLicensed => user?.isLicensed ?? false; + + /// Whether the radio only ever heard this node through an MQTT bridge — + /// i.e. over the internet, not over the air. Such a node can be anywhere on + /// the planet, so it says nothing about radio reachability. + bool get viaMqtt => nodeInfo.viaMqtt; + + /// Role of this node + Config_DeviceConfig_Role? get role => user?.role; + + /// Current latitude (if position available) + double? get latitude { + if (position == null || !position!.hasLatitudeI()) return null; + return position!.latitudeI / 1e7; + } + + /// Current longitude (if position available) + double? get longitude { + if (position == null || !position!.hasLongitudeI()) return null; + return position!.longitudeI / 1e7; + } + + /// Current altitude (if position available) + int? get altitude => + position?.hasAltitude() == true ? position!.altitude : null; + + /// Battery level percentage (if device metrics available) + int? get batteryLevel => deviceMetrics?.hasBatteryLevel() == true + ? deviceMetrics!.batteryLevel + : null; + + /// Voltage (if device metrics available) + double? get voltage => + deviceMetrics?.hasVoltage() == true ? deviceMetrics!.voltage : null; + + /// Channel utilization percentage + double? get channelUtilization => + deviceMetrics?.hasChannelUtilization() == true + ? deviceMetrics!.channelUtilization + : null; + + /// Air utilization percentage + double? get airUtilTx => + deviceMetrics?.hasAirUtilTx() == true ? deviceMetrics!.airUtilTx : null; + + /// Distance from our position (requires both nodes to have position) + double? distanceFrom(NodeInfoWrapper otherNode) { + if (latitude == null || + longitude == null || + otherNode.latitude == null || + otherNode.longitude == null) { + return null; + } + + return _calculateDistance( + latitude!, + longitude!, + otherNode.latitude!, + otherNode.longitude!, + ); + } + + /// Calculate distance between two points using Haversine formula + static double _calculateDistance( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + const double earthRadius = 6371000; // Earth radius in meters + + double dLat = _degreesToRadians(lat2 - lat1); + double dLon = _degreesToRadians(lon2 - lon1); + + double a = + math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(_degreesToRadians(lat1)) * + math.cos(_degreesToRadians(lat2)) * + math.sin(dLon / 2) * + math.sin(dLon / 2); + + double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + + return earthRadius * c; + } + + static double _degreesToRadians(double degrees) { + return degrees * (math.pi / 180); + } + + /// Get a display name for this node (prioritizes long name, falls back to short name, then node ID) + String get displayName { + if (longName?.isNotEmpty == true) return longName!; + if (shortName?.isNotEmpty == true) return shortName!; + return 'Node ${num.toRadixString(16).toUpperCase()}'; + } + + /// Get a brief status description + String get statusDescription { + final parts = []; + + if (batteryLevel != null) { + parts.add('Battery: $batteryLevel%'); + } + + if (channelUtilization != null) { + parts.add('Channel: ${channelUtilization!.toStringAsFixed(1)}%'); + } + + if (lastHeard != null) { + final ago = DateTime.now().difference(lastHeard!); + if (ago.inMinutes < 60) { + parts.add('${ago.inMinutes}m ago'); + } else if (ago.inHours < 24) { + parts.add('${ago.inHours}h ago'); + } else { + parts.add('${ago.inDays}d ago'); + } + } + + return parts.join(' • '); + } + + @override + String toString() { + return 'NodeInfoWrapper(num: ${num.toRadixString(16)}, displayName: $displayName, ' + 'isOnline: $isOnline, lastHeard: $lastHeard)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is NodeInfoWrapper && other.nodeInfo == nodeInfo; + } + + @override + int get hashCode => nodeInfo.hashCode; +} diff --git a/third_party/meshtastic_flutter/pubspec.lock b/third_party/meshtastic_flutter/pubspec.lock new file mode 100644 index 000000000..60c3daee8 --- /dev/null +++ b/third_party/meshtastic_flutter/pubspec.lock @@ -0,0 +1,410 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + fixnum: + dependency: "direct main" + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" + url: "https://pub.dev" + source: hosted + version: "1.36.8" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" + url: "https://pub.dev" + source: hosted + version: "7.0.4" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 + url: "https://pub.dev" + source: hosted + version: "7.0.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 + url: "https://pub.dev" + source: hosted + version: "12.0.3" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8 + url: "https://pub.dev" + source: hosted + version: "9.6.1" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac" + url: "https://pub.dev" + source: hosted + version: "0.1.4+1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23 + url: "https://pub.dev" + source: hosted + version: "4.4.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd + url: "https://pub.dev" + source: hosted + version: "0.2.2" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + protobuf: + dependency: "direct main" + description: + name: protobuf + sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e + url: "https://pub.dev" + source: hosted + version: "4.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0-0 <4.0.0" + flutter: ">=3.24.0" diff --git a/third_party/meshtastic_flutter/pubspec.yaml b/third_party/meshtastic_flutter/pubspec.yaml new file mode 100644 index 000000000..0eddba972 --- /dev/null +++ b/third_party/meshtastic_flutter/pubspec.yaml @@ -0,0 +1,59 @@ +name: meshtastic_flutter +description: "A comprehensive Flutter package for communicating with Meshtastic devices over Bluetooth Low Energy (BLE)." +version: 0.0.3 +homepage: https://github.com/M4dhav/meshtastic_flutter + +environment: + sdk: ^3.8.1 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + protobuf: ^4.2.0 + flutter_blue_plus: ^1.35.5 + permission_handler: ^12.0.1 + logging: ^1.3.0 + fixnum: ^1.1.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/tool/gen_sky_textures.py b/tool/gen_sky_textures.py index f34aa7bc0..5aa7fca94 100644 --- a/tool/gen_sky_textures.py +++ b/tool/gen_sky_textures.py @@ -215,8 +215,10 @@ def main(): # The star field is the one texture where lossless costs real bytes and # buys nothing: it is consumed as a dim background, and near-lossless # WebP holds every star while halving the file. - ("starmap.webp", starmap(), dict(format="WEBP", quality=86, method=6)), - ("sun_rays.webp", sun_rays(), WEBP_LOSSLESS), + ("starmap.webp", starmap(), dict(format="WEBP", quality=75, method=6)), + # The ray fan is pure visual (the shader multiplies it dim); q85 keeps + # every ray edge while shrinking the file ~8x. + ("sun_rays.webp", sun_rays(), dict(format="WEBP", quality=85, method=6)), ("sun_profile.webp", sun_profile(), WEBP_LOSSLESS), ("annulus.webp", annulus(), WEBP_LOSSLESS), ):