From 464a12b9f79eab6169eb544e16912dc04c57dce4 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:25:37 -0500 Subject: [PATCH] chore: standardize resources and update documentation for Navigation 3 (#4961) --- .github/copilot-instructions.md | 22 ++++++++------ AGENTS.md | 9 ++++-- GEMINI.md | 19 ++++++++---- README.md | 4 +-- app/README.md | 2 +- .../meshtastic/buildlogic/KotlinAndroid.kt | 5 +--- .../core/database/DatabaseBuilder.kt | 2 ++ core/navigation/README.md | 25 +++++++++++----- .../composeResources/values/strings.xml | 16 ++++++++++ core/ui/build.gradle.kts | 3 +- .../core/ui/component/IndoorAirQuality.kt | 20 +++++++------ .../ui/component/LazyColumnDragAndDropDemo.kt | 13 +++++--- .../ui/component/MeshtasticCommonAppSetup.kt | 2 +- .../core/ui/component/RegularPreference.kt | 2 +- .../core/ui/component/TelemetryInfo.kt | 3 +- .../core/ui/emoji/EmojiPickerDialog.kt | 8 +++-- .../meshtastic/core/ui/util/AlertPreviews.kt | 8 +++-- desktop/build.gradle.kts | 2 +- .../kotlin/org/meshtastic/desktop/Main.kt | 10 +++++-- docs/agent-playbooks/README.md | 8 ++--- docs/agent-playbooks/common-practices.md | 5 ++-- .../di-navigation3-anti-patterns-playbook.md | 2 +- docs/agent-playbooks/task-playbooks.md | 9 +++--- .../testing-and-ci-playbook.md | 8 ++--- .../navigation3-api-alignment-2026-03.md | 30 +++++++------------ docs/decisions/navigation3-parity-2026-03.md | 8 ++--- docs/kmp-status.md | 4 +-- feature/connections/build.gradle.kts | 8 +++-- feature/intro/build.gradle.kts | 16 +++++----- feature/map/build.gradle.kts | 14 +++++---- feature/messaging/build.gradle.kts | 10 ++++--- .../component/MessageActionsBottomSheet.kt | 8 +++-- .../messaging/component/MessageItem.kt | 8 ++--- feature/node/build.gradle.kts | 16 +++++----- .../feature/node/component/ChannelInfo.kt | 5 +++- .../feature/node/component/InfoCard.kt | 4 ++- .../node/component/NodeDetailComponents.kt | 4 ++- .../node/component/NodeDetailsSection.kt | 4 ++- .../feature/node/component/TelemetryInfo.kt | 3 +- .../node/metrics/PositionLogComponents.kt | 3 +- feature/settings/build.gradle.kts | 9 ------ .../radio/component/DeviceConfigScreen.kt | 4 --- 42 files changed, 216 insertions(+), 149 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 48aaf6d14..39838d04d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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` in `commonMain` (e.g., `fun EntryProviderScope.settingsGraph(backStack: NavBackStack)`). Host shells (`app`, `desktop`) assemble these into a single `entryProvider` 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` 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. diff --git a/AGENTS.md b/AGENTS.md index 09a98620e..39838d04d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` in `commonMain` (e.g., `fun EntryProviderScope.settingsGraph(backStack: NavBackStack)`). Host shells (`app`, `desktop`) assemble these into a single `entryProvider` 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` 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` 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. diff --git a/GEMINI.md b/GEMINI.md index 16432e35e..39838d04d 100644 --- a/GEMINI.md +++ b/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` in `commonMain` (e.g., `fun EntryProviderScope.settingsGraph(backStack: NavBackStack)`). Host shells (`app`, `desktop`) assemble these into a single `entryProvider` 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` 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` 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. diff --git a/README.md b/README.md index 5aa7ebef0..4ad4c4921 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/README.md b/app/README.md index d462c3d1b..e0924789f 100644 --- a/app/README.md +++ b/app/README.md @@ -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. diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt index 7f96dd45a..392139947 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt @@ -189,12 +189,10 @@ private inline fun 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 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", ) } } diff --git a/core/database/src/iosMain/kotlin/org/meshtastic/core/database/DatabaseBuilder.kt b/core/database/src/iosMain/kotlin/org/meshtastic/core/database/DatabaseBuilder.kt index 183ff647b..f0c4499a1 100644 --- a/core/database/src/iosMain/kotlin/org/meshtastic/core/database/DatabaseBuilder.kt +++ b/core/database/src/iosMain/kotlin/org/meshtastic/core/database/DatabaseBuilder.kt @@ -58,6 +58,7 @@ actual fun getInMemoryDatabaseBuilder(): RoomDatabase.Builder { } /** Creates an iOS DataStore for database preferences. */ +@OptIn(ExperimentalForeignApi::class) actual fun createDatabaseDataStore(name: String): DataStore { val dir = documentDirectory() + "/datastore" NSFileManager.defaultManager.createDirectoryAtPath(dir, true, null, null) diff --git a/core/navigation/README.md b/core/navigation/README.md index d9fd84d1c..9927ebf7d 100644 --- a/core/navigation/README.md +++ b/core/navigation/README.md @@ -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`. ```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, destNum: Int) { + backStack.add(NodesRoutes.NodeDetail(destNum)) +} ``` ## Module dependency graph diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index b4dd96bf8..d44b19b7d 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -276,6 +276,21 @@ Match All | Any This will remove all log packets and database entries from your device - It is a full reset, and is permanent. Clear + Search emoji... + More reactions + Channel + %1$s: %2$s + Message from %1$s: %2$s + Header + Item %1$d + Footer + Pill + Dot + Text + Gauge + Gradient + This is a custom composable + With multiple lines and styles Message delivery status New messages below Direct message notifications @@ -783,6 +798,7 @@ Timestamp Heading Speed + %1$d Km/h Sats Alt Freq diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index a50d13d44..dbbe12db9 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -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) } } } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt index deb6cd03e..b84c11e13 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt @@ -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) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt index 7826480ea..b6ffd6e9c 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt @@ -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)) } } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticCommonAppSetup.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticCommonAppSetup.kt index c9e761e7a..8b512bc24 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticCommonAppSetup.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticCommonAppSetup.kt @@ -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( diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt index 04b86f71e..afa82460d 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt @@ -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, ) diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TelemetryInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TelemetryInfo.kt index 84cb45a69..26877ab5f 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TelemetryInfo.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TelemetryInfo.kt @@ -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, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt index 71c6dac40..9a67babc0 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerDialog.kt @@ -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), ) } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt index 3a4b2371a..bc4937fd5 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/AlertPreviews.kt @@ -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)) } }, ), diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 150c7841d..67d39e40c 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -39,7 +39,7 @@ kotlin { } compilerOptions { jvmTarget.set(JvmTarget.JVM_21) - freeCompilerArgs.add("-Xjvm-default=all") + freeCompilerArgs.add("-jvm-default=no-compatibility") } } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt index ea8562e21..1fe8ada5f 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -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) = 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() } + val httpClient = remember { koinApp.koin.get() } LaunchedEffect(args) { args.forEach { arg -> @@ -247,8 +251,10 @@ fun main(args: Array) = 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 { diff --git a/docs/agent-playbooks/README.md b/docs/agent-playbooks/README.md index 15550deea..428b3842d 100644 --- a/docs/agent-playbooks/README.md +++ b/docs/agent-playbooks/README.md @@ -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). diff --git a/docs/agent-playbooks/common-practices.md b/docs/agent-playbooks/common-practices.md index 05190aead..00f845846 100644 --- a/docs/agent-playbooks/common-practices.md +++ b/docs/agent-playbooks/common-practices.md @@ -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` and `backStack.add/removeLastOrNull`: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`. -- Example feature flow using `rememberNavBackStack` and `NavDisplay`: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`. +- Example graph using `EntryProviderScope` 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 diff --git a/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md b/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md index bbb4f62e1..2dc2352c2 100644 --- a/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md +++ b/docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md @@ -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` diff --git a/docs/agent-playbooks/task-playbooks.md b/docs/agent-playbooks/task-playbooks.md index 1929f157c..808279e6a 100644 --- a/docs/agent-playbooks/task-playbooks.md +++ b/docs/agent-playbooks/task-playbooks.md @@ -19,12 +19,12 @@ Reference examples: 1. Implement or extend base ViewModel logic in `feature//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 `()` 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: diff --git a/docs/agent-playbooks/testing-and-ci-playbook.md b/docs/agent-playbooks/testing-and-ci-playbook.md index 6e227a736..1ed2d469c 100644 --- a/docs/agent-playbooks/testing-and-ci-playbook.md +++ b/docs/agent-playbooks/testing-and-ci-playbook.md @@ -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): diff --git a/docs/decisions/navigation3-api-alignment-2026-03.md b/docs/decisions/navigation3-api-alignment-2026-03.md index a7f6452d8..503b0a503 100644 --- a/docs/decisions/navigation3-api-alignment-2026-03.md +++ b/docs/decisions/navigation3-api-alignment-2026-03.md @@ -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>` | Allows NavDisplay to render multi-pane Scenes | ❌ Not used — defaulting to `SinglePaneSceneStrategy` | +| `sceneStrategies: List>` | Allows NavDisplay to render multi-pane Scenes | ⚠️ Partially used — `MeshtasticNavDisplay` applies dialog/list-detail/supporting/single strategies | | `SceneStrategy` interface | Custom scene calculation from backstack entries | ❌ Not used | -| `DialogSceneStrategy` | Renders `entry(metadata = dialog())` entries as overlay Dialogs | ❌ Not used — dialogs handled manually | +| `DialogSceneStrategy` | Renders `entry(metadata = dialog())` entries as overlay Dialogs | ✅ Enabled in shared host wrapper | | `SceneDecoratorStrategy` | 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>` | Wraps entry content with additional behavior | ❌ Not used (defaulting to `SaveableStateHolderNavEntryDecorator`) | +| `entryDecorators: List>` | 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 { entry { ... } }` | 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` 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` 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(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 diff --git a/docs/decisions/navigation3-parity-2026-03.md b/docs/decisions/navigation3-parity-2026-03.md index c5633a6ee..1d1a8c7ed 100644 --- a/docs/decisions/navigation3-parity-2026-03.md +++ b/docs/decisions/navigation3-parity-2026-03.md @@ -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` 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 diff --git a/docs/kmp-status.md b/docs/kmp-status.md index 470d8e565..ad31e7578 100644 --- a/docs/kmp-status.md +++ b/docs/kmp-status.md @@ -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` diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index b96836c28..9ac1a69ba 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -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) + } } } } diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index fca91b056..e93ce2924 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -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) + } } } } diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index 0ab6d1e33..1880b136c 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -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) + } } } } diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 80e9d8c8c..e6634e0a1 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -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) + } } } } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt index f95c64b45..b89a88984 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt @@ -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, 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, ) diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 9a24b8a01..261fb0948 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -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, diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index 7a455abe9..8c6c3b746 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -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) + } } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt index dd5fed37a..cfaa5943a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/ChannelInfo.kt @@ -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, ) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt index b905b1887..d7ff83a7b 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/InfoCard.kt @@ -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), ) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt index 3f79154a7..514d890c1 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt @@ -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) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index 95291e07c..925e4ab5d 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -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( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt index 12acecf9d..46178dcce 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetryInfo.kt @@ -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, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt index d8eb46b0e..2a79f2fb1 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogComponents.kt @@ -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) diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index e98b068d1..43b5aeece 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -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")) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt index 66515e9c7..ee2dc19fb 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt @@ -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")