mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
fix: fix animation stalls and update dependencies for stability (#4784)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
90844301e8
commit
427c0f3bbb
38 changed files with 384 additions and 243 deletions
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
|
|
@ -74,6 +74,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
|
||||
- **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`.
|
||||
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
|
||||
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
|
||||
|
||||
|
|
@ -108,7 +109,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
**Testing:**
|
||||
```bash
|
||||
./gradlew test # Run local unit tests
|
||||
./gradlew testDebugUnitTest # CI-aligned Android unit tests
|
||||
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest # CI-aligned Android unit tests (flavor-explicit)
|
||||
./gradlew connectedAndroidTest # Run instrumented tests
|
||||
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
|
||||
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
|
||||
|
|
|
|||
10
AGENTS.md
10
AGENTS.md
|
|
@ -16,9 +16,9 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Core Architecture:** Modern Android Development (MAD) with KMP core.
|
||||
- **KMP Modules:** Most `core:*` modules. All declare `jvm()` target and compile clean on JVM.
|
||||
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX + flavor-specific decoder). Shared contracts abstracted into `core:ui/commonMain`.
|
||||
- **UI:** Jetpack Compose (Material 3).
|
||||
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app`.
|
||||
- **Navigation:** AndroidX Navigation 3 (JetBrains multiplatform fork) with shared backstack state.
|
||||
- **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 (Multiplatform fork) with shared backstack state.
|
||||
- **Lifecycle:** JetBrains multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`.
|
||||
- **Database:** Room KMP.
|
||||
|
||||
|
|
@ -74,6 +74,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
|
||||
- **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.
|
||||
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
|
||||
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
|
||||
|
||||
|
|
@ -108,7 +110,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
**Testing:**
|
||||
```bash
|
||||
./gradlew test # Run local unit tests
|
||||
./gradlew testDebugUnitTest # CI-aligned Android unit tests
|
||||
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest # CI-aligned Android unit tests (flavor-explicit)
|
||||
./gradlew connectedAndroidTest # Run instrumented tests
|
||||
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
|
||||
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
|
||||
|
|
|
|||
10
GEMINI.md
10
GEMINI.md
|
|
@ -16,9 +16,9 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Core Architecture:** Modern Android Development (MAD) with KMP core.
|
||||
- **KMP Modules:** Most `core:*` modules. All declare `jvm()` target and compile clean on JVM.
|
||||
- **Android-only Modules:** `core:api` (AIDL), `core:barcode` (CameraX + flavor-specific decoder). Shared contracts abstracted into `core:ui/commonMain`.
|
||||
- **UI:** Jetpack Compose (Material 3).
|
||||
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app`.
|
||||
- **Navigation:** AndroidX Navigation 3 (JetBrains multiplatform fork) with shared backstack state.
|
||||
- **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 (Multiplatform fork) with shared backstack state.
|
||||
- **Lifecycle:** JetBrains multiplatform `lifecycle-viewmodel-compose` and `lifecycle-runtime-compose`.
|
||||
- **Database:** Room KMP.
|
||||
|
||||
|
|
@ -74,6 +74,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`.
|
||||
- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries.
|
||||
- **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.
|
||||
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
|
||||
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
|
||||
|
||||
|
|
@ -108,7 +110,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
**Testing:**
|
||||
```bash
|
||||
./gradlew test # Run local unit tests
|
||||
./gradlew testDebugUnitTest # CI-aligned Android unit tests
|
||||
./gradlew testFdroidDebugUnitTest testGoogleDebugUnitTest # CI-aligned Android unit tests (flavor-explicit)
|
||||
./gradlew connectedAndroidTest # Run instrumented tests
|
||||
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
|
||||
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -11,7 +11,7 @@
|
|||
[](https://opencollective.com/meshtastic/)
|
||||
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
|
||||
|
||||
This is a tool for using Android with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
|
||||
This is a tool for using Android (and Compose Desktop) with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
|
||||
|
||||
This project is currently beta testing across various providers. If you have questions or feedback please [Join our discussion forum](https://github.com/orgs/meshtastic/discussions) or the [Discord Group](https://discord.gg/meshtastic) . We would love to hear from you!
|
||||
|
||||
|
|
@ -60,11 +60,11 @@ You can generate the documentation locally to preview your changes.
|
|||
|
||||
### Modern Android Development (MAD)
|
||||
The app follows modern Android development practices, built on top of a shared Kotlin Multiplatform (KMP) Core:
|
||||
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, enabling future support for Desktop and Web.
|
||||
- **UI:** Jetpack Compose (Material 3) using Compose Multiplatform resources.
|
||||
- **KMP Modules:** Business logic (`core:domain`), data sources (`core:data`, `core:database`, `core:datastore`), and communications (`core:network`, `core:ble`) are entirely platform-agnostic, targeting Android and Compose Desktop.
|
||||
- **UI:** JetBrains Compose Multiplatform (Material 3) using Compose Multiplatform resources.
|
||||
- **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow.
|
||||
- **Dependency Injection:** Koin with Koin Annotations (Compiler Plugin).
|
||||
- **Navigation:** Type-Safe Navigation (Jetpack Navigation).
|
||||
- **Dependency Injection:** Koin with Koin Annotations (K2 Compiler Plugin).
|
||||
- **Navigation:** JetBrains Navigation 3 (Multiplatform routing).
|
||||
- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms).
|
||||
|
||||
### Bluetooth Low Energy (BLE)
|
||||
|
|
|
|||
|
|
@ -235,9 +235,9 @@ dependencies {
|
|||
implementation(projects.feature.settings)
|
||||
implementation(projects.feature.firmware)
|
||||
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.androidx.compose.material3.adaptive.layout)
|
||||
implementation(libs.androidx.compose.material3.adaptive.navigation)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.layout)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
||||
implementation(libs.androidx.compose.material3.navigationSuite)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
|
|
@ -248,10 +248,10 @@ dependencies {
|
|||
implementation(libs.androidx.glance.appwidget.preview)
|
||||
implementation(libs.androidx.glance.material3)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.navigation3.runtime)
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.material.icons.rounded.TripOrigin
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
|
|
@ -140,12 +139,7 @@ private const val TRACEROUTE_OFFSET_METERS = 100.0
|
|||
private const val TRACEROUTE_BOUNDS_PADDING_PX = 120
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@OptIn(
|
||||
MapsComposeExperimentalApi::class,
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class,
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun MapView(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -803,7 +797,6 @@ fun Uri.getFileName(context: android.content.Context): String {
|
|||
return name
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayUnits = DisplayUnits.METRIC) {
|
||||
|
|
@ -812,7 +805,7 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayU
|
|||
Row(modifier = Modifier.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label, style = MaterialTheme.typography.labelMedium)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(value, style = MaterialTheme.typography.labelMediumEmphasized)
|
||||
Text(value, style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package org.meshtastic.app.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -29,8 +30,6 @@ import androidx.compose.material.icons.outlined.Navigation
|
|||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material.icons.rounded.LocationDisabled
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalFloatingToolbar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -47,7 +46,6 @@ import org.meshtastic.core.resources.refresh
|
|||
import org.meshtastic.core.resources.toggle_my_position
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun MapControlsOverlay(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -71,86 +69,80 @@ fun MapControlsOverlay(
|
|||
isRefreshing: Boolean = false,
|
||||
onRefresh: () -> Unit = {},
|
||||
) {
|
||||
HorizontalFloatingToolbar(
|
||||
modifier = modifier,
|
||||
expanded = true,
|
||||
leadingContent = {},
|
||||
trailingContent = {},
|
||||
content = {
|
||||
CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
|
||||
if (isNodeMap) {
|
||||
Row(modifier = modifier) {
|
||||
CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
|
||||
if (isNodeMap) {
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Tune,
|
||||
contentDescription = stringResource(Res.string.map_filter),
|
||||
onClick = onToggleMapFilterMenu,
|
||||
)
|
||||
NodeMapFilterDropdown(
|
||||
expanded = mapFilterMenuExpanded,
|
||||
onDismissRequest = onMapFilterMenuDismissRequest,
|
||||
mapViewModel = mapViewModel,
|
||||
)
|
||||
} else {
|
||||
Box {
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Tune,
|
||||
contentDescription = stringResource(Res.string.map_filter),
|
||||
onClick = onToggleMapFilterMenu,
|
||||
)
|
||||
NodeMapFilterDropdown(
|
||||
MapFilterDropdown(
|
||||
expanded = mapFilterMenuExpanded,
|
||||
onDismissRequest = onMapFilterMenuDismissRequest,
|
||||
mapViewModel = mapViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Map,
|
||||
contentDescription = stringResource(Res.string.map_tile_source),
|
||||
onClick = onToggleMapTypeMenu,
|
||||
)
|
||||
MapTypeDropdown(
|
||||
expanded = mapTypeMenuExpanded,
|
||||
onDismissRequest = onMapTypeMenuDismissRequest,
|
||||
mapViewModel = mapViewModel, // Pass mapViewModel
|
||||
onManageCustomTileProvidersClicked = onManageCustomTileProvidersClicked, // Pass new callback
|
||||
)
|
||||
}
|
||||
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Layers,
|
||||
contentDescription = stringResource(Res.string.manage_map_layers),
|
||||
onClick = onManageLayersClicked,
|
||||
)
|
||||
|
||||
if (showRefresh) {
|
||||
if (isRefreshing) {
|
||||
Box(modifier = Modifier.padding(8.dp)) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
} else {
|
||||
Box {
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Tune,
|
||||
contentDescription = stringResource(Res.string.map_filter),
|
||||
onClick = onToggleMapFilterMenu,
|
||||
)
|
||||
MapFilterDropdown(
|
||||
expanded = mapFilterMenuExpanded,
|
||||
onDismissRequest = onMapFilterMenuDismissRequest,
|
||||
mapViewModel = mapViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Map,
|
||||
contentDescription = stringResource(Res.string.map_tile_source),
|
||||
onClick = onToggleMapTypeMenu,
|
||||
)
|
||||
MapTypeDropdown(
|
||||
expanded = mapTypeMenuExpanded,
|
||||
onDismissRequest = onMapTypeMenuDismissRequest,
|
||||
mapViewModel = mapViewModel, // Pass mapViewModel
|
||||
onManageCustomTileProvidersClicked = onManageCustomTileProvidersClicked, // Pass new callback
|
||||
icon = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(Res.string.refresh),
|
||||
onClick = onRefresh,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MapButton(
|
||||
icon = Icons.Outlined.Layers,
|
||||
contentDescription = stringResource(Res.string.manage_map_layers),
|
||||
onClick = onManageLayersClicked,
|
||||
)
|
||||
|
||||
if (showRefresh) {
|
||||
if (isRefreshing) {
|
||||
Box(modifier = Modifier.padding(8.dp)) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
} else {
|
||||
MapButton(
|
||||
icon = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(Res.string.refresh),
|
||||
onClick = onRefresh,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Location tracking button
|
||||
MapButton(
|
||||
icon =
|
||||
if (isLocationTrackingEnabled) {
|
||||
Icons.Rounded.LocationDisabled
|
||||
} else {
|
||||
Icons.Outlined.MyLocation
|
||||
},
|
||||
contentDescription = stringResource(Res.string.toggle_my_position),
|
||||
onClick = onToggleLocationTracking,
|
||||
)
|
||||
},
|
||||
)
|
||||
// Location tracking button
|
||||
MapButton(
|
||||
icon =
|
||||
if (isLocationTrackingEnabled) {
|
||||
Icons.Rounded.LocationDisabled
|
||||
} else {
|
||||
Icons.Outlined.MyLocation
|
||||
},
|
||||
contentDescription = stringResource(Res.string.toggle_my_position),
|
||||
onClick = onToggleLocationTracking,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# Track fix_android_animations_20260313 Context
|
||||
|
||||
- [Specification](./spec.md)
|
||||
- [Implementation Plan](./plan.md)
|
||||
- [Metadata](./metadata.json)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"track_id": "fix_android_animations_20260313",
|
||||
"type": "bug",
|
||||
"status": "new",
|
||||
"created_at": "2026-03-13T12:00:00Z",
|
||||
"updated_at": "2026-03-13T12:00:00Z",
|
||||
"description": "Android animations broken - mainly noticeable on Connections screen, the indescriminate circular and linear progress bars don't move, and the MeshActivity animation is not firing, investigate recomposition and threading strangely enough they're working on Desktop"
|
||||
}
|
||||
27
conductor/archive/fix_android_animations_20260313/plan.md
Normal file
27
conductor/archive/fix_android_animations_20260313/plan.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Implementation Plan: Fix Android Animation Stalls
|
||||
|
||||
## Phase 1: Research and Reproduction
|
||||
- [x] Task: Historical Regression Analysis
|
||||
- [x] Compare current code with pre-2.7.14-internal versions to identify changes in threading or UI state management.
|
||||
- [x] Check `gh` history for commits related to `ConnectionsScreen` and `MeshActivity` transitions.
|
||||
- [x] Task: Reproduction and Diagnosis
|
||||
- [x] Create a reproduction case (manual or automated) that consistently shows stalled progress bars on Android.
|
||||
- [x] Inspect Recomposition counts using Layout Inspector or logging.
|
||||
- [x] Verify Coroutine Dispatchers used for UI state updates.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Research and Reproduction' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Fix Implementation
|
||||
- [x] Task: Core Animation Fix
|
||||
- [x] Apply fix to resolve threading/recomposition stalls (e.g., correct `Dispatcher.Main` usage or state hoisting).
|
||||
- [x] Verify progress bars on Connections screen are animating.
|
||||
- [x] Task: MeshActivity Transition Fix
|
||||
- [x] Fix animation firing for `MeshActivity` entries and exits.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Fix Implementation' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: Project-wide Audit and Final Verification
|
||||
- [x] Task: Audit App Animations
|
||||
- [x] Scan other screens for similar animation stalls and apply fixes where necessary.
|
||||
- [x] Task: Automated Testing
|
||||
- [x] Write/Update Compose UI tests to ensure animations are running on Android.
|
||||
- [x] Verify no regressions on Desktop.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: Project-wide Audit and Final Verification' (Protocol in workflow.md)
|
||||
25
conductor/archive/fix_android_animations_20260313/spec.md
Normal file
25
conductor/archive/fix_android_animations_20260313/spec.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Track Specification: Fix Android Animation Stalls (Regression)
|
||||
|
||||
## Overview
|
||||
This track aims to diagnose and resolve a regression introduced in recent `2.7.14-internal` releases where animations (standard Compose progress indicators and custom transitions) fail to fire on Android. While these animations work correctly on Desktop, they are "stuck" or "stalled" on Android, likely due to threading issues or recomposition failures.
|
||||
|
||||
## Historical Context
|
||||
- **Introduction**: This issue appeared during the `2.7.14-internal` release cycle.
|
||||
- **Comparison**: Older versions or the current Desktop build can be used as references to identify code changes that might have triggered the regression.
|
||||
|
||||
## Functional Requirements
|
||||
- **Animation Restoration**: Restore movement to indeterminate circular and linear progress bars, particularly on the Connections screen.
|
||||
- **Transition Fixes**: Ensure `MeshActivity` animations (entry/exit/transitions) fire as expected.
|
||||
- **Project-wide Audit**: Audit other screens for similar "stuck" animations.
|
||||
- **KMP Parity**: Ensure shared `commonMain` code functions correctly on both Android and Desktop.
|
||||
|
||||
## Non-Functional Requirements
|
||||
- **Performance**: Ensure no UI jank or excessive recompositions.
|
||||
- **Verification**: Use historical code comparison (via `gh` or temporary copies) to isolate the breaking change.
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Indeterminate progress bars on the Connections screen animate continuously.
|
||||
- [ ] `MeshActivity` animations fire correctly.
|
||||
- [ ] Root cause identified (Regression since 2.7.14-internal).
|
||||
- [ ] Automated UI tests verify animation behavior on Android.
|
||||
- [ ] Unit tests verify state flow if threading/ViewModels are involved.
|
||||
|
|
@ -43,7 +43,7 @@ kotlin {
|
|||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.jetbrains.lifecycle.runtime)
|
||||
implementation(libs.androidx.paging.common)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kermit)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ kotlin {
|
|||
commonMain.dependencies {
|
||||
implementation(projects.core.resources)
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.jetbrains.navigation3.runtime)
|
||||
}
|
||||
|
||||
commonTest.dependencies { implementation(kotlin("test")) }
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ kotlin {
|
|||
|
||||
androidMain.dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.ui)
|
||||
implementation(libs.compose.multiplatform.runtime)
|
||||
implementation(libs.compose.multiplatform.ui)
|
||||
}
|
||||
|
||||
commonTest.dependencies { implementation(kotlin("test")) }
|
||||
|
|
|
|||
|
|
@ -401,6 +401,10 @@
|
|||
<string name="battery">Battery</string>
|
||||
<string name="channel_utilization">ChUtil</string>
|
||||
<string name="air_utilization">AirUtil</string>
|
||||
<string name="device_metrics_percent_value">%1$s: %2$.1f%%</string>
|
||||
<string name="device_metrics_voltage_value">%1$s: %2$.1f V</string>
|
||||
<string name="device_metrics_numeric_value">%1$.1f</string>
|
||||
<string name="device_metrics_label_value">%1$s: %2$s</string>
|
||||
<string name="temperature">Temp</string>
|
||||
<string name="humidity">Hum</string>
|
||||
<string name="soil_temperature">Soil Temp</string>
|
||||
|
|
|
|||
|
|
@ -44,13 +44,13 @@ kotlin {
|
|||
implementation(projects.core.resources)
|
||||
implementation(projects.core.service)
|
||||
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.uiTooling)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.ui)
|
||||
implementation(libs.compose.multiplatform.foundation)
|
||||
implementation(libs.compose.multiplatform.runtime)
|
||||
implementation(libs.compose.multiplatform.resources)
|
||||
implementation(libs.compose.multiplatform.ui.tooling)
|
||||
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
|
|
|
|||
|
|
@ -107,11 +107,11 @@ dependencies {
|
|||
|
||||
// Compose Desktop
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.components.resources)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.runtime)
|
||||
implementation(libs.compose.multiplatform.foundation)
|
||||
implementation(libs.compose.multiplatform.resources)
|
||||
|
||||
// JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive)
|
||||
|
|
@ -119,10 +119,10 @@ dependencies {
|
|||
implementation(libs.jetbrains.compose.material3.adaptive.navigation)
|
||||
|
||||
// Navigation 3 (JetBrains fork — multiplatform)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
|
||||
// Koin DI
|
||||
implementation(libs.koin.core)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ Build Verification:
|
|||
- **Removed:** Manual `dependsOn(...)` wiring from `core:common`, `core:model`, `core:network`, and `core:ui`
|
||||
- **Analyzed:** Composition opportunities for other duplicate plugins
|
||||
- **Documented:** Future optimization paths and consolidation criteria
|
||||
- **Migrated:** JetBrains Compose Multiplatform dependencies from hard-coded/legacy `compose.xyz` references to proper version catalog entries.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -136,6 +137,7 @@ Build Verification:
|
|||
### Short Term
|
||||
- [ ] Consider plugin validation test suite
|
||||
- [ ] Review other configuration functions for consolidation opportunities
|
||||
- [ ] Investigate factoring out JetBrains CMP dependencies into `meshtastic.kmp.library.compose` convention.
|
||||
|
||||
### Long Term
|
||||
- [ ] Monitor if Android Application/Library handling diverges
|
||||
|
|
|
|||
|
|
@ -9,16 +9,33 @@ 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.10`
|
||||
- Koin: `4.2.0-RC1` (`koin-annotations` `2.1.0`, compiler plugin `0.3.0`)
|
||||
- AndroidX Navigation 3 (JetBrains fork): `1.1.0-alpha03` (`org.jetbrains.androidx.navigation3`)
|
||||
- JetBrains Lifecycle (multiplatform): `2.10.0-alpha08` (`org.jetbrains.androidx.lifecycle`)
|
||||
- AndroidX Lifecycle (Android-only): `2.10.0`
|
||||
- Koin: `4.2.0-RC2` (`koin-annotations` `2.1.0`, compiler plugin `0.4.0`)
|
||||
- JetBrains Navigation 3: `1.1.0-alpha04` (`org.jetbrains.androidx.navigation3`)
|
||||
- JetBrains Lifecycle (multiplatform): `2.10.0-beta01` (`org.jetbrains.androidx.lifecycle`)
|
||||
- AndroidX Lifecycle (Android-only): `2.10.0` (`androidx.lifecycle`)
|
||||
- Kotlin Coroutines: `1.10.2`
|
||||
- Compose Multiplatform: `1.11.0-alpha03`
|
||||
- JetBrains Material 3 Adaptive: `1.3.0-alpha05` (`org.jetbrains.compose.material3.adaptive`)
|
||||
- Compose Multiplatform: `1.11.0-alpha04`
|
||||
- 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).
|
||||
|
||||
## Dependency alias quick-reference
|
||||
|
||||
Version catalog aliases split cleanly by fork provenance. **Use the right prefix for the right source set.**
|
||||
|
||||
| Alias prefix | Coordinates | Use in |
|
||||
|---|---|---|
|
||||
| `jetbrains-lifecycle-*` | `org.jetbrains.androidx.lifecycle:*` | `commonMain`, `androidMain` |
|
||||
| `jetbrains-navigation3-*` | `org.jetbrains.androidx.navigation3:*` | `commonMain`, `androidMain` |
|
||||
| `jetbrains-compose-material3-adaptive-*` | `org.jetbrains.compose.material3.adaptive:*` | `commonMain`, `androidMain` |
|
||||
| `androidx-lifecycle-process` | `androidx.lifecycle:lifecycle-process` | `androidMain` only — `ProcessLifecycleOwner` |
|
||||
| `androidx-lifecycle-runtime-ktx` | `androidx.lifecycle:lifecycle-runtime-ktx` | `androidMain` only |
|
||||
| `androidx-lifecycle-viewmodel-ktx` | `androidx.lifecycle:lifecycle-viewmodel-ktx` | `androidMain` only |
|
||||
| `androidx-lifecycle-testing` | `androidx.lifecycle:lifecycle-runtime-testing` | `androidUnitTest` only |
|
||||
| `androidx-navigation-common` | `androidx.navigation:navigation-common` | `androidMain` only |
|
||||
|
||||
> `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same `navigation3-ui` artifact — JetBrains does not publish a separate runtime artifact yet.
|
||||
|
||||
Quick references:
|
||||
|
||||
- Koin annotations (4.2 docs): `https://insert-koin.io/docs/reference/koin-annotations/start`
|
||||
|
|
@ -37,5 +54,3 @@ Quick references:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,34 @@ Both modules still define separate graph-builder files (`app/navigation/*.kt`, `
|
|||
4. **Route keys are shared; graph registration is per-platform.**
|
||||
- This is the expected state — platform shells wire entries differently while consuming the same route types.
|
||||
|
||||
## Alpha04 Changelog Impact Check (2026-03-13)
|
||||
|
||||
Source reviewed: Compose Multiplatform `v1.11.0-alpha04` release notes.
|
||||
|
||||
1. **No direct Navigation 3 API breakage called out.**
|
||||
- Release notes include component version bumps for Navigation 3 (`1.1.0-alpha04`) but no `NavBackStack`, `NavDisplay`, or `entryProvider` API migration requirements.
|
||||
- Existing shell patterns in `app` and `desktop` remain valid.
|
||||
2. **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.
|
||||
3. **Saved-state and typed-route parity risk remains unchanged.**
|
||||
- Desktop still uses manual serializer registration; this is an existing risk and not introduced by alpha04.
|
||||
4. **Compose-wide migration notes do not currently impact navigation codepaths.**
|
||||
- `Shader` wrapper changes and `Canvas.nativeCanvas` deprecations are not used in the Navigation 3 shell files.
|
||||
|
||||
### Actions Taken
|
||||
|
||||
- Renamed all JetBrains-forked lifecycle/nav3 version catalog aliases from `androidx-*` to `jetbrains-*` prefix to make fork provenance unambiguous:
|
||||
- `jetbrains-lifecycle-runtime`, `jetbrains-lifecycle-runtime-compose`, `jetbrains-lifecycle-viewmodel-compose`, `jetbrains-lifecycle-viewmodel-navigation3`
|
||||
- `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`).
|
||||
- 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
|
||||
|
||||
- Add automated validation that desktop serializer registrations stay in sync with shared route keys.
|
||||
|
||||
## Options Evaluated
|
||||
|
||||
### Option A: Reuse `:app` navigation implementation directly in desktop
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# KMP Migration Status
|
||||
|
||||
> Last updated: 2026-03-12
|
||||
> Last updated: 2026-03-13
|
||||
|
||||
Single source of truth for Kotlin Multiplatform migration progress. For the forward-looking roadmap, see [`roadmap.md`](./roadmap.md). For completed decision records, see [`decisions/`](./decisions/).
|
||||
|
||||
|
|
@ -105,7 +105,8 @@ 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 (Nordic Hybrid) | ✅ Done | See [`decisions/ble-strategy.md`](./decisions/ble-strategy.md) |
|
||||
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha05` aligned with CMP `1.11.0-alpha03` |
|
||||
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-alpha04` |
|
||||
| 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` shared in `core:network` |
|
||||
| **Transport UI Unification** | ✅ Done | `RadioInterfaceService` provides dynamic transport capability to shared UI |
|
||||
|
|
@ -140,10 +141,10 @@ Extracted to shared `commonMain` (no longer app-only):
|
|||
|
||||
| Dependency | Version | Why |
|
||||
|---|---|---|
|
||||
| Compose Multiplatform | `1.11.0-alpha03` | Required for JetBrains Adaptive `1.3.0-alpha05` |
|
||||
| Koin | `4.2.0-RC1` | Nav3 + K2 compiler plugin support |
|
||||
| JetBrains Lifecycle | `2.10.0-alpha08` | Multiplatform ViewModel/lifecycle |
|
||||
| JetBrains Navigation 3 | `1.1.0-alpha03` | Multiplatform navigation |
|
||||
| Compose Multiplatform | `1.11.0-alpha04` | Required for JetBrains Adaptive `1.3.0-alpha06` |
|
||||
| Koin | `4.2.0-RC2` | Nav3 + K2 compiler plugin support |
|
||||
| JetBrains Lifecycle | `2.10.0-beta01` | Multiplatform ViewModel/lifecycle |
|
||||
| JetBrains Navigation 3 | `1.1.0-alpha04` | Multiplatform navigation |
|
||||
| Nordic BLE | `2.0.0-alpha16` | Behind abstraction boundary |
|
||||
|
||||
**Policy:** Stable by default. RC when it unlocks KMP functionality. Alpha only behind hard abstraction seams. Do not downgrade CMP or Koin — they enable critical KMP features.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ These items address structural gaps identified in the March 2026 architecture re
|
|||
| Add feature module `commonTest` (settings, node, messaging) | Medium | Medium | ✅ |
|
||||
| Desktop Koin `checkModules()` integration test | Medium | Low | ✅ |
|
||||
| Auto-wire Desktop ViewModels via K2 Compiler (eliminate manual wiring) | Medium | Low | ✅ |
|
||||
| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ |
|
||||
|
||||
## Active Work
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ These items address structural gaps identified in the March 2026 architecture re
|
|||
4. **`feature:connections` module** — ✅ Done: Extracted connections UI into KMP feature module with dynamic transport availability detection
|
||||
5. **Navigation 3 parity baseline** — ✅ Done: shared `TopLevelDestination` in `core:navigation`; both shells use same enum; parity tests in `core:navigation/commonTest` and `desktop/test`
|
||||
6. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations)
|
||||
7. **Build-logic consolidation** — **Planned:** Consolidate expansive build-logic convention plugins. There is currently some duplication in Compose dependencies that should be factored into common conventions (`meshtastic.kmp.library.compose` vs manually specifying JetBrains CMP deps in feature modules).
|
||||
|
||||
## Medium-Term Priorities (60 days)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.foundation)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.foundation)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
|
|
@ -52,8 +52,8 @@ kotlin {
|
|||
implementation(projects.core.ble)
|
||||
implementation(projects.feature.settings)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.navigation3.runtime)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kermit)
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ kotlin {
|
|||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
implementation(libs.usb.serial.android)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -77,8 +79,11 @@ open class ScannerViewModel(
|
|||
timeout = kotlin.time.Duration.INFINITE,
|
||||
serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID,
|
||||
)
|
||||
.flowOn(kotlinx.coroutines.Dispatchers.IO)
|
||||
.collect { device ->
|
||||
scannedBleDevices.update { current -> current + (device.address to device) }
|
||||
if (!scannedBleDevices.value.containsKey(device.address)) {
|
||||
scannedBleDevices.update { current -> current + (device.address to device) }
|
||||
}
|
||||
}
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
co.touchlab.kermit.Logger.w(e) { "BLE scan failed" }
|
||||
|
|
@ -113,22 +118,29 @@ open class ScannerViewModel(
|
|||
// Sort by name
|
||||
(bonded + unbondedScanned).sortedBy { it.name }
|
||||
}
|
||||
.flowOn(kotlinx.coroutines.Dispatchers.Default)
|
||||
.distinctUntilChanged()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
/** UI StateFlow for USB devices. */
|
||||
val usbDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
discoveredDevicesFlow.map { it?.usbDevices ?: emptyList() }.stateInWhileSubscribed(initialValue = emptyList())
|
||||
discoveredDevicesFlow
|
||||
.map { it?.usbDevices ?: emptyList() }
|
||||
.distinctUntilChanged()
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
/** UI StateFlow for discovered TCP devices. */
|
||||
/** UI StateFlow for discovered TCP devices (NSD). */
|
||||
val discoveredTcpDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
discoveredDevicesFlow
|
||||
.map { it?.discoveredTcpDevices ?: emptyList() }
|
||||
.distinctUntilChanged()
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
/** UI StateFlow for recently connected TCP devices that are not currently discovered. */
|
||||
/** UI StateFlow for recent TCP devices. */
|
||||
val recentTcpDevicesForUi: StateFlow<List<DeviceListEntry>> =
|
||||
discoveredDevicesFlow
|
||||
.map { it?.recentTcpDevices ?: emptyList() }
|
||||
.distinctUntilChanged()
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val selectedAddressFlow: StateFlow<String?> = radioInterfaceService.currentDeviceAddressFlow
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
|
@ -104,7 +105,20 @@ fun ConnectionsScreen(
|
|||
val config by connectionsViewModel.localConfig.collectAsStateWithLifecycle()
|
||||
val scanStatusText by scanModel.errorText.collectAsStateWithLifecycle()
|
||||
val connectionState by connectionsViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
|
||||
// Prevent continuous recomposition from lastHeard and snr updates on the node
|
||||
val ourNode by
|
||||
remember(connectionsViewModel.ourNodeInfo) {
|
||||
connectionsViewModel.ourNodeInfo.distinctUntilChanged { old, new ->
|
||||
old?.num == new?.num &&
|
||||
old?.user == new?.user &&
|
||||
old?.batteryLevel == new?.batteryLevel &&
|
||||
old?.voltage == new?.voltage &&
|
||||
old?.metadata?.firmware_version == new?.metadata?.firmware_version
|
||||
}
|
||||
}
|
||||
.collectAsStateWithLifecycle(initialValue = connectionsViewModel.ourNodeInfo.value)
|
||||
|
||||
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
|
||||
val regionUnset = config.lora?.region == Config.LoRaConfig.RegionCode.UNSET
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
|
|
@ -35,8 +34,7 @@ import androidx.compose.ui.graphics.BlendMode
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.model.MeshActivity
|
||||
|
|
@ -56,13 +54,12 @@ fun AnimatedConnectionsNavIcon(
|
|||
) {
|
||||
var currentGlowColor by remember { mutableStateOf(Color.Transparent) }
|
||||
val animatedGlowAlpha = remember { Animatable(0f) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val sendColor = colorScheme.StatusGreen
|
||||
val receiveColor = colorScheme.StatusBlue
|
||||
|
||||
LaunchedEffect(meshActivityFlow, colorScheme) {
|
||||
meshActivityFlow.collectLatest { activity ->
|
||||
meshActivityFlow.conflate().collect { activity ->
|
||||
val newTargetColor =
|
||||
when (activity) {
|
||||
is MeshActivity.Send -> sendColor
|
||||
|
|
@ -70,15 +67,15 @@ fun AnimatedConnectionsNavIcon(
|
|||
}
|
||||
|
||||
currentGlowColor = newTargetColor
|
||||
// Launching in a new coroutine ensures the collect block is not suspended.
|
||||
coroutineScope.launch {
|
||||
animatedGlowAlpha.stop()
|
||||
animatedGlowAlpha.snapTo(1.0f)
|
||||
animatedGlowAlpha.animateTo(
|
||||
targetValue = 0.0f,
|
||||
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
||||
)
|
||||
}
|
||||
|
||||
// Suspend the collection until the animation finishes.
|
||||
// conflate() will drop any fast events that arrive during this 1-second animation.
|
||||
animatedGlowAlpha.stop()
|
||||
animatedGlowAlpha.snapTo(1.0f)
|
||||
animatedGlowAlpha.animateTo(
|
||||
targetValue = 0.0f,
|
||||
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ kotlin {
|
|||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("TooManyFunctions")
|
||||
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
|
||||
package org.meshtastic.feature.firmware
|
||||
|
||||
|
|
@ -46,13 +45,12 @@ import androidx.compose.material3.Button
|
|||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearWavyProgressIndicator
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
@ -228,6 +226,7 @@ fun FirmwareUpdateScreen(onNavigateUp: () -> Unit, viewModel: FirmwareUpdateView
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FirmwareUpdateScaffold(
|
||||
onNavigateUp: () -> Unit,
|
||||
|
|
@ -342,7 +341,7 @@ private fun FirmwareUpdateContent(
|
|||
|
||||
@Composable
|
||||
private fun VerifyingState() {
|
||||
CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
CircularProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(stringResource(Res.string.firmware_update_verifying), style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
|
@ -357,7 +356,7 @@ private fun VerifyingState() {
|
|||
|
||||
@Composable
|
||||
private fun CheckingState() {
|
||||
CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
CircularProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(stringResource(Res.string.firmware_update_checking), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
|
|
@ -706,7 +705,7 @@ private fun ProgressContent(
|
|||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
CircularWavyProgressIndicator(
|
||||
CircularProgressIndicator(
|
||||
progress = { if (isUpdating) progressState.progress else 1f },
|
||||
modifier = Modifier.size(64.dp),
|
||||
)
|
||||
|
|
@ -730,7 +729,7 @@ private fun ProgressContent(
|
|||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
if (isDownloading || isUpdating) {
|
||||
LinearWavyProgressIndicator(
|
||||
LinearProgressIndicator(
|
||||
progress = { progressState.progress },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||
)
|
||||
|
|
@ -761,7 +760,7 @@ private fun AwaitingFileSaveState(state: FirmwareUpdateState.AwaitingFileSave, o
|
|||
)
|
||||
}
|
||||
|
||||
CircularWavyProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
CircularProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(
|
||||
stringResource(Res.string.firmware_update_save_dfu_file),
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ kotlin {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.resources)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.jetbrains.navigation3.runtime)
|
||||
}
|
||||
|
||||
androidMain.dependencies {
|
||||
|
|
@ -53,7 +53,7 @@ kotlin {
|
|||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
}
|
||||
|
||||
commonTest.dependencies { implementation(projects.core.testing) }
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ kotlin {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.di)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ kotlin {
|
|||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.navigation.common)
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.foundation)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.foundation)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
|
|
@ -48,8 +48,8 @@ kotlin {
|
|||
implementation(projects.core.service)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.navigation3.runtime)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.androidx.paging.common)
|
||||
|
|
@ -68,7 +68,7 @@ kotlin {
|
|||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(libs.coil)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
|
|
@ -52,8 +52,9 @@ kotlin {
|
|||
implementation(projects.core.di)
|
||||
implementation(projects.feature.map)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.navigation3.runtime)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
|
|
|||
|
|
@ -30,10 +30,8 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
|
|
@ -42,7 +40,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -68,7 +65,6 @@ import org.meshtastic.feature.node.component.NodeFilterTextField
|
|||
import org.meshtastic.feature.node.component.NodeItem
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun NodeListScreen(
|
||||
|
|
@ -125,21 +121,18 @@ fun NodeListScreen(
|
|||
floatingActionButton = {
|
||||
val shareCapable = ourNode?.capabilities?.supportsQrCodeSharing ?: false
|
||||
val sharedContact: SharedContact? by viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
MeshtasticImportFAB(
|
||||
sharedContact = sharedContact,
|
||||
modifier =
|
||||
Modifier.animateFloatingActionButton(
|
||||
visible = !isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onImport = { uriString ->
|
||||
viewModel.handleScannedUri(uriString) {
|
||||
scope.launch { context.showToast(Res.string.channel_invalid) }
|
||||
}
|
||||
},
|
||||
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
|
||||
isContactContext = true,
|
||||
)
|
||||
if (!isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable) {
|
||||
MeshtasticImportFAB(
|
||||
sharedContact = sharedContact,
|
||||
onImport = { uriString ->
|
||||
viewModel.handleScannedUri(uriString) {
|
||||
scope.launch { context.showToast(Res.string.channel_invalid) }
|
||||
}
|
||||
},
|
||||
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
|
||||
isContactContext = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(contentPadding).focusable()) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,11 @@ import org.meshtastic.core.resources.air_utilization
|
|||
import org.meshtastic.core.resources.battery
|
||||
import org.meshtastic.core.resources.ch_util_definition
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.device_metrics_label_value
|
||||
import org.meshtastic.core.resources.device_metrics_log
|
||||
import org.meshtastic.core.resources.device_metrics_numeric_value
|
||||
import org.meshtastic.core.resources.device_metrics_percent_value
|
||||
import org.meshtastic.core.resources.device_metrics_voltage_value
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.component.MaterialBatteryInfo
|
||||
|
|
@ -240,16 +244,23 @@ private fun DeviceMetricsChart(
|
|||
val voltageColor = Device.VOLTAGE.color
|
||||
val chUtilColor = Device.CH_UTIL.color
|
||||
val airUtilColor = Device.AIR_UTIL.color
|
||||
val batteryLabel = stringResource(Res.string.battery)
|
||||
val voltageLabel = stringResource(Res.string.voltage)
|
||||
val channelUtilizationLabel = stringResource(Res.string.channel_utilization)
|
||||
val airUtilizationLabel = stringResource(Res.string.air_utilization)
|
||||
val percentValueTemplate = stringResource(Res.string.device_metrics_percent_value)
|
||||
val voltageValueTemplate = stringResource(Res.string.device_metrics_voltage_value)
|
||||
val numericValueTemplate = stringResource(Res.string.device_metrics_numeric_value)
|
||||
val marker =
|
||||
ChartStyling.rememberMarker(
|
||||
valueFormatter =
|
||||
ChartStyling.createColoredMarkerValueFormatter { value, color ->
|
||||
when (color.copy(alpha = 1f)) {
|
||||
batteryColor -> "Battery: %.1f%%".format(value)
|
||||
voltageColor -> "Voltage: %.1f V".format(value)
|
||||
chUtilColor -> "ChUtil: %.1f%%".format(value)
|
||||
airUtilColor -> "AirUtil: %.1f%%".format(value)
|
||||
else -> "%.1f".format(value)
|
||||
batteryColor -> percentValueTemplate.format(batteryLabel, value)
|
||||
voltageColor -> voltageValueTemplate.format(voltageLabel, value)
|
||||
chUtilColor -> percentValueTemplate.format(channelUtilizationLabel, value)
|
||||
airUtilColor -> percentValueTemplate.format(airUtilizationLabel, value)
|
||||
else -> numericValueTemplate.format(value)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -422,6 +433,11 @@ private fun DeviceMetricsChartPreview() {
|
|||
private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick: () -> Unit) {
|
||||
val deviceMetrics = telemetry.device_metrics
|
||||
val time = telemetry.time.toLong() * MS_PER_SEC
|
||||
val channelUtilizationLabel = stringResource(Res.string.channel_utilization)
|
||||
val airUtilizationLabel = stringResource(Res.string.air_utilization)
|
||||
val uptimeLabel = stringResource(Res.string.uptime)
|
||||
val percentValueTemplate = stringResource(Res.string.device_metrics_percent_value)
|
||||
val labelValueTemplate = stringResource(Res.string.device_metrics_label_value)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp).clickable { onClick() },
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
|
||||
|
|
@ -471,7 +487,11 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
|
|||
MetricIndicator(Device.CH_UTIL.color)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Ch: %.1f%%".format(deviceMetrics.channel_utilization ?: 0f),
|
||||
text =
|
||||
percentValueTemplate.format(
|
||||
channelUtilizationLabel,
|
||||
deviceMetrics.channel_utilization ?: 0f,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
)
|
||||
|
|
@ -481,7 +501,11 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
|
|||
MetricIndicator(Device.AIR_UTIL.color)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Air: %.1f%%".format(deviceMetrics.air_util_tx ?: 0f),
|
||||
text =
|
||||
percentValueTemplate.format(
|
||||
airUtilizationLabel,
|
||||
deviceMetrics.air_util_tx ?: 0f,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
)
|
||||
|
|
@ -489,9 +513,10 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
|
|||
}
|
||||
Text(
|
||||
text =
|
||||
stringResource(Res.string.uptime) +
|
||||
": " +
|
||||
labelValueTemplate.format(
|
||||
uptimeLabel,
|
||||
formatUptime(deviceMetrics?.uptime_seconds ?: 0),
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
|
|
@ -49,8 +49,8 @@ kotlin {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.di)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
|
@ -31,10 +30,8 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.rounded.Clear
|
||||
import androidx.compose.material.icons.rounded.PhoneAndroid
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults.MediumContainerHeight
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
|
@ -150,7 +147,6 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource
|
|||
Res.string.rebroadcast_mode_core_portnums_only_desc
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Suppress("DEPRECATION", "LongMethod")
|
||||
@Composable
|
||||
fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
|
|
@ -283,7 +279,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
HorizontalDivider()
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier.height(MediumContainerHeight).fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = state.connected,
|
||||
shape = RectangleShape,
|
||||
onClick = { formState.value = formState.value.copy(tzdef = appTzPosixString) },
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ appcompat = "1.7.1"
|
|||
accompanist = "0.37.3"
|
||||
|
||||
# androidx
|
||||
androidxComposeMaterial3Adaptive = "1.2.0"
|
||||
androidxTracing = "1.10.5"
|
||||
datastore = "1.2.1"
|
||||
glance = "1.2.0-rc01"
|
||||
lifecycle = "2.10.0"
|
||||
jetbrains-lifecycle = "2.10.0-beta01"
|
||||
navigation = "2.9.7"
|
||||
navigation3 = "1.1.0-alpha03"
|
||||
navigation3 = "1.1.0-alpha04"
|
||||
paging = "3.4.2"
|
||||
room = "2.8.4"
|
||||
savedstate = "1.4.0"
|
||||
|
|
@ -81,25 +80,26 @@ androidx-core-location-altitude = { module = "androidx.core:core-location-altitu
|
|||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
androidx-emoji2-emojipicker = { module = "androidx.emoji2:emoji2-emojipicker", version = "1.6.0" }
|
||||
androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
|
||||
androidx-glance-appwidget-preview = { module = "androidx.glance:glance-appwidget-preview", version.ref = "glance" }
|
||||
androidx-glance-preview = { module = "androidx.glance:glance-preview", version.ref = "glance" }
|
||||
androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
|
||||
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
||||
# Android-only lifecycle (no KMP equivalent — use only in androidMain)
|
||||
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-testing = { module = "androidx.lifecycle:lifecycle-runtime-testing", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" }
|
||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" }
|
||||
# JetBrains KMP lifecycle (use in commonMain and androidMain)
|
||||
jetbrains-lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime", version.ref = "jetbrains-lifecycle" }
|
||||
jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" }
|
||||
jetbrains-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jetbrains-lifecycle" }
|
||||
jetbrains-lifecycle-viewmodel-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "jetbrains-lifecycle" }
|
||||
# AndroidX Navigation (legacy nav-compose; Android-only nav utilities)
|
||||
androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigation" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
|
||||
androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "navigation" }
|
||||
androidx-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||
androidx-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||
# JetBrains Navigation 3 currently publishes `navigation3-ui` (no separate `navigation3-runtime` artifact).
|
||||
# Both `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same coordinate.
|
||||
jetbrains-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||
androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "paging" }
|
||||
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
|
|
@ -113,15 +113,11 @@ androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version
|
|||
androidx-work-testing = { module = "androidx.work:work-testing", version = "2.11.1" }
|
||||
|
||||
# AndroidX Compose
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version = "2026.03.00" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2025.12.00" }
|
||||
androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }
|
||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-material3-navigationSuite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" }
|
||||
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "androidxComposeMaterial3Adaptive" }
|
||||
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" }
|
||||
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" }
|
||||
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
|
||||
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
|
||||
androidx-compose-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "androidxTracing" }
|
||||
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||
|
|
@ -132,7 +128,12 @@ androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling
|
|||
|
||||
# Compose Multiplatform
|
||||
compose-multiplatform-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version = "1.7.3" }
|
||||
|
||||
# JetBrains Material 3 Adaptive (multiplatform — Android, Desktop, iOS)
|
||||
jetbrains-compose-material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "jetbrains-adaptive" }
|
||||
|
|
@ -143,7 +144,6 @@ jetbrains-compose-material3-adaptive-navigation = { module = "org.jetbrains.comp
|
|||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.10.0" }
|
||||
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
||||
guava = { module = "com.google.guava:guava", version = "33.5.0-jre" }
|
||||
location-services = { module = "com.google.android.gms:play-services-location", version = "21.3.0" }
|
||||
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
||||
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
||||
|
|
@ -168,7 +168,6 @@ dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", versi
|
|||
|
||||
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" }
|
||||
kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version = "0.31.0" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-android" }
|
||||
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines-android" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-android" }
|
||||
|
|
@ -199,7 +198,6 @@ aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref
|
|||
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-network-core = { module = "io.coil-kt.coil3:coil-network-core", version.ref = "coil" }
|
||||
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" }
|
||||
dd-sdk-android-compose = { module = "com.datadoghq:dd-sdk-android-compose", version.ref = "dd-sdk-android" }
|
||||
|
|
@ -224,7 +222,6 @@ nordic-ble-env-android = { module = "no.nordicsemi.kotlin.ble:environment-androi
|
|||
nordic-ble-env-android-compose = { module = "no.nordicsemi.kotlin.ble:environment-android-compose", version.ref = "nordic-ble" }
|
||||
|
||||
nordic-common-core = { module = "no.nordicsemi.android.common:core", version.ref = "nordic-common" }
|
||||
nordic-common-logger = { module = "no.nordicsemi.android.common:logger", version.ref = "nordic-common" }
|
||||
nordic-common-permissions-ble = { module = "no.nordicsemi.android.common:permissions-ble", version.ref = "nordic-common" }
|
||||
nordic-common-permissions-notification = { module = "no.nordicsemi.android.common:permissions-notification", version.ref = "nordic-common" }
|
||||
nordic-common-scanner-ble = { module = "no.nordicsemi.android.common:scanner-ble", version.ref = "nordic-common" }
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ dependencies {
|
|||
implementation(projects.core.proto)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
|
||||
implementation(libs.jetbrains.lifecycle.runtime)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.material)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue