mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore: standardize resources and update documentation for Navigation 3 (#4961)
This commit is contained in:
parent
1faa802fe6
commit
464a12b9f7
42 changed files with 216 additions and 149 deletions
22
.github/copilot-instructions.md
vendored
22
.github/copilot-instructions.md
vendored
|
|
@ -18,7 +18,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX + flavor-specific decoder). Shared contracts abstracted into `core:ui/commonMain`.
|
||||
- **UI:** Jetpack Compose Multiplatform (Material 3).
|
||||
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app` and `desktop`.
|
||||
- **Navigation:** JetBrains Navigation 3 (Stable Scene-based architecture) with shared backstack state.
|
||||
- **Navigation:** JetBrains Navigation 3 (Stable Scene-based architecture) with shared backstack state. Deep linking uses RESTful paths (e.g. `/nodes/1234`) parsed by `DeepLinkRouter` in `core:navigation`.
|
||||
- **Lifecycle:** JetBrains multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`.
|
||||
- **Adaptive UI:** Material 3 Adaptive (v1.3+) with support for Large (1200dp) and Extra-large (1600dp) breakpoints.
|
||||
- **Database:** Room KMP.
|
||||
|
|
@ -28,7 +28,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
| Directory | Description |
|
||||
| :--- | :--- |
|
||||
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
|
||||
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). |
|
||||
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`). |
|
||||
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
|
||||
| `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. |
|
||||
| `core/model` | Domain models and common data structures. |
|
||||
|
|
@ -39,9 +39,9 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
| `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). |
|
||||
| `core:domain` | Pure KMP business logic and UseCases. |
|
||||
| `core:data` | Core manager implementations and data orchestration. |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`). |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioInterface`). |
|
||||
| `core:di` | Common DI qualifiers and dispatchers. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3, `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` for backstack persistence. |
|
||||
| `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
|
||||
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
|
||||
| `core:api` | Public AIDL/API integration module for external clients. |
|
||||
|
|
@ -61,10 +61,10 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Material 3:** The app uses Material 3.
|
||||
- **Strings:** MUST use the **Compose Multiplatform Resource** library in `core:resources` (`stringResource(Res.string.your_key)`). For ViewModels or non-composable Coroutines, use the asynchronous `getStringSuspend(Res.string.your_key)`. NEVER use hardcoded strings, and NEVER use the blocking `getString()` in a coroutine.
|
||||
- **Dialogs:** Use centralized components in `core:ui` (e.g., `MeshtasticResourceDialog`).
|
||||
- **Alerts:** Use `AlertHost(alertManager)` from `core:ui/commonMain` in each platform host shell (`Main.kt`, `DesktopMainScreen.kt`). Do NOT duplicate inline alert-rendering boilerplate. For shared QR/contact dialogs, use the `SharedDialogs` composable.
|
||||
- **Alerts:** Use `AlertHost(alertManager)` from `core:ui/commonMain` in each platform host shell (`Main.kt`, `DesktopMainScreen.kt`). For global responses like traceroute and firmware validation, use the specialized common handlers: `TracerouteAlertHandler(uiViewModel)` and `FirmwareVersionCheck(uiViewModel)`. Do NOT duplicate inline alert-rendering logic or trigger alerts directly during composition. For shared QR/contact dialogs, use the `SharedDialogs(uiViewModel)` composable.
|
||||
- **Placeholders:** For desktop/JVM features not yet implemented, use `PlaceholderScreen(name)` from `core:ui/commonMain`. Do NOT define inline placeholder composables in feature modules.
|
||||
- **Theme Picker:** Use `ThemePickerDialog` and `ThemeOption` from `feature:settings/commonMain`. Do NOT duplicate the theme dialog or enum in platform-specific source sets.
|
||||
- **Adaptive Layouts:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support the 2026 Desktop Experience breakpoints. Prioritize **higher information density** and mouse-precision interactions for Desktop and External Display (Android 16 QPR3) targets. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) using Navigation 3 Scenes and `ThreePaneScaffold` for widths ≥ 1200dp.
|
||||
- **Adaptive Layouts:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support the 2026 Desktop Experience breakpoints. Prioritize **higher information density** and mouse-precision interactions for Desktop and External Display (Android 16 QPR3) targets. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) using Navigation 3 Scenes, `extraPane()`, and draggable dividers (`VerticalDragHandle` + `paneExpansionState`) for widths ≥ 1200dp.
|
||||
- **Platform/Flavor UI:** Inject platform-specific behavior (e.g., map providers) via `CompositionLocal` from `app`.
|
||||
|
||||
### B. Logic & Data Layer
|
||||
|
|
@ -75,8 +75,12 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- `java.util.concurrent.locks.*` → `kotlinx.coroutines.sync.Mutex`.
|
||||
- `java.io.*` → Okio (`BufferedSource`/`BufferedSink`).
|
||||
- `kotlinx.coroutines.Dispatchers.IO` → `org.meshtastic.core.common.util.ioDispatcher` (expect/actual).
|
||||
- **Shared helpers over duplicated lambdas:** When `androidMain` and `jvmMain` contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract it to a function in `commonMain`. Examples: `formatLogsTo()` in `feature:settings`, `handleNodeAction()` in `feature:node`, `findNodeByNameSuffix()` in `feature:connections`.
|
||||
- **Shared helpers over duplicated lambdas:** When `androidMain` and `jvmMain` contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract it to a function in `commonMain`. Examples: `formatLogsTo()` in `feature:settings`, `handleNodeAction()` in `feature:node`, `findNodeByNameSuffix()` in `feature:connections`, `MeshtasticAppShell` in `core:ui/commonMain`, and `BaseRadioTransportFactory` in `core:network/commonMain`.
|
||||
- **KMP file naming:** In KMP modules, `commonMain` and platform source sets (`androidMain`, `jvmMain`) share the same package namespace. If both contain a file with the same name (e.g., `LogExporter.kt`), the Kotlin/JVM compiler will produce a duplicate class error. Use distinct filenames: keep the `expect` declaration in `LogExporter.kt` and put shared helpers in a separate file like `LogFormatter.kt`.
|
||||
- **`jvmAndroidMain` source set:** Modules that share JVM-specific code between Android and Desktop apply the `meshtastic.kmp.jvm.android` convention plugin. This creates a `jvmAndroidMain` source set via Kotlin's hierarchy template API. Used in `core:common`, `core:model`, `core:data`, `core:network`, and `core:ui`.
|
||||
- **Hierarchy template first:** Prefer Kotlin's default hierarchy template and convention plugins over manual `dependsOn(...)` graphs. Manual source-set wiring should be reserved for cases the template cannot model.
|
||||
- **`expect`/`actual` restraint:** Prefer interfaces + DI for platform capabilities; use `expect`/`actual` for small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.
|
||||
- **Feature navigation graphs:** Feature modules export Navigation 3 graph functions as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>)`). Host shells (`app`, `desktop`) assemble these into a single `entryProvider<NavKey>` block. Do NOT define navigation graphs in platform-specific source sets.
|
||||
- **Concurrency:** Use Kotlin Coroutines and Flow.
|
||||
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. Both `app` and `desktop` use `MeshtasticNavDisplay` from `core:ui/commonMain`, which configures `ViewModelStoreNavEntryDecorator` + `SaveableStateHolderNavEntryDecorator`. ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are scoped to the entry's backstack lifetime and cleared on pop.
|
||||
|
|
@ -140,8 +144,8 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes.
|
||||
- **Runner strategy (three tiers):**
|
||||
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). These run only shell scripts or GitHub API calls and benefit from ARM runners' shorter queue times.
|
||||
- **`ubuntu-24.04`** — Gradle-heavy jobs (CI host-check, android-check, release builds, Dokka, CodeQL, publish, dependency-submission). Pinned for reproducibility; avoid `ubuntu-latest` to prevent breakage when GitHub rolls the alias forward.
|
||||
- **Desktop release matrix** — `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]` for cross-platform native packaging (DMG, MSI, deb/rpm/AppImage for x64 and ARM).
|
||||
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `host-check`/`android-check`, release builds, Dokka, CodeQL, publish, dependency-submission). Pin where possible for reproducibility.
|
||||
- **Desktop runners:** Reusable CI uses `ubuntu-latest` for the `build-desktop` job in `.github/workflows/reusable-check.yml`; release packaging matrix remains `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]`.
|
||||
- **CI JVM tuning:** `gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI workflows override via `GRADLE_OPTS` env var to fit the 7GB RAM budget of standard runners: `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon, VFS watching disabled, workers capped at 4.
|
||||
- **KMP Smoke Compile:** Use `./gradlew kmpSmokeCompile` instead of listing individual module compile tasks. The `kmpSmokeCompile` lifecycle task (registered in `RootConventionPlugin`) auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks.
|
||||
- **`mavenLocal()` gated:** The `mavenLocal()` repository is disabled by default to prevent CI cache poisoning. For local JitPack testing, pass `-PuseMavenLocal` to Gradle.
|
||||
|
|
|
|||
|
|
@ -78,10 +78,13 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Shared helpers over duplicated lambdas:** When `androidMain` and `jvmMain` contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract it to a function in `commonMain`. Examples: `formatLogsTo()` in `feature:settings`, `handleNodeAction()` in `feature:node`, `findNodeByNameSuffix()` in `feature:connections`, `MeshtasticAppShell` in `core:ui/commonMain`, and `BaseRadioTransportFactory` in `core:network/commonMain`.
|
||||
- **KMP file naming:** In KMP modules, `commonMain` and platform source sets (`androidMain`, `jvmMain`) share the same package namespace. If both contain a file with the same name (e.g., `LogExporter.kt`), the Kotlin/JVM compiler will produce a duplicate class error. Use distinct filenames: keep the `expect` declaration in `LogExporter.kt` and put shared helpers in a separate file like `LogFormatter.kt`.
|
||||
- **`jvmAndroidMain` source set:** Modules that share JVM-specific code between Android and Desktop apply the `meshtastic.kmp.jvm.android` convention plugin. This creates a `jvmAndroidMain` source set via Kotlin's hierarchy template API. Used in `core:common`, `core:model`, `core:data`, `core:network`, and `core:ui`.
|
||||
- **Hierarchy template first:** Prefer Kotlin's default hierarchy template and convention plugins over manual `dependsOn(...)` graphs. Manual source-set wiring should be reserved for cases the template cannot model.
|
||||
- **`expect`/`actual` restraint:** Prefer interfaces + DI for platform capabilities; use `expect`/`actual` for small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.
|
||||
- **Feature navigation graphs:** Feature modules export Navigation 3 graph functions as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>)`). Host shells (`app`, `desktop`) assemble these into a single `entryProvider<NavKey>` block. Do NOT define navigation graphs in platform-specific source sets.
|
||||
- **Concurrency:** Use Kotlin Coroutines and Flow.
|
||||
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. Both `app` and `desktop` pass `ViewModelStoreNavEntryDecorator` to `NavDisplay`, so ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are scoped to the entry's backstack lifetime and cleared on pop.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. Both `app` and `desktop` use `MeshtasticNavDisplay` from `core:ui/commonMain`, which configures `ViewModelStoreNavEntryDecorator` + `SaveableStateHolderNavEntryDecorator`. ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are scoped to the entry's backstack lifetime and cleared on pop.
|
||||
- **Navigation host:** Use `MeshtasticNavDisplay` from `core:ui/commonMain` instead of calling `NavDisplay` directly. It provides entry-scoped ViewModel decoration, `DialogSceneStrategy` for dialog entries, and a shared 350 ms crossfade transition. Host modules (`app`, `desktop`) should NOT configure `entryDecorators`, `sceneStrategies`, or `transitionSpec` themselves.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Kable.
|
||||
- **Networking:** Pure **Ktor** — no OkHttp anywhere. Engines: `ktor-client-android` for Android, `ktor-client-java` for desktop/JVM. Use Ktor `Logging` plugin for HTTP debug logging (not OkHttp interceptors). `HttpClient` is provided via Koin in `app/di/NetworkModule` and `core:network/di/CoreNetworkAndroidModule`.
|
||||
- **Image Loading (Coil):** Use `coil-network-ktor3` with `KtorNetworkFetcherFactory` on **all** platforms. `ImageLoader` is configured in host modules only (`app` via Koin `@Single`, `desktop` via `setSingletonImageLoaderFactory`). Feature modules depend only on `libs.coil` (coil-compose) for `AsyncImage` — never add `coil-network-*` or `coil-svg` to feature modules.
|
||||
|
|
@ -141,8 +144,8 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes.
|
||||
- **Runner strategy (three tiers):**
|
||||
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). These run only shell scripts or GitHub API calls and benefit from ARM runners' shorter queue times.
|
||||
- **`ubuntu-24.04`** — Gradle-heavy jobs (CI host-check, android-check, release builds, Dokka, CodeQL, publish, dependency-submission). Pinned for reproducibility; avoid `ubuntu-latest` to prevent breakage when GitHub rolls the alias forward.
|
||||
- **Desktop release matrix** — `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]` for cross-platform native packaging (DMG, MSI, deb/rpm/AppImage for x64 and ARM).
|
||||
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `host-check`/`android-check`, release builds, Dokka, CodeQL, publish, dependency-submission). Pin where possible for reproducibility.
|
||||
- **Desktop runners:** Reusable CI uses `ubuntu-latest` for the `build-desktop` job in `.github/workflows/reusable-check.yml`; release packaging matrix remains `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]`.
|
||||
- **CI JVM tuning:** `gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI workflows override via `GRADLE_OPTS` env var to fit the 7GB RAM budget of standard runners: `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon, VFS watching disabled, workers capped at 4.
|
||||
- **KMP Smoke Compile:** Use `./gradlew kmpSmokeCompile` instead of listing individual module compile tasks. The `kmpSmokeCompile` lifecycle task (registered in `RootConventionPlugin`) auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks.
|
||||
- **`mavenLocal()` gated:** The `mavenLocal()` repository is disabled by default to prevent CI cache poisoning. For local JitPack testing, pass `-PuseMavenLocal` to Gradle.
|
||||
|
|
|
|||
19
GEMINI.md
19
GEMINI.md
|
|
@ -28,7 +28,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
| Directory | Description |
|
||||
| :--- | :--- |
|
||||
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
|
||||
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). |
|
||||
| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`). |
|
||||
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
|
||||
| `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. |
|
||||
| `core/model` | Domain models and common data structures. |
|
||||
|
|
@ -41,7 +41,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
| `core:data` | Core manager implementations and data orchestration. |
|
||||
| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioInterface`). |
|
||||
| `core:di` | Common DI qualifiers and dispatchers. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3, `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` for backstack persistence. |
|
||||
| `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. |
|
||||
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
|
||||
| `core:api` | Public AIDL/API integration module for external clients. |
|
||||
|
|
@ -64,7 +64,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Alerts:** Use `AlertHost(alertManager)` from `core:ui/commonMain` in each platform host shell (`Main.kt`, `DesktopMainScreen.kt`). For global responses like traceroute and firmware validation, use the specialized common handlers: `TracerouteAlertHandler(uiViewModel)` and `FirmwareVersionCheck(uiViewModel)`. Do NOT duplicate inline alert-rendering logic or trigger alerts directly during composition. For shared QR/contact dialogs, use the `SharedDialogs(uiViewModel)` composable.
|
||||
- **Placeholders:** For desktop/JVM features not yet implemented, use `PlaceholderScreen(name)` from `core:ui/commonMain`. Do NOT define inline placeholder composables in feature modules.
|
||||
- **Theme Picker:** Use `ThemePickerDialog` and `ThemeOption` from `feature:settings/commonMain`. Do NOT duplicate the theme dialog or enum in platform-specific source sets.
|
||||
- **Adaptive Layouts:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support the 2026 Desktop Experience breakpoints. Prioritize **higher information density** and mouse-precision interactions for Desktop and External Display (Android 16 QPR3) targets. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) using Navigation 3 Scenes and `ThreePaneScaffold` for widths ≥ 1200dp.
|
||||
- **Adaptive Layouts:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support the 2026 Desktop Experience breakpoints. Prioritize **higher information density** and mouse-precision interactions for Desktop and External Display (Android 16 QPR3) targets. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) using Navigation 3 Scenes, `extraPane()`, and draggable dividers (`VerticalDragHandle` + `paneExpansionState`) for widths ≥ 1200dp.
|
||||
- **Platform/Flavor UI:** Inject platform-specific behavior (e.g., map providers) via `CompositionLocal` from `app`.
|
||||
|
||||
### B. Logic & Data Layer
|
||||
|
|
@ -77,10 +77,17 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- `kotlinx.coroutines.Dispatchers.IO` → `org.meshtastic.core.common.util.ioDispatcher` (expect/actual).
|
||||
- **Shared helpers over duplicated lambdas:** When `androidMain` and `jvmMain` contain identical pure-Kotlin logic (formatting, action dispatch, validation), extract it to a function in `commonMain`. Examples: `formatLogsTo()` in `feature:settings`, `handleNodeAction()` in `feature:node`, `findNodeByNameSuffix()` in `feature:connections`, `MeshtasticAppShell` in `core:ui/commonMain`, and `BaseRadioTransportFactory` in `core:network/commonMain`.
|
||||
- **KMP file naming:** In KMP modules, `commonMain` and platform source sets (`androidMain`, `jvmMain`) share the same package namespace. If both contain a file with the same name (e.g., `LogExporter.kt`), the Kotlin/JVM compiler will produce a duplicate class error. Use distinct filenames: keep the `expect` declaration in `LogExporter.kt` and put shared helpers in a separate file like `LogFormatter.kt`.
|
||||
- **`jvmAndroidMain` source set:** Modules that share JVM-specific code between Android and Desktop apply the `meshtastic.kmp.jvm.android` convention plugin. This creates a `jvmAndroidMain` source set via Kotlin's hierarchy template API. Used in `core:common`, `core:model`, `core:data`, `core:network`, and `core:ui`.
|
||||
- **Hierarchy template first:** Prefer Kotlin's default hierarchy template and convention plugins over manual `dependsOn(...)` graphs. Manual source-set wiring should be reserved for cases the template cannot model.
|
||||
- **`expect`/`actual` restraint:** Prefer interfaces + DI for platform capabilities; use `expect`/`actual` for small unavoidable platform primitives. Avoid broad expect/actual class hierarchies when an interface-based boundary is sufficient.
|
||||
- **Feature navigation graphs:** Feature modules export Navigation 3 graph functions as extension functions on `EntryProviderScope<NavKey>` in `commonMain` (e.g., `fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>)`). Host shells (`app`, `desktop`) assemble these into a single `entryProvider<NavKey>` block. Do NOT define navigation graphs in platform-specific source sets.
|
||||
- **Concurrency:** Use Kotlin Coroutines and Flow.
|
||||
- **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (`koin-plugin` in version catalog). The `koin-annotations` library version is unified with `koin-core` (both use `version.ref = "koin"`). The `KoinConventionPlugin` uses the typed `KoinGradleExtension` to configure the K2 plugin (e.g., `compileSafety.set(false)`). Keep root graph assembly in `app`.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. Both `app` and `desktop` pass `ViewModelStoreNavEntryDecorator` to `NavDisplay`, so ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are scoped to the entry's backstack lifetime and cleared on pop.
|
||||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. Both `app` and `desktop` use `MeshtasticNavDisplay` from `core:ui/commonMain`, which configures `ViewModelStoreNavEntryDecorator` + `SaveableStateHolderNavEntryDecorator`. ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are scoped to the entry's backstack lifetime and cleared on pop.
|
||||
- **Navigation host:** Use `MeshtasticNavDisplay` from `core:ui/commonMain` instead of calling `NavDisplay` directly. It provides entry-scoped ViewModel decoration, `DialogSceneStrategy` for dialog entries, and a shared 350 ms crossfade transition. Host modules (`app`, `desktop`) should NOT configure `entryDecorators`, `sceneStrategies`, or `transitionSpec` themselves.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Kable.
|
||||
- **Networking:** Pure **Ktor** — no OkHttp anywhere. Engines: `ktor-client-android` for Android, `ktor-client-java` for desktop/JVM. Use Ktor `Logging` plugin for HTTP debug logging (not OkHttp interceptors). `HttpClient` is provided via Koin in `app/di/NetworkModule` and `core:network/di/CoreNetworkAndroidModule`.
|
||||
- **Image Loading (Coil):** Use `coil-network-ktor3` with `KtorNetworkFetcherFactory` on **all** platforms. `ImageLoader` is configured in host modules only (`app` via Koin `@Single`, `desktop` via `setSingletonImageLoaderFactory`). Feature modules depend only on `libs.coil` (coil-compose) for `AsyncImage` — never add `coil-network-*` or `coil-svg` to feature modules.
|
||||
- **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available.
|
||||
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
|
||||
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
|
||||
|
|
@ -137,8 +144,8 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes.
|
||||
- **Runner strategy (three tiers):**
|
||||
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). These run only shell scripts or GitHub API calls and benefit from ARM runners' shorter queue times.
|
||||
- **`ubuntu-24.04`** — Gradle-heavy jobs (CI host-check, android-check, release builds, Dokka, CodeQL, publish, dependency-submission). Pinned for reproducibility; avoid `ubuntu-latest` to prevent breakage when GitHub rolls the alias forward.
|
||||
- **Desktop release matrix** — `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]` for cross-platform native packaging (DMG, MSI, deb/rpm/AppImage for x64 and ARM).
|
||||
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `host-check`/`android-check`, release builds, Dokka, CodeQL, publish, dependency-submission). Pin where possible for reproducibility.
|
||||
- **Desktop runners:** Reusable CI uses `ubuntu-latest` for the `build-desktop` job in `.github/workflows/reusable-check.yml`; release packaging matrix remains `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]`.
|
||||
- **CI JVM tuning:** `gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI workflows override via `GRADLE_OPTS` env var to fit the 7GB RAM budget of standard runners: `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon, VFS watching disabled, workers capped at 4.
|
||||
- **KMP Smoke Compile:** Use `./gradlew kmpSmokeCompile` instead of listing individual module compile tasks. The `kmpSmokeCompile` lifecycle task (registered in `RootConventionPlugin`) auto-discovers all KMP modules and depends on their `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` tasks.
|
||||
- **`mavenLocal()` gated:** The `mavenLocal()` repository is disabled by default to prevent CI cache poisoning. For local JitPack testing, pass `-PuseMavenLocal` to Gradle.
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ You can generate the documentation locally to preview your changes.
|
|||
|
||||
1. **Run the Dokka task:**
|
||||
```bash
|
||||
./gradlew :app:dokkaHtml
|
||||
./gradlew dokkaGeneratePublicationHtml
|
||||
```
|
||||
2. **View the output:**
|
||||
The generated HTML files will be located in the `app/build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation.
|
||||
The generated HTML files will be located in the `build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ The `:app` module is the entry point for the Meshtastic Android application. It
|
|||
## Key Components
|
||||
|
||||
### 1. `MainActivity` & `Main.kt`
|
||||
The single Activity of the application. It hosts the `NavHost` and manages the root UI structure (Navigation Bar, Rail, etc.).
|
||||
The single Activity of the application. It hosts the shared `MeshtasticNavDisplay` navigation shell and manages the root UI structure (Navigation Bar, Rail, etc.).
|
||||
|
||||
### 2. `MeshService`
|
||||
The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background.
|
||||
|
|
|
|||
|
|
@ -189,12 +189,10 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
|||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlin.uuid.ExperimentalUuidApi",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xcontext-parameters",
|
||||
"-Xannotation-default-target=param-property",
|
||||
"-Xskip-prerelease-check",
|
||||
"-Xjvm-default=all",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -215,12 +213,11 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
|||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlin.uuid.ExperimentalUuidApi",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xcontext-parameters",
|
||||
"-Xannotation-default-target=param-property",
|
||||
"-Xskip-prerelease-check",
|
||||
"-Xjvm-default=all",
|
||||
"-jvm-default=no-compatibility",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ actual fun getInMemoryDatabaseBuilder(): RoomDatabase.Builder<MeshtasticDatabase
|
|||
actual fun getDatabaseDirectory(): Path = documentDirectory().toPath()
|
||||
|
||||
/** Deletes the database and its Room-associated files on iOS. */
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun deleteDatabase(dbName: String) {
|
||||
val dir = documentDirectory()
|
||||
NSFileManager.defaultManager.removeItemAtPath(dir + "/$dbName.db", null)
|
||||
|
|
@ -83,6 +84,7 @@ private object PreferencesSerializer : OkioSerializer<Preferences> {
|
|||
}
|
||||
|
||||
/** Creates an iOS DataStore for database preferences. */
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun createDatabaseDataStore(name: String): DataStore<Preferences> {
|
||||
val dir = documentDirectory() + "/datastore"
|
||||
NSFileManager.defaultManager.createDirectoryAtPath(dir, true, null, null)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,35 @@
|
|||
# `:core:navigation`
|
||||
|
||||
## Overview
|
||||
The `:core:navigation` module defines the type-safe navigation structure for the entire application using Kotlin Serialization and the Jetpack Navigation library.
|
||||
The `:core:navigation` module defines the type-safe Navigation 3 route model for Android and Desktop using Kotlin Serialization.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. `Routes.kt`
|
||||
Contains all the serializable classes and objects that represent destinations in the app.
|
||||
Contains serializable `NavKey` route classes/objects used by shared feature graphs.
|
||||
|
||||
### 2. `DeepLinkRouter.kt`
|
||||
Parses Meshtastic deep-link URIs and synthesizes a typed backstack (for example `/nodes/1234/device-metrics`).
|
||||
|
||||
### 3. `NavigationConfig.kt`
|
||||
Defines `MeshtasticNavSavedStateConfig` so Navigation 3 backstacks can be persisted/restored safely.
|
||||
|
||||
## Features
|
||||
- **Type-Safety**: Leverages Kotlin Serialization to pass data between screens without fragile Bundle keys.
|
||||
- **Centralized Definition**: All routes are defined in one place to prevent circular dependencies between feature modules.
|
||||
- **Type-Safety**: Uses serializable `NavKey` routes instead of ad-hoc string routes.
|
||||
- **Deep-link synthesis**: Converts incoming URIs into typed backstacks via `DeepLinkRouter`.
|
||||
- **Centralized definition**: Routes and saved-state serializers are declared in one place to avoid feature-module cycles.
|
||||
|
||||
## Usage
|
||||
Feature modules depend on this module to define their entry points and navigate to other features.
|
||||
Feature modules depend on this module to define their entry points and navigate via `NavBackStack<NavKey>`.
|
||||
|
||||
```kotlin
|
||||
import org.meshtastic.core.navigation.MessagingRoutes
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
|
||||
navController.navigate(MessagingRoutes.Chat(nodeId = 12345))
|
||||
fun openNodeDetail(backStack: NavBackStack<NavKey>, destNum: Int) {
|
||||
backStack.add(NodesRoutes.NodeDetail(destNum))
|
||||
}
|
||||
```
|
||||
|
||||
## Module dependency graph
|
||||
|
|
|
|||
|
|
@ -276,6 +276,21 @@
|
|||
<string name="match_all">Match All | Any</string>
|
||||
<string name="debug_clear_logs_confirm">This will remove all log packets and database entries from your device - It is a full reset, and is permanent.</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="search_emoji">Search emoji...</string>
|
||||
<string name="more_reactions">More reactions</string>
|
||||
<string name="channel_label">Channel</string>
|
||||
<string name="a11y_label_value">%1$s: %2$s</string>
|
||||
<string name="a11y_message_from">Message from %1$s: %2$s</string>
|
||||
<string name="preview_header">Header</string>
|
||||
<string name="preview_item">Item %1$d</string>
|
||||
<string name="preview_footer">Footer</string>
|
||||
<string name="preview_pill">Pill</string>
|
||||
<string name="preview_dot">Dot</string>
|
||||
<string name="preview_text">Text</string>
|
||||
<string name="preview_gauge">Gauge</string>
|
||||
<string name="preview_gradient">Gradient</string>
|
||||
<string name="preview_custom_composable_line_one">This is a custom composable</string>
|
||||
<string name="preview_custom_composable_line_two">With multiple lines and styles</string>
|
||||
<string name="message_delivery_status">Message delivery status</string>
|
||||
<string name="new_messages_below">New messages below</string>
|
||||
<string name="meshtastic_messages_notifications">Direct message notifications</string>
|
||||
|
|
@ -783,6 +798,7 @@
|
|||
<string name="timestamp">Timestamp</string>
|
||||
<string name="heading">Heading</string>
|
||||
<string name="speed">Speed</string>
|
||||
<string name="speed_kmh">%1$d Km/h</string>
|
||||
<string name="sats">Sats</string>
|
||||
<string name="alt">Alt</string>
|
||||
<string name="freq">Freq</string>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ kotlin {
|
|||
android {
|
||||
namespace = "org.meshtastic.core.ui"
|
||||
androidResources.enable = false
|
||||
withHostTest { isIncludeAndroidResources = true }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
@ -75,6 +76,6 @@ kotlin {
|
|||
implementation(libs.kotest.property)
|
||||
}
|
||||
|
||||
androidUnitTest.dependencies { implementation(libs.androidx.test.runner) }
|
||||
val androidHostTest by getting { dependencies { implementation(libs.androidx.test.runner) } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -54,6 +53,11 @@ import org.meshtastic.core.resources.Res
|
|||
import org.meshtastic.core.resources.air_quality_icon
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.resources.indoor_air_quality_iaq
|
||||
import org.meshtastic.core.resources.preview_dot
|
||||
import org.meshtastic.core.resources.preview_gauge
|
||||
import org.meshtastic.core.resources.preview_gradient
|
||||
import org.meshtastic.core.resources.preview_pill
|
||||
import org.meshtastic.core.resources.preview_text
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.ThumbUp
|
||||
import org.meshtastic.core.ui.icon.Warning
|
||||
|
|
@ -112,8 +116,6 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
|
|||
}
|
||||
var isLegendOpen by remember { mutableStateOf(false) }
|
||||
val iaqEnum = getIaq(iaq)
|
||||
val gradient = Brush.linearGradient(colors = Iaq.entries.map { it.color })
|
||||
|
||||
if (iaqEnum != null) {
|
||||
Column {
|
||||
when (displayMode) {
|
||||
|
|
@ -166,7 +168,7 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
|
|||
strokeWidth = 8.dp,
|
||||
color = iaqEnum.color,
|
||||
)
|
||||
Text(text = "${iaqEnum.description}")
|
||||
Text(text = iaqEnum.description)
|
||||
}
|
||||
|
||||
IaqDisplayMode.Gradient -> {
|
||||
|
|
@ -230,7 +232,7 @@ private fun IndoorAirQualityPreview() {
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("Pill", style = MaterialTheme.typography.titleLarge)
|
||||
Text(stringResource(Res.string.preview_pill), style = MaterialTheme.typography.titleLarge)
|
||||
Row {
|
||||
IndoorAirQuality(iaq = 6)
|
||||
IndoorAirQuality(iaq = 51)
|
||||
|
|
@ -244,7 +246,7 @@ private fun IndoorAirQualityPreview() {
|
|||
IndoorAirQuality(iaq = 351)
|
||||
}
|
||||
|
||||
Text("Dot", style = MaterialTheme.typography.titleLarge)
|
||||
Text(stringResource(Res.string.preview_dot), style = MaterialTheme.typography.titleLarge)
|
||||
Row {
|
||||
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Dot)
|
||||
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Dot)
|
||||
|
|
@ -254,7 +256,7 @@ private fun IndoorAirQualityPreview() {
|
|||
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Dot)
|
||||
}
|
||||
|
||||
Text("Text", style = MaterialTheme.typography.titleLarge)
|
||||
Text(stringResource(Res.string.preview_text), style = MaterialTheme.typography.titleLarge)
|
||||
Row {
|
||||
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Text)
|
||||
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Text)
|
||||
|
|
@ -266,7 +268,7 @@ private fun IndoorAirQualityPreview() {
|
|||
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Text)
|
||||
}
|
||||
|
||||
Text("Gauge", style = MaterialTheme.typography.titleLarge)
|
||||
Text(stringResource(Res.string.preview_gauge), style = MaterialTheme.typography.titleLarge)
|
||||
Row {
|
||||
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gauge)
|
||||
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gauge)
|
||||
|
|
@ -284,7 +286,7 @@ private fun IndoorAirQualityPreview() {
|
|||
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gauge)
|
||||
}
|
||||
|
||||
Text("Gradient", style = MaterialTheme.typography.titleLarge)
|
||||
Text(stringResource(Res.string.preview_gradient), style = MaterialTheme.typography.titleLarge)
|
||||
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gradient)
|
||||
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gradient)
|
||||
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gradient)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ import androidx.compose.ui.zIndex
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.preview_footer
|
||||
import org.meshtastic.core.resources.preview_header
|
||||
import org.meshtastic.core.resources.preview_item
|
||||
|
||||
// Derived in part from:
|
||||
// https://github.com/androidx/androidx/blob/c92ad2941368202b2d78b8d14c71bf81e9525944/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
|
||||
|
|
@ -80,15 +85,15 @@ fun LazyColumnDragAndDropDemo() {
|
|||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
item { Text("Header", Modifier.fillMaxWidth().padding(20.dp)) }
|
||||
item { Text(stringResource(Res.string.preview_header), Modifier.fillMaxWidth().padding(20.dp)) }
|
||||
|
||||
itemsIndexed(list, key = { _, item -> item }) { index, item ->
|
||||
DraggableItem(dragDropState, index + 1) { isDragging ->
|
||||
Card { Text("Item $item", Modifier.fillMaxWidth().padding(20.dp)) }
|
||||
DraggableItem(dragDropState, index + 1) {
|
||||
Card { Text(stringResource(Res.string.preview_item, item), Modifier.fillMaxWidth().padding(20.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
item { Text("Footer", Modifier.fillMaxWidth().padding(20.dp)) }
|
||||
item { Text(stringResource(Res.string.preview_footer), Modifier.fillMaxWidth().padding(20.dp)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import org.meshtastic.core.ui.viewmodel.UIViewModel
|
|||
* - System-wide alerts and snackbar hosts
|
||||
* - Deep link navigation interception logic
|
||||
*
|
||||
* Platform hosts (Main.kt) should invoke this at the root of their theme before rendering the main NavDisplay.
|
||||
* Platform hosts should invoke this near the root before rendering `MeshtasticNavDisplay`.
|
||||
*/
|
||||
@Composable
|
||||
fun MeshtasticCommonAppSetup(
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ fun RegularPreference(
|
|||
Box {
|
||||
Icon(
|
||||
imageVector = trailingIcon,
|
||||
contentDescription = "trailingIcon",
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = 8.dp).wrapContentWidth(Alignment.End),
|
||||
tint = color,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.baro_pressure
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.hardware_model
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.node_id
|
||||
|
|
@ -225,7 +226,7 @@ fun HardwareInfo(
|
|||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = MeshtasticIcons.HardwareModel,
|
||||
contentDescription = "Hardware Model",
|
||||
contentDescription = stringResource(Res.string.hardware_model),
|
||||
text = hwModel,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
contentColor = contentColor,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.search_emoji
|
||||
import org.meshtastic.core.ui.component.BottomSheetDialog
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -207,7 +211,7 @@ private fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
|
|||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Search emoji\u2026",
|
||||
text = stringResource(Res.string.search_emoji),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
@ -221,7 +225,7 @@ private fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
|
|||
IconButton(onClick = { onQueryChange("") }) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = "Clear",
|
||||
contentDescription = stringResource(Res.string.clear),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.preview_custom_composable_line_one
|
||||
import org.meshtastic.core.resources.preview_custom_composable_line_two
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
|
|
@ -120,8 +124,8 @@ fun PreviewComposableAlert() {
|
|||
title = "Custom Content",
|
||||
composableMessage = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("This is a custom composable")
|
||||
Text("With multiple lines and styles")
|
||||
Text(stringResource(Res.string.preview_custom_composable_line_one))
|
||||
Text(stringResource(Res.string.preview_custom_composable_line_two))
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ kotlin {
|
|||
}
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
freeCompilerArgs.add("-Xjvm-default=all")
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,12 +48,14 @@ import androidx.compose.ui.window.rememberTrayState
|
|||
import androidx.compose.ui.window.rememberWindowState
|
||||
import co.touchlab.kermit.Logger
|
||||
import coil3.ImageLoader
|
||||
import coil3.annotation.ExperimentalCoilApi
|
||||
import coil3.compose.setSingletonImageLoaderFactory
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
||||
import coil3.request.crossfade
|
||||
import coil3.svg.SvgDecoder
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okio.Path.Companion.toPath
|
||||
import org.jetbrains.skia.Image
|
||||
|
|
@ -90,12 +92,14 @@ private fun classpathPainterResource(path: String): Painter {
|
|||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
fun main(args: Array<String>) = application(exitProcessOnExit = false) {
|
||||
Logger.i { "Meshtastic Desktop — Starting" }
|
||||
|
||||
val koinApp = remember { startKoin { modules(desktopPlatformModule(), desktopModule()) } }
|
||||
val systemLocale = remember { Locale.getDefault() }
|
||||
val uiViewModel = remember { koinApp.koin.get<UIViewModel>() }
|
||||
val httpClient = remember { koinApp.koin.get<HttpClient>() }
|
||||
|
||||
LaunchedEffect(args) {
|
||||
args.forEach { arg ->
|
||||
|
|
@ -247,8 +251,10 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
|
|||
val cacheDir = System.getProperty("user.home") + "/.meshtastic/image_cache_v3"
|
||||
ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(KtorNetworkFetcherFactory())
|
||||
add(SvgDecoder.Factory(renderToBitmap = false))
|
||||
add(KtorNetworkFetcherFactory(httpClient = httpClient))
|
||||
// Render SVGs to a bitmap on Desktop to avoid Skiko vector rendering artifacts
|
||||
// that show up as solid/black hardware images.
|
||||
add(SvgDecoder.Factory(renderToBitmap = true))
|
||||
}
|
||||
.memoryCache { MemoryCache.Builder().maxSizeBytes(MEMORY_CACHE_MAX_BYTES).build() }
|
||||
.diskCache {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ Use `AGENTS.md` as the source of truth for architecture boundaries and required
|
|||
When checking upstream docs/examples, match these repository-pinned versions from `gradle/libs.versions.toml`:
|
||||
|
||||
- Kotlin: `2.3.20`
|
||||
- Koin: `4.2.0` (`koin-annotations` `2.1.0`, compiler plugin `0.4.1`)
|
||||
- JetBrains Navigation 3: `1.1.0-alpha04` (`org.jetbrains.androidx.navigation3`)
|
||||
- JetBrains Lifecycle (multiplatform): `2.10.0-beta01` (`org.jetbrains.androidx.lifecycle`)
|
||||
- Koin: `4.2.0` (`koin-annotations` `4.2.0`, compiler plugin `0.4.1`)
|
||||
- JetBrains Navigation 3: `1.1.0-beta01` (`org.jetbrains.androidx.navigation3`)
|
||||
- JetBrains Lifecycle (multiplatform): `2.11.0-alpha02` (`org.jetbrains.androidx.lifecycle`)
|
||||
- AndroidX Lifecycle (Android-only): `2.10.0` (`androidx.lifecycle`)
|
||||
- Kotlin Coroutines: `1.10.2`
|
||||
- Compose Multiplatform: `1.11.0-alpha04`
|
||||
- Compose Multiplatform: `1.11.0-beta01`
|
||||
- JetBrains Material 3 Adaptive: `1.3.0-alpha06` (`org.jetbrains.compose.material3.adaptive`)
|
||||
|
||||
Prefer versioned docs pages that match those versions (for example, Koin `4.2` docs rather than older `4.0/4.1` pages).
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ This document captures discoverable patterns that are already used in the reposi
|
|||
## 3) Navigation conventions (Navigation 3)
|
||||
|
||||
- Use Navigation 3 types (`NavKey`, `NavBackStack`, entry providers) instead of legacy controller-first patterns.
|
||||
- Example graph using `EntryProviderScope<NavKey>` and `backStack.add/removeLastOrNull`: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`.
|
||||
- Example feature flow using `rememberNavBackStack` and `NavDisplay<NavKey>`: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`.
|
||||
- Example graph using `EntryProviderScope<NavKey>` and `backStack.add/removeLastOrNull`: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`.
|
||||
- Hosts should render navigation via `MeshtasticNavDisplay` from `core:ui/commonMain` (not raw `NavDisplay`) so entry decorators, scene strategies, and transitions stay consistent.
|
||||
- Host examples: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`, `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`.
|
||||
|
||||
## 4) UI and resources
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Version note: align guidance with repository-pinned versions in `gradle/libs.ver
|
|||
|
||||
- Typed routes: `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
|
||||
- Shared saved-state config: `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt`
|
||||
- App root backstack + `NavDisplay`: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
|
||||
- App root backstack + `MeshtasticNavDisplay`: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`
|
||||
- Shared graph entry provider pattern: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
|
||||
- Desktop Navigation 3 shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
|
||||
- Desktop nav graph assembly: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ Reference examples:
|
|||
1. Implement or extend base ViewModel logic in `feature/<name>/src/commonMain/...`.
|
||||
2. Keep shared class free of Android framework dependencies.
|
||||
3. Keep Android framework dependencies out of shared logic; if the module already uses Koin annotations in `commonMain`, keep patterns consistent and ensure app root inclusion.
|
||||
4. Update navigation entry points in `feature/*/src/androidMain/kotlin/org/meshtastic/feature/*/navigation/...` to resolve ViewModels with `koinViewModel()`.
|
||||
4. Update shared navigation entry points in `feature/*/src/commonMain/kotlin/org/meshtastic/feature/*/navigation/...` to resolve ViewModels with `koinViewModel()`.
|
||||
|
||||
Reference examples:
|
||||
- Shared base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
|
||||
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
|
||||
- Navigation usage: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
|
||||
- Navigation usage: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
|
||||
- Desktop navigation usage: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt`
|
||||
|
||||
## Playbook C: Add a new dependency or service binding
|
||||
|
|
@ -50,7 +50,8 @@ Reference examples:
|
|||
|
||||
Reference examples:
|
||||
- Shared graph wiring: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
|
||||
- Android specific content: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsMainScreen.kt`
|
||||
- Shared graph content: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
|
||||
- Android-specific content actual: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsMainScreen.kt`
|
||||
- Desktop specific content: `feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsMainScreen.kt`
|
||||
- Feature intro graph pattern: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt`
|
||||
- Desktop nav shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
|
||||
|
|
@ -77,7 +78,7 @@ Reference examples:
|
|||
4. Add `kotlinx-coroutines-swing` (JVM/Desktop) or the equivalent platform coroutines dispatcher module. Without it, `Dispatchers.Main` is unavailable and any code using `lifecycle.coroutineScope` will crash at runtime.
|
||||
5. Progressively replace stubs with real implementations (e.g., serial transport for desktop, CoreBluetooth for iOS).
|
||||
6. Add `<platform>()` target to feature modules as needed (all `core:*` modules already declare `jvm()`).
|
||||
7. Update CI JVM smoke compile step in `.github/workflows/reusable-check.yml` to include new modules.
|
||||
7. Ensure the new module applies the expected KMP convention plugin so root `kmpSmokeCompile` auto-discovers and validates it in CI.
|
||||
8. If `commonMain` code fails to compile for the new target, it's a KMP migration debt — fix the shared code, not the target.
|
||||
|
||||
Reference examples:
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ Notes:
|
|||
- `feature/commonMain logic` changes:
|
||||
- `spotlessCheck`, `detekt`, `test`, `assembleDebug`.
|
||||
- `navigation/DI wiring` changes (app graph, Koin module/wrapper changes):
|
||||
- `spotlessCheck`, `detekt`, `assembleDebug`, `test`, plus `testDebugUnitTest` if available locally.
|
||||
- If touching any KMP module, also run the relevant `:compileKotlinJvm` task. CI validates all 22 KMP modules + `desktop:test`.
|
||||
- `spotlessCheck`, `detekt`, `assembleDebug`, `test`, plus `testFdroidDebugUnitTest` and `testGoogleDebugUnitTest` when available locally.
|
||||
- If touching any KMP module, also run `kmpSmokeCompile`.
|
||||
- `worker/service/background` changes:
|
||||
- `spotlessCheck`, `detekt`, `assembleDebug`, `test`, and targeted tests around WorkManager/service behavior.
|
||||
- `BLE/networking/core repository` changes:
|
||||
|
|
@ -57,8 +57,8 @@ Current reusable check workflow includes:
|
|||
`app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug mesh_service_example:lintDebug`
|
||||
- Host tests plus coverage aggregation:
|
||||
`test koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug core:api:koverXmlReportDebug core:barcode:koverXmlReportFdroidDebug core:barcode:koverXmlReportGoogleDebug mesh_service_example:koverXmlReportDebug desktop:koverXmlReport`
|
||||
- JVM smoke compile for all KMP JVM targets (all compile-only modules remain explicit):
|
||||
`:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:testing:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm`
|
||||
- KMP smoke compile lifecycle task (auto-discovers KMP modules and runs JVM + iOS simulator compile checks):
|
||||
`kmpSmokeCompile`
|
||||
- Android build tasks:
|
||||
`app:assembleFdroidDebug app:assembleGoogleDebug mesh_service_example:assembleDebug`
|
||||
- Instrumented tests (when emulator tests are enabled):
|
||||
|
|
|
|||
|
|
@ -30,46 +30,36 @@
|
|||
|
||||
### 1. NavDisplay — Scene Architecture (available since `1.1.0-alpha04`, stable in `beta01`)
|
||||
|
||||
**Available APIs we're NOT using:**
|
||||
**Remaining APIs we're NOT using broadly yet:**
|
||||
|
||||
| API | Purpose | Status in project |
|
||||
|---|---|---|
|
||||
| `sceneStrategies: List<SceneStrategy<T>>` | Allows NavDisplay to render multi-pane Scenes | ❌ Not used — defaulting to `SinglePaneSceneStrategy` |
|
||||
| `sceneStrategies: List<SceneStrategy<T>>` | Allows NavDisplay to render multi-pane Scenes | ⚠️ Partially used — `MeshtasticNavDisplay` applies dialog/list-detail/supporting/single strategies |
|
||||
| `SceneStrategy<T>` interface | Custom scene calculation from backstack entries | ❌ Not used |
|
||||
| `DialogSceneStrategy` | Renders `entry<T>(metadata = dialog())` entries as overlay Dialogs | ❌ Not used — dialogs handled manually |
|
||||
| `DialogSceneStrategy` | Renders `entry<T>(metadata = dialog())` entries as overlay Dialogs | ✅ Enabled in shared host wrapper |
|
||||
| `SceneDecoratorStrategy<T>` | Wraps/decorates scenes with additional UI | ❌ Not used |
|
||||
| `NavEntry.metadata` | Attaches typed metadata to entries (transitions, dialog hints, Scene classification) | ❌ Not used |
|
||||
| `NavDisplay.TransitionKey` / `PopTransitionKey` / `PredictivePopTransitionKey` | Per-entry custom transitions via metadata | ❌ Not used |
|
||||
| `transitionSpec` / `popTransitionSpec` / `predictivePopTransitionSpec` params | Default transition animations for NavDisplay | ❌ Not used — no transitions at all |
|
||||
| `transitionSpec` / `popTransitionSpec` / `predictivePopTransitionSpec` params | Default transition animations for NavDisplay | ⚠️ Partially used — shared forward/pop crossfade adopted; predictive-pop custom spec not yet used |
|
||||
| `sharedTransitionScope: SharedTransitionScope?` | Shared element transitions between scenes | ❌ Not used |
|
||||
| `entryDecorators: List<NavEntryDecorator<T>>` | Wraps entry content with additional behavior | ❌ Not used (defaulting to `SaveableStateHolderNavEntryDecorator`) |
|
||||
| `entryDecorators: List<NavEntryDecorator<T>>` | Wraps entry content with additional behavior | ✅ Used via `MeshtasticNavDisplay` (`SaveableStateHolder` + `ViewModelStore`) |
|
||||
|
||||
**APIs we ARE using correctly:**
|
||||
|
||||
| API | Usage |
|
||||
|---|---|
|
||||
| `NavDisplay(backStack, entryProvider, modifier)` | Both `app/Main.kt` and `desktop/DesktopMainScreen.kt` |
|
||||
| `MeshtasticNavDisplay(...)` wrapper around `NavDisplay` | Both `app/Main.kt` and `desktop/DesktopMainScreen.kt` |
|
||||
| `rememberNavBackStack(SavedStateConfiguration, startKey)` | Backstack persistence |
|
||||
| `entryProvider<NavKey> { entry<T> { ... } }` | All feature graph registrations |
|
||||
| `NavigationBackHandler` from `navigationevent-compose` | Used in `AdaptiveListDetailScaffold` |
|
||||
|
||||
### 2. ViewModel Scoping (`lifecycle-viewmodel-navigation3` `2.11.0-alpha02`)
|
||||
|
||||
**Key finding:** The `ViewModelStoreNavEntryDecorator` is available and provides automatic per-entry ViewModel scoping tied to backstack lifetime. The project declares this dependency in `desktop/build.gradle.kts` but does **not** pass it as an `entryDecorator` to `NavDisplay`.
|
||||
|
||||
Currently, `koinViewModel()` calls inside `entry<T>` blocks use the nearest `ViewModelStoreOwner` from the composition — which is the Activity/Window level. This means:
|
||||
- ViewModels are **not** automatically cleared when their entry is popped from the backstack.
|
||||
- The project works around this with manual `key = "metrics-$destNum"` parameter keying.
|
||||
|
||||
**Opportunity:** Adding `rememberViewModelStoreNavEntryDecorator()` to `NavDisplay.entryDecorators` would give each backstack entry its own `ViewModelStoreOwner`, so `koinViewModel()` calls would be automatically scoped to the entry's lifetime.
|
||||
**Current status:** Adopted. `MeshtasticNavDisplay` applies `rememberViewModelStoreNavEntryDecorator()` with `rememberSaveableStateHolderNavEntryDecorator()`, so `koinViewModel()` instances are entry-scoped and clear on pop.
|
||||
|
||||
### 3. Material 3 Adaptive — Nav3 Scene Integration
|
||||
|
||||
**Key finding:** The JetBrains `adaptive-navigation` artifact at `1.3.0-alpha06` does **NOT** include `MaterialListDetailSceneStrategy`. That API only exists in the Google AndroidX version (`androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha09+`).
|
||||
|
||||
This means the project **cannot** currently use the official M3 Adaptive Scene bridge through `NavDisplay(sceneStrategies = ...)`. The current approach of hosting `ListDetailPaneScaffold` inside `entry<T>` blocks (via `AdaptiveListDetailScaffold`) is the correct pattern for the JetBrains fork at this version.
|
||||
|
||||
**When to revisit:** Monitor the JetBrains adaptive fork for `MaterialListDetailSceneStrategy` inclusion. It will likely arrive when the JetBrains fork catches up to the AndroidX `1.3.0-alpha09+` feature set.
|
||||
**Current status:** Adopted for shared host-level strategies. `MeshtasticNavDisplay` uses adaptive Navigation 3 scene strategies (`rememberListDetailSceneStrategy`, `rememberSupportingPaneSceneStrategy`) with draggable pane expansion handles, while feature-level scaffold composition remains valid for route-specific layouts.
|
||||
|
||||
### 4. NavigationSuiteScaffold (`1.11.0-alpha05`)
|
||||
|
||||
|
|
@ -99,7 +89,7 @@ This means the project **cannot** currently use the official M3 Adaptive Scene b
|
|||
|
||||
**Status:** ✅ Adopted (2026-03-26). A new `MeshtasticNavDisplay` composable in `core:ui/commonMain` encapsulates the standard `NavDisplay` configuration:
|
||||
- Entry decorators: `rememberSaveableStateHolderNavEntryDecorator` + `rememberViewModelStoreNavEntryDecorator`
|
||||
- Scene strategies: `DialogSceneStrategy` + `SinglePaneSceneStrategy`
|
||||
- Scene strategies: `DialogSceneStrategy` + adaptive list-detail/supporting pane strategies + `SinglePaneSceneStrategy`
|
||||
- Transition specs: 350 ms crossfade (forward + pop)
|
||||
|
||||
Both `app/Main.kt` and `desktop/DesktopMainScreen.kt` now call `MeshtasticNavDisplay` instead of configuring `NavDisplay` directly. The `lifecycle-viewmodel-navigation3` dependency was moved from host modules to `core:ui`.
|
||||
|
|
@ -112,7 +102,7 @@ Individual entries can declare custom transitions via `entry<T>(metadata = NavDi
|
|||
|
||||
### Deferred: Scene-based multi-pane layout
|
||||
|
||||
The `MaterialListDetailSceneStrategy` is not available in the JetBrains adaptive fork at `1.3.0-alpha06`. The project's `AdaptiveListDetailScaffold` wrapper is the correct approach for now. Revisit when the JetBrains fork includes the Scene bridge, or consider writing a custom `SceneStrategy` that integrates with the existing `ListDetailPaneScaffold`.
|
||||
Additional route-level Scene metadata adoption is deferred. The project now applies shared adaptive scene strategies in `MeshtasticNavDisplay`, and feature-level `AdaptiveListDetailScaffold` remains valid for route-specific layouts. Revisit custom per-route `SceneStrategy` policies when multi-pane route classification needs expand.
|
||||
|
||||
## Decision
|
||||
|
||||
|
|
|
|||
|
|
@ -48,15 +48,15 @@ Source reviewed: Navigation 3 `1.1.0-beta01` (JetBrains fork), CMP `1.11.0-beta0
|
|||
- New `sharedTransitionScope: SharedTransitionScope?` parameter for shared element transitions.
|
||||
- Existing shell patterns in `app` and `desktop` remain valid using the default `SinglePaneSceneStrategy`.
|
||||
2. **Entry-scoped ViewModel lifecycle adopted.**
|
||||
- Both `app` and `desktop` now pass `ViewModelStoreNavEntryDecorator` + `SaveableStateHolderNavEntryDecorator` as explicit `entryDecorators` to `NavDisplay`.
|
||||
- Both `app` and `desktop` now use `MeshtasticNavDisplay` (`core:ui/commonMain`), which applies `ViewModelStoreNavEntryDecorator` + `SaveableStateHolderNavEntryDecorator` per active backstack.
|
||||
- ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are now scoped to the entry's backstack lifetime.
|
||||
3. **No direct Navigation 3 API breakage.**
|
||||
- Release is beta (API stabilized). No migration from alpha04 was required for existing usage patterns.
|
||||
4. **Primary risk is dependency wiring drift, not runtime behavior.**
|
||||
- JetBrains Navigation 3 currently publishes `navigation3-ui` coordinates (no separate `navigation3-runtime` artifact in Maven Central). The `jetbrains-navigation3-runtime` alias intentionally points to `navigation3-ui` and is documented in the version catalog.
|
||||
- Note: The `remember*` composable factory functions from `navigation3-runtime` are not visible in non-KMP Android modules due to Kotlin metadata resolution. Use direct class constructors instead (as done in `app/Main.kt`).
|
||||
5. **Saved-state and typed-route parity risk remains unchanged.**
|
||||
- Desktop still uses manual serializer registration; this is an existing risk and not introduced by beta01.
|
||||
5. **Saved-state and typed-route parity improved.**
|
||||
- Both hosts share `MeshtasticNavSavedStateConfig` from `core:navigation/commonMain` via `MultiBackstack`, reducing platform drift risk in serializer registration.
|
||||
6. **Updated active docs to reflect the current dependency baseline (`1.11.0-beta01`, `1.1.0-beta01`, `1.3.0-alpha06`, `2.11.0-alpha02`).**
|
||||
|
||||
### Actions Taken
|
||||
|
|
@ -66,7 +66,7 @@ Source reviewed: Navigation 3 `1.1.0-beta01` (JetBrains fork), CMP `1.11.0-beta0
|
|||
- `jetbrains-navigation3-runtime`, `jetbrains-navigation3-ui`
|
||||
- Documented in the version catalog that `jetbrains-navigation3-runtime` intentionally maps to `navigation3-ui` until a separate runtime artifact is published.
|
||||
- Migrated `core:data` `commonMain` from `androidx.lifecycle:lifecycle-runtime` (Google) to `org.jetbrains.androidx.lifecycle:lifecycle-runtime` (JetBrains fork) for full consistency.
|
||||
- Updated active docs to reflect the current dependency baseline (`1.11.0-alpha04`, `1.1.0-alpha04`, `1.3.0-alpha06`, `2.10.0-beta01`).
|
||||
- Updated active docs to reflect the current dependency baseline (`1.11.0-beta01`, `1.1.0-beta01`, `1.3.0-alpha06`, `2.11.0-alpha02`).
|
||||
- Consolidated `app` adaptive dependencies to JetBrains Material 3 Adaptive coordinates (`org.jetbrains.compose.material3.adaptive:*`) so Android and Desktop consume the same adaptive artifact family. The Android-only navigation suite remains on `androidx.compose.material3:material3-adaptive-navigation-suite`.
|
||||
|
||||
### Deferred Follow-ups
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Based on the latest codebase investigation, the following steps are proposed to
|
|||
| Navigation 3 parity model (shared `TopLevelDestination` + platform adapters) | ✅ Done | Both shells use shared enum + parity tests. See [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md) |
|
||||
| Hilt → Koin | ✅ Done | See [`decisions/koin-migration.md`](./decisions/koin-migration.md) |
|
||||
| BLE abstraction (Kable) | ✅ Done | See [`decisions/ble-strategy.md`](./decisions/ble-strategy.md) |
|
||||
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-alpha04`; supports Large (1200dp) and Extra-large (1600dp) breakpoints |
|
||||
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-beta01`; supports Large (1200dp) and Extra-large (1600dp) breakpoints |
|
||||
| JetBrains lifecycle/nav3 alias alignment | ✅ Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime |
|
||||
| Expect/actual consolidation | ✅ Done | 7 pairs eliminated; 15+ genuinely platform-specific retained |
|
||||
| Transport deduplication | ✅ Done | `StreamFrameCodec`, `TcpTransport`, and `SerialTransport` shared in `core:network` |
|
||||
|
|
@ -131,7 +131,7 @@ Based on the latest codebase investigation, the following steps are proposed to
|
|||
|
||||
All major ViewModels have now been extracted to `commonMain` and no longer rely on Android-specific subclasses. Platform-specific dependencies (like `android.net.Uri` or Location permissions) have been successfully isolated behind injected `core:repository` interfaces (e.g., `FileService`, `LocationService`).
|
||||
|
||||
**The extraction of all feature-specific navigation graphs, background services, and widgets out of `:app` is complete.** The `:app` module now only serves as the root DI assembler and NavHost container.
|
||||
**The extraction of all feature-specific navigation graphs, background services, and widgets out of `:app` is complete.** The `:app` module now only serves as the root DI assembler and shared Navigation 3 host shell (`MeshtasticNavDisplay`) container.
|
||||
|
||||
Extracted to shared `commonMain` (no longer app-only):
|
||||
- `SettingsViewModel` → `feature:settings/commonMain`
|
||||
|
|
|
|||
|
|
@ -50,9 +50,11 @@ kotlin {
|
|||
|
||||
androidMain.dependencies { implementation(libs.usb.serial.android) }
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.robolectric)
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.robolectric)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,15 @@ kotlin {
|
|||
implementation(libs.jetbrains.navigation3.ui)
|
||||
}
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(project.dependencies.platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(project.dependencies.platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,12 +46,14 @@ kotlin {
|
|||
|
||||
androidMain.dependencies { implementation(libs.material) }
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(project.dependencies.platform(libs.androidx.compose.bom))
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.test.core)
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(project.dependencies.platform(libs.androidx.compose.bom))
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.test.core)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,10 +56,12 @@ kotlin {
|
|||
|
||||
androidMain.dependencies { implementation(libs.androidx.work.runtime.ktx) }
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.androidx.work.testing)
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.robolectric)
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.androidx.work.testing)
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.robolectric)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ import org.meshtastic.core.model.MessageStatus
|
|||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.device_metrics_label_value
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.more_reactions
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.resources.select
|
||||
|
||||
|
|
@ -78,7 +80,9 @@ fun MessageActionsContent(
|
|||
val statusText = statusString?.second?.let { stringResource(it) }
|
||||
|
||||
ListItem(
|
||||
headlineContent = { Text("$title : $statusText") },
|
||||
headlineContent = {
|
||||
Text(stringResource(Res.string.device_metrics_label_value, title, statusText.orEmpty()))
|
||||
},
|
||||
leadingContent = { MessageStatusIcon(status = status) },
|
||||
modifier = Modifier.clickable(onClick = onStatus),
|
||||
)
|
||||
|
|
@ -140,7 +144,7 @@ private fun QuickEmojiRow(quickEmojis: List<String>, onReact: (String) -> Unit,
|
|||
) {
|
||||
Icon(
|
||||
Icons.Rounded.AddReaction,
|
||||
contentDescription = "More reactions",
|
||||
contentDescription = stringResource(Res.string.more_reactions),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import org.meshtastic.core.model.MessageStatus
|
|||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.a11y_message_from
|
||||
import org.meshtastic.core.resources.filter_message_label
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.ui.component.AutoLinkText
|
||||
|
|
@ -209,6 +210,8 @@ fun MessageItem(
|
|||
Modifier
|
||||
},
|
||||
)
|
||||
val senderName = if (message.fromLocal) ourNode.user.long_name else node.user.long_name
|
||||
val messageA11yText = stringResource(Res.string.a11y_message_from, senderName, message.text)
|
||||
if (showUserName && !message.fromLocal) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
|
|
@ -242,10 +245,7 @@ fun MessageItem(
|
|||
onDoubleClick = onDoubleClick,
|
||||
)
|
||||
.then(messageModifier)
|
||||
.semantics(mergeDescendants = true) {
|
||||
val senderName = if (message.fromLocal) ourNode.user.long_name else node.user.long_name
|
||||
contentDescription = "Message from $senderName: ${message.text}"
|
||||
},
|
||||
.semantics(mergeDescendants = true) { contentDescription = messageA11yText },
|
||||
color = containerColor,
|
||||
contentColor = contentColorFor(containerColor),
|
||||
shape = messageShape,
|
||||
|
|
|
|||
|
|
@ -68,13 +68,15 @@ kotlin {
|
|||
implementation(libs.markdown.renderer.android)
|
||||
}
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(libs.turbine)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(libs.turbine)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_label
|
||||
|
||||
@Composable
|
||||
fun ChannelInfo(
|
||||
|
|
@ -32,7 +35,7 @@ fun ChannelInfo(
|
|||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = Icons.Rounded.Tsunami,
|
||||
contentDescription = "Channel",
|
||||
contentDescription = stringResource(Res.string.channel_label),
|
||||
text = channel.toString(),
|
||||
contentColor = contentColor,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import org.jetbrains.compose.resources.DrawableResource
|
|||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.a11y_label_value
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.thenIf
|
||||
|
|
@ -65,6 +66,7 @@ fun InfoCard(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
val shape = MaterialTheme.shapes.medium
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
val contentDescriptionText = stringResource(Res.string.a11y_label_value, text, value)
|
||||
|
||||
Card(
|
||||
modifier =
|
||||
|
|
@ -77,7 +79,7 @@ fun InfoCard(
|
|||
onClick = {},
|
||||
role = Role.Button,
|
||||
)
|
||||
.semantics(mergeDescendants = true) { contentDescription = "$text: $value" },
|
||||
.semantics(mergeDescendants = true) { contentDescription = contentDescriptionText },
|
||||
shape = shape,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.a11y_label_value
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
|
||||
|
|
@ -94,6 +95,7 @@ internal fun InfoItem(
|
|||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
val contentDescriptionText = stringResource(Res.string.a11y_label_value, label, value)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
|
|
@ -109,7 +111,7 @@ internal fun InfoItem(
|
|||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
.semantics(mergeDescendants = true) {
|
||||
// Screen readers read as a unified data unit
|
||||
contentDescription = "$label: $value"
|
||||
contentDescription = contentDescriptionText
|
||||
},
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import org.meshtastic.core.model.DataPacket
|
|||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.a11y_label_value
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.details
|
||||
import org.meshtastic.core.resources.encryption_error
|
||||
|
|
@ -323,6 +324,7 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
}
|
||||
val label = stringResource(Res.string.public_key)
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
val contentDescriptionText = stringResource(Res.string.a11y_label_value, label, publicKeyBase64)
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
|
|
@ -339,7 +341,7 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
role = Role.Button,
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
.semantics(mergeDescendants = true) { contentDescription = "$label: $publicKeyBase64" },
|
||||
.semantics(mergeDescendants = true) { contentDescription = contentDescriptionText },
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.hardware_model
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.node_id
|
||||
|
|
@ -167,7 +168,7 @@ fun HardwareInfo(
|
|||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = Icons.Rounded.Router,
|
||||
contentDescription = "Hardware Model",
|
||||
contentDescription = stringResource(Res.string.hardware_model),
|
||||
text = hwModel,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
contentColor = contentColor,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import org.meshtastic.core.resources.latitude
|
|||
import org.meshtastic.core.resources.longitude
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.resources.speed
|
||||
import org.meshtastic.core.resources.speed_kmh
|
||||
import org.meshtastic.core.resources.timestamp
|
||||
import org.meshtastic.core.ui.util.formatPositionTime
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -92,7 +93,7 @@ fun PositionItem(compactWidth: Boolean, position: Position, system: Config.Displ
|
|||
PositionText(position.sats_in_view.toString(), WEIGHT_10)
|
||||
PositionText((position.altitude ?: 0).metersIn(system).toString(system), WEIGHT_15)
|
||||
if (!compactWidth) {
|
||||
PositionText("${position.ground_speed ?: 0} Km/h", WEIGHT_15)
|
||||
PositionText(stringResource(Res.string.speed_kmh, position.ground_speed ?: 0), WEIGHT_15)
|
||||
PositionText(formatString("%.0f°", (position.ground_track ?: 0) * HEADING_DEG), WEIGHT_15)
|
||||
}
|
||||
PositionText(position.formatPositionTime(), WEIGHT_40)
|
||||
|
|
|
|||
|
|
@ -56,15 +56,6 @@ kotlin {
|
|||
implementation(libs.androidx.appcompat)
|
||||
}
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
implementation(libs.turbine)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(project(":core:testing"))
|
||||
implementation(project(":core:datastore"))
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ import org.meshtastic.core.resources.role_tracker_desc
|
|||
import org.meshtastic.core.resources.router_role_confirmation_text
|
||||
import org.meshtastic.core.resources.time_zone
|
||||
import org.meshtastic.core.resources.triple_click_adhoc_ping
|
||||
import org.meshtastic.core.resources.unrecognized
|
||||
import org.meshtastic.core.ui.component.DropDownPreference
|
||||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.component.InsetDivider
|
||||
|
|
@ -137,7 +136,6 @@ private val Config.DeviceConfig.Role.description: StringResource
|
|||
Config.DeviceConfig.Role.LOST_AND_FOUND -> Res.string.role_lost_and_found_desc
|
||||
Config.DeviceConfig.Role.TAK_TRACKER -> Res.string.role_tak_tracker_desc
|
||||
Config.DeviceConfig.Role.ROUTER_LATE -> Res.string.role_router_late_desc
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
private val Config.DeviceConfig.RebroadcastMode.description: StringResource
|
||||
|
|
@ -150,8 +148,6 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource
|
|||
Config.DeviceConfig.RebroadcastMode.NONE -> Res.string.rebroadcast_mode_none_desc
|
||||
Config.DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY ->
|
||||
Res.string.rebroadcast_mode_core_portnums_only_desc
|
||||
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "LongMethod")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue