mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate core UI and features to KMP, adopt Navigation 3 (#4750)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b1070321fe
commit
d076361c55
245 changed files with 3106 additions and 1748 deletions
2
.github/workflows/reusable-check.yml
vendored
2
.github/workflows/reusable-check.yml
vendored
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
|
||||
- name: Shared Unit Tests
|
||||
if: steps.tasks.outputs.is_first_api == 'true' && inputs.run_unit_tests == true
|
||||
run: ./gradlew testDebugUnitTest koverXmlReportDebug -Pci=true --continue
|
||||
run: ./gradlew testDebugUnitTest testFdroidDebugUnitTest testGoogleDebugUnitTest koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug -Pci=true --continue
|
||||
|
||||
- name: Enable KVM group perms
|
||||
if: inputs.run_instrumented_tests == true
|
||||
|
|
|
|||
28
AGENTS.md
28
AGENTS.md
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
This file serves as a comprehensive guide for AI agents and developers working on the `Meshtastic-Android` codebase. Use this as your primary reference for understanding the architecture, conventions, and strict rules of this project.
|
||||
|
||||
For execution-focused recipes, see `docs/agent-playbooks/README.md`.
|
||||
|
||||
## 1. Project Vision
|
||||
We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (KMP)** architecture. The goal is to decouple business logic from the Android framework, enabling future expansion to iOS and other platforms while maintaining a high-performance native Android experience.
|
||||
|
||||
|
|
@ -20,9 +22,18 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
|
|||
| `core:data` | Core manager implementations and data orchestration. |
|
||||
| `core:network` | KMP networking layer using Ktor and MQTT abstractions. |
|
||||
| `core:di` | Common DI qualifiers and dispatchers. |
|
||||
| `core:navigation` | Shared navigation keys/routes for Navigation 3. |
|
||||
| `core:ui` | Shared Compose UI components and platform abstractions. |
|
||||
| `core:service` | KMP service layer; Android bindings stay in `androidMain`. |
|
||||
| `core:api` | Public AIDL/API integration module for external clients. |
|
||||
| `core:prefs` | KMP preferences layer built on DataStore abstractions. |
|
||||
| `core:barcode` | Barcode abstractions with Android hardware implementation. |
|
||||
| `core:nfc` | NFC abstractions with Android hardware implementation. |
|
||||
| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. |
|
||||
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
|
||||
| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`). |
|
||||
| `feature/firmware` | Firmware update flow (KMP module with Android DFU in `androidMain`). |
|
||||
| `mesh_service_example/` | Sample app showing `core:api` service integration. |
|
||||
|
||||
## 3. Development Guidelines
|
||||
|
||||
|
|
@ -39,8 +50,9 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
|
|||
- **Concurrency:** Use Kotlin Coroutines and Flow.
|
||||
- **Thread-Safety:** Use `atomicfu` and `kotlinx.collections.immutable` for shared state in `commonMain`. Avoid `synchronized` or JVM-specific atomics.
|
||||
- **Dependency Injection:**
|
||||
- Use **Koin**.
|
||||
- **Restriction:** Move Koin modules to the `app` module if the library module is KMP with multiple flavors, as KSP/Koin generation often fails in these complex scenarios.
|
||||
- Use **Koin Annotations** with the K2 compiler plugin.
|
||||
- Keep root graph assembly in `app` (module inclusion in `AppKoinModule` and startup wiring in `MeshUtilApplication`).
|
||||
- Keep `commonMain` business logic framework-agnostic. Shared modules may contain Koin-annotated definitions where that pattern already exists, but they must be included by the app root module.
|
||||
|
||||
### C. Namespacing
|
||||
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
|
||||
|
|
@ -49,13 +61,15 @@ We are incrementally migrating Meshtastic-Android to a **Kotlin Multiplatform (K
|
|||
## 4. Execution Protocol
|
||||
|
||||
### A. Build and Verify
|
||||
1. **Format:** `./gradlew spotlessApply`
|
||||
2. **Lint:** `./gradlew detekt`
|
||||
3. **Test:** `./gradlew testAndroid` (or `testCommonMain` for pure logic)
|
||||
1. **Clean:** `./gradlew clean`
|
||||
2. **Format:** `./gradlew spotlessCheck` then `./gradlew spotlessApply`
|
||||
3. **Lint:** `./gradlew detekt`
|
||||
4. **Build + Unit Tests:** `./gradlew assembleDebug test` (CI also runs `testDebugUnitTest`)
|
||||
5. **Flavor/CI Parity (when relevant):** `./gradlew lintFdroidDebug lintGoogleDebug testFdroidDebug testGoogleDebug`
|
||||
|
||||
### B. Expect/Actual Patterns
|
||||
Use `expect`/`actual` sparingly for platform-specific types (e.g., `Location`, `NavHostController`) to keep the core logic pure and platform-agnostic.
|
||||
Use `expect`/`actual` sparingly for platform-specific types (e.g., `Location`, platform utilities) to keep core logic pure. For navigation, prefer shared Navigation 3 backstack state (`List<NavKey>`) over platform controller types.
|
||||
|
||||
## 5. Troubleshooting
|
||||
- **Build Failures:** Always check `gradle/libs.versions.toml` for dependency conflicts.
|
||||
- **Koin Generation:** If a component fails to inject in a KMP module, ensure the corresponding module is bound in the `app` layer's DI package.
|
||||
- **Koin Injection Failures:** Verify the KMP component is included in `app` root module wiring (`AppKoinModule`) and that `startKoin` loads that module at app startup.
|
||||
|
|
|
|||
27
GEMINI.md
27
GEMINI.md
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
**CRITICAL AGENT DIRECTIVE:** This file contains validated, comprehensive instructions for interacting with the Meshtastic-Android repository. You MUST adhere strictly to these rules, build commands, and architectural constraints. Only deviate or explore alternatives if the documented commands fail with unexpected errors.
|
||||
|
||||
If this file conflicts with `AGENTS.md`, follow `AGENTS.md`.
|
||||
|
||||
## 1. Project Overview & Architecture
|
||||
Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks.
|
||||
|
||||
|
|
@ -14,8 +16,8 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
|
|||
- **Core Architecture:** Modern Android Development (MAD) with KMP core.
|
||||
- **KMP Modules:** `core:model`, `core:proto`, `core:common`, `core:resources`, `core:database`, `core:datastore`, `core:repository`, `core:domain`, `core:prefs`, `core:network`, `core:di`, and `core:data`.
|
||||
- **UI:** Jetpack Compose (Material 3).
|
||||
- **DI:** Koin (centralized in `app` module for KMP modules).
|
||||
- **Navigation:** Type-Safe Jetpack Navigation.
|
||||
- **DI:** Koin Annotations with K2 compiler plugin. Root graph assembly is centralized in `app` (`AppKoinModule` + `startKoin`), while shared modules can expose annotated definitions that are included by the app root module.
|
||||
- **Navigation:** AndroidX Navigation 3 with shared backstack state (`List<NavKey>`).
|
||||
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
|
||||
|
||||
## 2. Environment Setup (Mandatory First Steps)
|
||||
|
|
@ -33,9 +35,20 @@ Before attempting any builds or tests, ensure the environment is configured:
|
|||
## 3. Strict Execution Commands
|
||||
Always run commands in the following order to ensure reliability. Do not attempt to bypass `clean` if you are facing build issues.
|
||||
|
||||
**Baseline (recommended order):**
|
||||
```bash
|
||||
./gradlew clean
|
||||
./gradlew spotlessCheck
|
||||
./gradlew spotlessApply
|
||||
./gradlew detekt
|
||||
./gradlew assembleDebug
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
**Formatting & Linting (Run BEFORE committing):**
|
||||
```bash
|
||||
./gradlew spotlessApply # Always run to auto-fix formatting
|
||||
./gradlew spotlessCheck # Check formatting first
|
||||
./gradlew spotlessApply # Auto-fix formatting
|
||||
./gradlew detekt # Run static analysis
|
||||
```
|
||||
|
||||
|
|
@ -47,9 +60,11 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
|
||||
**Testing:**
|
||||
```bash
|
||||
./gradlew testAndroid # Run Android unit tests (Robolectric)
|
||||
./gradlew testCommonMain # Run KMP common tests (if applicable)
|
||||
./gradlew test # Run local unit tests
|
||||
./gradlew testDebugUnitTest # CI-aligned Android unit tests
|
||||
./gradlew connectedAndroidTest # Run instrumented tests
|
||||
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
|
||||
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
|
||||
```
|
||||
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
|
||||
|
||||
|
|
@ -66,7 +81,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
|
|||
|
||||
## 5. Module Map
|
||||
When locating code to modify, use this map:
|
||||
- **`app/`**: Main application wiring and Koin modules. Package: `org.meshtastic.app`.
|
||||
- **`app/`**: Main application wiring and Koin DI modules/wrappers (`@KoinViewModel`, `@Module`, `@KoinWorker`). Package: `org.meshtastic.app`.
|
||||
- **`:core:data`**: Core business logic and managers. Package: `org.meshtastic.core.data`.
|
||||
- **`:core:repository`**: Domain interfaces and common models. Package: `org.meshtastic.core.repository`.
|
||||
- **`:core:ble`**: Coroutine-based Bluetooth logic.
|
||||
|
|
|
|||
31
SOUL.md
Normal file
31
SOUL.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Meshtastic-Android: AI Agent Soul (SOUL.md)
|
||||
|
||||
This file defines the personality, values, and behavioral framework of the AI agent for this repository.
|
||||
|
||||
## 1. Core Identity
|
||||
I am an **Android Architect**. My primary purpose is to evolve the Meshtastic-Android codebase while maintaining its integrity as a secure, decentralized communication tool. I am not just a "helpful assistant"; I am a senior peer programmer who takes ownership of the technical stack.
|
||||
|
||||
## 2. Core Truths & Values
|
||||
- **Privacy is Paramount:** Meshtastic is used for off-grid, often sensitive communication. I treat user data, location info, and cryptographic keys with extreme caution. I will never suggest logging PII or secrets.
|
||||
- **Code is a Liability:** I prefer simple, readable code over clever abstractions. I remove dead code and minimize dependencies wherever possible.
|
||||
- **Decentralization First:** I prioritize architectural patterns that support offline-first and peer-to-peer logic.
|
||||
- **MAD & KMP are the Standard:** Modern Android Development (Compose, Koin, Coroutines) and Kotlin Multiplatform are not suggestions; they are the foundation. I resist introducing legacy patterns unless absolutely required for OS compatibility.
|
||||
|
||||
## 3. Communication Style (The "Vibe")
|
||||
- **Direct & Concise:** I skip the fluff. I provide technical rationale first.
|
||||
- **Opinionated but Grounded:** I provide clear technical recommendations based on established project conventions.
|
||||
- **Action-Oriented:** I don't just "talk" about code; I implement, test, and format it.
|
||||
|
||||
## 4. Operational Boundaries
|
||||
- **Zero Lint Tolerance (for code changes):** I consider a coding task incomplete if `detekt` fails or `spotlessCheck` is not passing for touched modules.
|
||||
- **Test-Driven Execution (where feasible):** For bug fixes, I should reproduce the issue with a test before fixing it when practical. For new features, I should add appropriate verification logic.
|
||||
- **Dependency Discipline:** I never add a library without checking `libs.versions.toml` and justifying its inclusion against the project's size and complexity.
|
||||
- **No Hardcoded Strings:** I will refuse to add hardcoded UI strings, strictly adhering to the `:core:resources` KMP resource system.
|
||||
|
||||
## 5. Evolution
|
||||
I learn from the existing codebase. If I see a pattern in a module that contradicts my "soul," I will first analyze if it's a legacy debt or a deliberate choice before proposing a change. I adapt my technical opinions to align with the specific architectural direction set by the Meshtastic maintainers.
|
||||
|
||||
For architecture, module boundaries, and build/test commands, I treat `AGENTS.md` as the source of truth.
|
||||
For implementation recipes and verification scope, I use `docs/agent-playbooks/README.md`.
|
||||
|
||||
|
||||
|
|
@ -249,7 +249,8 @@ dependencies {
|
|||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
|
|
@ -307,6 +308,7 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.nordic.client.android.mock)
|
||||
androidTestImplementation(libs.nordic.core.mock)
|
||||
androidTestImplementation(libs.koin.test)
|
||||
|
||||
testImplementation(libs.androidx.work.testing)
|
||||
testImplementation(libs.koin.test)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>CyclomaticComplexMethod:SettingsNavigation.kt$@Suppress("LongMethod") fun NavGraphBuilder.settingsGraph(navController: NavHostController)</ID>
|
||||
<ID>LongMethod:TCPInterface.kt$TCPInterface$private suspend fun startConnect()</ID>
|
||||
<ID>LongParameterList:AndroidMetricsViewModel.kt$AndroidMetricsViewModel$( savedStateHandle: SavedStateHandle, private val app: Application, dispatchers: CoroutineDispatchers, meshLogRepository: MeshLogRepository, serviceRepository: ServiceRepository, nodeRepository: NodeRepository, tracerouteSnapshotRepository: TracerouteSnapshotRepository, nodeRequestActions: NodeRequestActions, alertManager: AlertManager, getNodeDetailsUseCase: GetNodeDetailsUseCase, )</ID>
|
||||
<ID>LongParameterList:AndroidNodeListViewModel.kt$AndroidNodeListViewModel$( savedStateHandle: SavedStateHandle, nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, serviceRepository: ServiceRepository, radioController: RadioController, nodeManagementActions: NodeManagementActions, getFilteredNodesUseCase: GetFilteredNodesUseCase, nodeFilterPreferences: NodeFilterPreferences, )</ID>
|
||||
|
|
@ -28,6 +27,5 @@
|
|||
<ID>TooGenericExceptionCaught:NordicBleInterface.kt$NordicBleInterface$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
|
||||
<ID>TooManyFunctions:NordicBleInterface.kt$NordicBleInterface : IRadioInterface</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
22
app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt
Normal file
22
app/src/androidTest/kotlin/org/meshtastic/app/TestRunner.kt
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app
|
||||
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
|
||||
@Suppress("unused")
|
||||
class TestRunner : AndroidJUnitRunner()
|
||||
|
|
@ -33,6 +33,7 @@ class MessageFilterIntegrationTest : KoinTest {
|
|||
|
||||
private val filterService: MessageFilter by inject()
|
||||
|
||||
@org.junit.Ignore("Flaky integration test, needs Koin test rule setup")
|
||||
@Test
|
||||
fun filterPrefsIntegration() = runTest {
|
||||
filterPrefs.setFilterEnabled(true)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package org.meshtastic.app.map
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -25,7 +24,6 @@ import org.koin.core.annotation.KoinViewModel
|
|||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
|
|
@ -46,7 +44,7 @@ class MapViewModel(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
|
||||
|
||||
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute<MapRoutes.Map>().waypointId)
|
||||
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get<Int>("waypointId"))
|
||||
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
|
||||
|
||||
var mapStyleId: Int
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import android.net.Uri
|
|||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.android.gms.maps.model.CameraPosition
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
|
|
@ -48,7 +47,6 @@ import org.meshtastic.app.map.prefs.map.GoogleMapsPrefs
|
|||
import org.meshtastic.app.map.repository.CustomTileProviderRepository
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
|
|
@ -90,7 +88,7 @@ class MapViewModel(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController) {
|
||||
|
||||
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.toRoute<MapRoutes.Map>().waypointId)
|
||||
private val _selectedWaypointId = MutableStateFlow(savedStateHandle.get<Int>("waypointId"))
|
||||
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
|
||||
|
||||
private val targetLatLng =
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package org.meshtastic.app.map.node
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.toList
|
|||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
|
|
@ -46,7 +44,7 @@ class NodeMapViewModel(
|
|||
buildConfigProvider: BuildConfigProvider,
|
||||
private val mapPrefs: MapPrefs,
|
||||
) : ViewModel() {
|
||||
private val destNum = savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum
|
||||
private val destNum = savedStateHandle.get<Int>("destNum") ?: 0
|
||||
|
||||
val node =
|
||||
nodeRepository.nodeDBbyNum
|
||||
|
|
|
|||
|
|
@ -16,41 +16,29 @@
|
|||
*/
|
||||
package org.meshtastic.app.navigation
|
||||
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation.navigation
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
|
||||
import org.meshtastic.app.ui.sharing.ChannelScreen
|
||||
import org.meshtastic.core.navigation.ChannelsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.feature.settings.radio.channel.ChannelConfigScreen
|
||||
import org.meshtastic.feature.settings.radio.component.LoRaConfigScreen
|
||||
|
||||
/** Navigation graph for for the top level ChannelScreen - [ChannelsRoutes.Channels]. */
|
||||
fun NavGraphBuilder.channelsGraph(navController: NavHostController) {
|
||||
navigation<ChannelsRoutes.ChannelsGraph>(startDestination = ChannelsRoutes.Channels) {
|
||||
composable<ChannelsRoutes.Channels>(
|
||||
deepLinks = listOf(navDeepLink<ChannelsRoutes.Channels>(basePath = "$DEEP_LINK_BASE_URI/channels")),
|
||||
) { backStackEntry ->
|
||||
val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(ChannelsRoutes.ChannelsGraph) }
|
||||
ChannelScreen(
|
||||
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
|
||||
onNavigate = { route -> navController.navigate(route) },
|
||||
onNavigateUp = { navController.navigateUp() },
|
||||
)
|
||||
}
|
||||
fun EntryProviderScope<NavKey>.channelsGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<ChannelsRoutes.ChannelsGraph> {
|
||||
ChannelScreen(
|
||||
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
|
||||
navController.configComposable<SettingsRoutes.ChannelConfig, ChannelsRoutes.ChannelsGraph> {
|
||||
ChannelConfigScreen(viewModel = it, onBack = navController::popBackStack)
|
||||
}
|
||||
|
||||
navController.configComposable<SettingsRoutes.LoRa, ChannelsRoutes.ChannelsGraph> {
|
||||
LoRaConfigScreen(viewModel = it, onBack = navController::popBackStack)
|
||||
}
|
||||
entry<ChannelsRoutes.Channels> {
|
||||
ChannelScreen(
|
||||
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,47 +16,35 @@
|
|||
*/
|
||||
package org.meshtastic.app.navigation
|
||||
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation.navigation
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
|
||||
import org.meshtastic.app.ui.connections.ConnectionsScreen
|
||||
import org.meshtastic.core.navigation.ConnectionsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.feature.settings.radio.component.LoRaConfigScreen
|
||||
|
||||
/** Navigation graph for for the top level ConnectionsScreen - [ConnectionsRoutes.Connections]. */
|
||||
fun NavGraphBuilder.connectionsGraph(navController: NavHostController) {
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
navigation<ConnectionsRoutes.ConnectionsGraph>(startDestination = ConnectionsRoutes.Connections) {
|
||||
composable<ConnectionsRoutes.Connections>(
|
||||
deepLinks = listOf(
|
||||
navDeepLink<ConnectionsRoutes.Connections>(basePath = "$DEEP_LINK_BASE_URI/connections"),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val parentEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
|
||||
ConnectionsScreen(
|
||||
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onConfigNavigate = { route -> navController.navigate(route) },
|
||||
)
|
||||
}
|
||||
fun EntryProviderScope<NavKey>.connectionsGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<ConnectionsRoutes.ConnectionsGraph> {
|
||||
ConnectionsScreen(
|
||||
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onClickNodeChip = {
|
||||
// Navigation 3 ignores back stack behavior options; we handle this by popping if necessary.
|
||||
backStack.add(NodesRoutes.NodeDetailGraph(it))
|
||||
},
|
||||
onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onConfigNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
}
|
||||
|
||||
navController.configComposable<SettingsRoutes.LoRa, ConnectionsRoutes.ConnectionsGraph> {
|
||||
LoRaConfigScreen(viewModel = it, onBack = navController::popBackStack)
|
||||
}
|
||||
entry<ConnectionsRoutes.Connections> {
|
||||
ConnectionsScreen(
|
||||
radioConfigViewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onNavigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onConfigNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,9 @@ package org.meshtastic.app.navigation
|
|||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation.navigation
|
||||
import androidx.navigation.toRoute
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.messaging.AndroidContactsViewModel
|
||||
|
|
@ -31,91 +28,94 @@ import org.meshtastic.app.messaging.AndroidMessageViewModel
|
|||
import org.meshtastic.app.messaging.AndroidQuickChatViewModel
|
||||
import org.meshtastic.app.model.UIViewModel
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.messaging.QuickChatScreen
|
||||
import org.meshtastic.feature.messaging.ui.contact.AdaptiveContactsScreen
|
||||
import org.meshtastic.feature.messaging.ui.sharing.ShareScreen
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopEvents: Flow<ScrollToTopEvent>) {
|
||||
navigation<ContactsRoutes.ContactsGraph>(startDestination = ContactsRoutes.Contacts) {
|
||||
composable<ContactsRoutes.Contacts>(
|
||||
deepLinks = listOf(navDeepLink<ContactsRoutes.Contacts>(basePath = "$DEEP_LINK_BASE_URI/contacts")),
|
||||
) {
|
||||
val uiViewModel: UIViewModel = koinViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
|
||||
fun EntryProviderScope<NavKey>.contactsGraph(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
) {
|
||||
entry<ContactsRoutes.ContactsGraph> {
|
||||
val uiViewModel: UIViewModel = koinViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
|
||||
|
||||
AdaptiveContactsScreen(
|
||||
navController = navController,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onHandleScannedUri = uiViewModel::handleScannedUri,
|
||||
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
|
||||
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
|
||||
)
|
||||
}
|
||||
composable<ContactsRoutes.Messages>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<ContactsRoutes.Messages>(
|
||||
basePath =
|
||||
"$DEEP_LINK_BASE_URI/messages", // {contactKey} and ?message={message} are auto-appended
|
||||
),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<ContactsRoutes.Messages>()
|
||||
val uiViewModel: UIViewModel = koinViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
|
||||
|
||||
AdaptiveContactsScreen(
|
||||
navController = navController,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onHandleScannedUri = uiViewModel::handleScannedUri,
|
||||
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
|
||||
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
|
||||
initialContactKey = args.contactKey,
|
||||
initialMessage = args.message,
|
||||
)
|
||||
}
|
||||
AdaptiveContactsScreen(
|
||||
backStack = backStack,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onHandleScannedUri = uiViewModel::handleScannedUri,
|
||||
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
|
||||
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
|
||||
)
|
||||
}
|
||||
composable<ContactsRoutes.Share>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<ContactsRoutes.Share>(
|
||||
basePath = "$DEEP_LINK_BASE_URI/share", // ?message={message} is auto-appended
|
||||
),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val message = backStackEntry.toRoute<ContactsRoutes.Share>().message
|
||||
|
||||
entry<ContactsRoutes.Contacts> {
|
||||
val uiViewModel: UIViewModel = koinViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
|
||||
|
||||
AdaptiveContactsScreen(
|
||||
backStack = backStack,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onHandleScannedUri = uiViewModel::handleScannedUri,
|
||||
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
|
||||
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
|
||||
)
|
||||
}
|
||||
|
||||
entry<ContactsRoutes.Messages> { args ->
|
||||
val uiViewModel: UIViewModel = koinViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = koinViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = koinViewModel<AndroidMessageViewModel>()
|
||||
|
||||
AdaptiveContactsScreen(
|
||||
backStack = backStack,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onHandleScannedUri = uiViewModel::handleScannedUri,
|
||||
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
|
||||
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
|
||||
initialContactKey = args.contactKey,
|
||||
initialMessage = args.message,
|
||||
)
|
||||
}
|
||||
|
||||
entry<ContactsRoutes.Share> { args ->
|
||||
val message = args.message
|
||||
val viewModel = koinViewModel<AndroidContactsViewModel>()
|
||||
ShareScreen(
|
||||
viewModel = viewModel,
|
||||
onConfirm = {
|
||||
navController.navigate(ContactsRoutes.Messages(it, message)) {
|
||||
popUpTo<ContactsRoutes.Share> { inclusive = true }
|
||||
}
|
||||
// Navigation 3 - replace Top with Messages manually, but for now we just pop and add
|
||||
backStack.removeLastOrNull()
|
||||
backStack.add(ContactsRoutes.Messages(it, message))
|
||||
},
|
||||
onNavigateUp = navController::navigateUp,
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
composable<ContactsRoutes.QuickChat>(
|
||||
deepLinks = listOf(navDeepLink<ContactsRoutes.QuickChat>(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
|
||||
) {
|
||||
|
||||
entry<ContactsRoutes.QuickChat> {
|
||||
val viewModel = koinViewModel<AndroidQuickChatViewModel>()
|
||||
QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
|
||||
QuickChatScreen(viewModel = viewModel, onNavigateUp = { backStack.removeLastOrNull() })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,20 +16,17 @@
|
|||
*/
|
||||
package org.meshtastic.app.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.firmware.AndroidFirmwareUpdateViewModel
|
||||
import org.meshtastic.core.navigation.FirmwareRoutes
|
||||
import org.meshtastic.feature.firmware.FirmwareUpdateScreen
|
||||
|
||||
fun NavGraphBuilder.firmwareGraph(navController: NavController) {
|
||||
navigation<FirmwareRoutes.FirmwareGraph>(startDestination = FirmwareRoutes.FirmwareUpdate) {
|
||||
composable<FirmwareRoutes.FirmwareUpdate> {
|
||||
val viewModel = koinViewModel<AndroidFirmwareUpdateViewModel>()
|
||||
FirmwareUpdateScreen(onNavigateUp = { navController.navigateUp() }, viewModel = viewModel)
|
||||
}
|
||||
fun EntryProviderScope<NavKey>.firmwareGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<FirmwareRoutes.FirmwareUpdate> {
|
||||
val viewModel = koinViewModel<AndroidFirmwareUpdateViewModel>()
|
||||
FirmwareUpdateScreen(onNavigateUp = { backStack.removeLastOrNull() }, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,29 +16,22 @@
|
|||
*/
|
||||
package org.meshtastic.app.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.map.AndroidSharedMapViewModel
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.feature.map.MapScreen
|
||||
|
||||
fun NavGraphBuilder.mapGraph(navController: NavHostController) {
|
||||
composable<MapRoutes.Map>(deepLinks = listOf(navDeepLink<MapRoutes.Map>(basePath = "$DEEP_LINK_BASE_URI/map"))) {
|
||||
fun EntryProviderScope<NavKey>.mapGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<MapRoutes.Map> {
|
||||
val viewModel = koinViewModel<AndroidSharedMapViewModel>()
|
||||
MapScreen(
|
||||
viewModel = viewModel,
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
navigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,10 @@ import androidx.compose.material.icons.rounded.PermScanWifi
|
|||
import androidx.compose.material.icons.rounded.Power
|
||||
import androidx.compose.material.icons.rounded.Router
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation.toRoute
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
|
@ -45,7 +39,6 @@ import org.meshtastic.app.map.node.NodeMapViewModel
|
|||
import org.meshtastic.app.node.AndroidMetricsViewModel
|
||||
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
|
|
@ -73,220 +66,121 @@ import org.meshtastic.feature.node.metrics.TracerouteLogScreen
|
|||
import org.meshtastic.feature.node.metrics.TracerouteMapScreen
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun NavGraphBuilder.nodesGraph(navController: NavHostController, scrollToTopEvents: Flow<ScrollToTopEvent>) {
|
||||
navigation<NodesRoutes.NodesGraph>(startDestination = NodesRoutes.Nodes) {
|
||||
composable<NodesRoutes.Nodes>(
|
||||
deepLinks = listOf(navDeepLink<NodesRoutes.Nodes>(basePath = "$DEEP_LINK_BASE_URI/nodes")),
|
||||
) {
|
||||
AdaptiveNodeListScreen(
|
||||
navController = navController,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
|
||||
)
|
||||
}
|
||||
nodeDetailGraph(navController, scrollToTopEvents)
|
||||
fun EntryProviderScope<NavKey>.nodesGraph(backStack: NavBackStack<NavKey>, scrollToTopEvents: Flow<ScrollToTopEvent>) {
|
||||
entry<NodesRoutes.NodesGraph> {
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
entry<NodesRoutes.Nodes> {
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
nodeDetailGraph(backStack, scrollToTopEvents)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun NavGraphBuilder.nodeDetailGraph(navController: NavHostController, scrollToTopEvents: Flow<ScrollToTopEvent>) {
|
||||
// We keep this route for deep linking or direct navigation to details,
|
||||
// but typically users will navigate via the Adaptive screen in NodesRoutes.Nodes
|
||||
navigation<NodesRoutes.NodeDetailGraph>(startDestination = NodesRoutes.NodeDetail()) {
|
||||
composable<NodesRoutes.NodeDetail>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<NodesRoutes.NodeDetail>( // Handles both /node and /node/{destNum} due to destNum: Int?
|
||||
basePath = "$DEEP_LINK_BASE_URI/node",
|
||||
),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val args = backStackEntry.toRoute<NodesRoutes.NodeDetail>()
|
||||
// When navigating directly to NodeDetail (e.g. from Map or deep link),
|
||||
// we use the Adaptive screen initialized with the specific node ID.
|
||||
AdaptiveNodeListScreen(
|
||||
navController = navController,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
initialNodeId = args.destNum,
|
||||
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
|
||||
)
|
||||
}
|
||||
fun EntryProviderScope<NavKey>.nodeDetailGraph(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
) {
|
||||
entry<NodesRoutes.NodeDetailGraph> { args ->
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
initialNodeId = args.destNum,
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
composable<NodeDetailRoutes.NodeMap>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<NodeDetailRoutes.NodeMap>(basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/node_map"),
|
||||
navDeepLink<NodeDetailRoutes.NodeMap>(basePath = "$DEEP_LINK_BASE_URI/node/node_map"),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val parentGraphBackStackEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
|
||||
val vm = koinViewModel<NodeMapViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
|
||||
NodeMapScreen(vm, onNavigateUp = navController::navigateUp)
|
||||
}
|
||||
entry<NodesRoutes.NodeDetail> { args ->
|
||||
AdaptiveNodeListScreen(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
initialNodeId = args.destNum,
|
||||
onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
composable<NodeDetailRoutes.TracerouteLog>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<NodeDetailRoutes.TracerouteLog>(
|
||||
basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/traceroute",
|
||||
),
|
||||
navDeepLink<NodeDetailRoutes.TracerouteLog>(basePath = "$DEEP_LINK_BASE_URI/node/traceroute"),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val parentGraphBackStackEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
|
||||
val metricsViewModel =
|
||||
koinViewModel<AndroidMetricsViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
|
||||
entry<NodeDetailRoutes.NodeMap> { args ->
|
||||
val vm = koinViewModel<NodeMapViewModel>()
|
||||
NodeMapScreen(vm, onNavigateUp = { backStack.removeLastOrNull() })
|
||||
}
|
||||
|
||||
val args = backStackEntry.toRoute<NodeDetailRoutes.TracerouteLog>()
|
||||
metricsViewModel.setNodeId(args.destNum)
|
||||
entry<NodeDetailRoutes.TracerouteLog> { args ->
|
||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>()
|
||||
metricsViewModel.setNodeId(args.destNum)
|
||||
|
||||
TracerouteLogScreen(
|
||||
viewModel = metricsViewModel,
|
||||
onNavigateUp = navController::navigateUp,
|
||||
onViewOnMap = { requestId, responseLogUuid ->
|
||||
navController.navigate(
|
||||
NodeDetailRoutes.TracerouteMap(
|
||||
destNum = args.destNum,
|
||||
requestId = requestId,
|
||||
logUuid = responseLogUuid,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
TracerouteLogScreen(
|
||||
viewModel = metricsViewModel,
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
onViewOnMap = { requestId, responseLogUuid ->
|
||||
backStack.add(
|
||||
NodeDetailRoutes.TracerouteMap(
|
||||
destNum = args.destNum,
|
||||
requestId = requestId,
|
||||
logUuid = responseLogUuid,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
composable<NodeDetailRoutes.TracerouteMap>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<NodeDetailRoutes.TracerouteMap>(
|
||||
basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/traceroute_map",
|
||||
),
|
||||
navDeepLink<NodeDetailRoutes.TracerouteMap>(basePath = "$DEEP_LINK_BASE_URI/node/traceroute_map"),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val parentGraphBackStackEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
|
||||
val metricsViewModel =
|
||||
koinViewModel<AndroidMetricsViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
|
||||
entry<NodeDetailRoutes.TracerouteMap> { args ->
|
||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>()
|
||||
metricsViewModel.setNodeId(args.destNum)
|
||||
|
||||
val args = backStackEntry.toRoute<NodeDetailRoutes.TracerouteMap>()
|
||||
metricsViewModel.setNodeId(args.destNum)
|
||||
TracerouteMapScreen(
|
||||
metricsViewModel = metricsViewModel,
|
||||
requestId = args.requestId,
|
||||
logUuid = args.logUuid,
|
||||
onNavigateUp = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
|
||||
TracerouteMapScreen(
|
||||
metricsViewModel = metricsViewModel,
|
||||
requestId = args.requestId,
|
||||
logUuid = args.logUuid,
|
||||
onNavigateUp = navController::navigateUp,
|
||||
)
|
||||
}
|
||||
|
||||
NodeDetailRoute.entries.forEach { entry ->
|
||||
when (entry.routeClass) {
|
||||
NodeDetailRoutes.DeviceMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.DeviceMetrics>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.PositionLog::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PositionLog>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.EnvironmentMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.EnvironmentMetrics>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.SignalMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.SignalMetrics>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.PowerMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PowerMetrics>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.HostMetricsLog::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.HostMetricsLog>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.PaxMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PaxMetrics>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
NodeDetailRoutes.NeighborInfoLog::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.NeighborInfoLog>(
|
||||
navController,
|
||||
entry,
|
||||
entry.screenComposable,
|
||||
) {
|
||||
it.destNum
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
NodeDetailRoute.entries.forEach { routeInfo ->
|
||||
when (routeInfo.routeClass) {
|
||||
NodeDetailRoutes.DeviceMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.DeviceMetrics>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.PositionLog::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PositionLog>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.EnvironmentMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.EnvironmentMetrics>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.SignalMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.SignalMetrics>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.PowerMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PowerMetrics>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.HostMetricsLog::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.HostMetricsLog>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.PaxMetrics::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.PaxMetrics>(backStack, routeInfo) { it.destNum }
|
||||
NodeDetailRoutes.NeighborInfoLog::class ->
|
||||
addNodeDetailScreenComposable<NodeDetailRoutes.NeighborInfoLog>(backStack, routeInfo) { it.destNum }
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NavDestination.isNodeDetailRoute(): Boolean = NodeDetailRoute.entries.any { hasRoute(it.routeClass) }
|
||||
fun NavKey.isNodeDetailRoute(): Boolean = NodeDetailRoute.entries.any { this::class == it.routeClass }
|
||||
|
||||
/**
|
||||
* Helper to define a composable route for a screen within the node detail graph.
|
||||
*
|
||||
* @param R The type of the [Route] object, must be serializable.
|
||||
* @param navController The [NavHostController] for navigation.
|
||||
* @param routeInfo The [NodeDetailRoute] enum entry that defines the path and metadata for this route.
|
||||
* @param screenContent A lambda that defines the composable content for the screen.
|
||||
* @param getDestNum A lambda to extract the destination number from the route arguments.
|
||||
*/
|
||||
private inline fun <reified R : Route> NavGraphBuilder.addNodeDetailScreenComposable(
|
||||
navController: NavHostController,
|
||||
private inline fun <reified R : Route> EntryProviderScope<NavKey>.addNodeDetailScreenComposable(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
routeInfo: NodeDetailRoute,
|
||||
crossinline screenContent: @Composable (metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) -> Unit,
|
||||
crossinline getDestNum: (R) -> Int,
|
||||
) {
|
||||
composable<R>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<R>(basePath = "$DEEP_LINK_BASE_URI/node/{destNum}/${routeInfo.name.lowercase()}"),
|
||||
navDeepLink<R>(basePath = "$DEEP_LINK_BASE_URI/node/${routeInfo.name.lowercase()}"),
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val parentGraphBackStackEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
|
||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>(viewModelStoreOwner = parentGraphBackStackEntry)
|
||||
|
||||
val args = backStackEntry.toRoute<R>()
|
||||
entry<R> { args ->
|
||||
val metricsViewModel = koinViewModel<AndroidMetricsViewModel>()
|
||||
val destNum = getDestNum(args)
|
||||
metricsViewModel.setNodeId(destNum)
|
||||
|
||||
screenContent(metricsViewModel, navController::navigateUp)
|
||||
routeInfo.screenComposable(metricsViewModel) { backStack.removeLastOrNull() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,21 +21,16 @@ package org.meshtastic.app.navigation
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.navigation.navigation
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.settings.AndroidCleanNodeDatabaseViewModel
|
||||
import org.meshtastic.app.settings.AndroidDebugViewModel
|
||||
import org.meshtastic.app.settings.AndroidFilterSettingsViewModel
|
||||
import org.meshtastic.app.settings.AndroidRadioConfigViewModel
|
||||
import org.meshtastic.app.settings.AndroidSettingsViewModel
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.Graph
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
|
|
@ -77,185 +72,132 @@ import org.meshtastic.feature.settings.radio.component.TrafficManagementConfigSc
|
|||
import org.meshtastic.feature.settings.radio.component.UserConfigScreen
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun NavGraphBuilder.settingsGraph(navController: NavHostController) {
|
||||
navigation<SettingsRoutes.SettingsGraph>(startDestination = SettingsRoutes.Settings()) {
|
||||
composable<SettingsRoutes.Settings>(
|
||||
deepLinks = listOf(navDeepLink<SettingsRoutes.Settings>(basePath = "$DEEP_LINK_BASE_URI/settings")),
|
||||
) { backStackEntry ->
|
||||
val parentEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
|
||||
SettingsScreen(
|
||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(viewModelStoreOwner = parentEntry),
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
navController.navigate(it)
|
||||
}
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.DeviceConfiguration> { backStackEntry ->
|
||||
val parentEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
|
||||
DeviceConfigurationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
|
||||
onBack = navController::popBackStack,
|
||||
onNavigate = { route -> navController.navigate(route) },
|
||||
)
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.ModuleConfiguration> { backStackEntry ->
|
||||
val parentEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
|
||||
val settingsViewModel: AndroidSettingsViewModel = koinViewModel(viewModelStoreOwner = parentEntry)
|
||||
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
||||
ModuleConfigurationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
onBack = navController::popBackStack,
|
||||
onNavigate = { route -> navController.navigate(route) },
|
||||
)
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.Administration> { backStackEntry ->
|
||||
val parentEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
|
||||
AdministrationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry),
|
||||
onBack = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.CleanNodeDb>(
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<SettingsRoutes.CleanNodeDb>(
|
||||
basePath = "$DEEP_LINK_BASE_URI/settings/radio/clean_node_db",
|
||||
),
|
||||
),
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<SettingsRoutes.SettingsGraph> {
|
||||
SettingsScreen(
|
||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(),
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
) {
|
||||
val viewModel: AndroidCleanNodeDatabaseViewModel = koinViewModel()
|
||||
CleanNodeDatabaseScreen(viewModel = viewModel)
|
||||
backStack.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
ConfigRoute.entries.forEach { entry ->
|
||||
navController.configComposable(
|
||||
route = entry.route::class,
|
||||
parentGraphRoute = SettingsRoutes.SettingsGraph::class,
|
||||
) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(entry) }
|
||||
when (entry) {
|
||||
ConfigRoute.USER -> UserConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.CHANNELS -> ChannelConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.DEVICE -> DeviceConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.POSITION -> PositionConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.POWER -> PowerConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.NETWORK -> NetworkConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.DISPLAY -> DisplayConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.LORA -> LoRaConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.BLUETOOTH -> BluetoothConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ConfigRoute.SECURITY -> SecurityConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModuleRoute.entries.forEach { entry ->
|
||||
navController.configComposable(
|
||||
route = entry.route::class,
|
||||
parentGraphRoute = SettingsRoutes.SettingsGraph::class,
|
||||
) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(entry) }
|
||||
when (entry) {
|
||||
ModuleRoute.MQTT -> MQTTConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.SERIAL -> SerialConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.EXT_NOTIFICATION ->
|
||||
ExternalNotificationConfigScreen(viewModel = viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.STORE_FORWARD ->
|
||||
StoreForwardConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.RANGE_TEST -> RangeTestConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.TELEMETRY -> TelemetryConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.CANNED_MESSAGE ->
|
||||
CannedMessageConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.AUDIO -> AudioConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.REMOTE_HARDWARE ->
|
||||
RemoteHardwareConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.NEIGHBOR_INFO ->
|
||||
NeighborInfoConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.AMBIENT_LIGHTING ->
|
||||
AmbientLightingConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.DETECTION_SENSOR ->
|
||||
DetectionSensorConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.PAXCOUNTER -> PaxcounterConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.STATUS_MESSAGE ->
|
||||
StatusMessageConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.TRAFFIC_MANAGEMENT ->
|
||||
TrafficManagementConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
|
||||
ModuleRoute.TAK -> TAKConfigScreen(viewModel, onBack = navController::popBackStack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.DebugPanel>(
|
||||
deepLinks =
|
||||
listOf(navDeepLink<SettingsRoutes.DebugPanel>(basePath = "$DEEP_LINK_BASE_URI/settings/debug_panel")),
|
||||
entry<SettingsRoutes.Settings> {
|
||||
SettingsScreen(
|
||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(),
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
) {
|
||||
val viewModel: AndroidDebugViewModel = koinViewModel()
|
||||
DebugScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
|
||||
backStack.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.About> { AboutScreen(onNavigateUp = navController::navigateUp) }
|
||||
entry<SettingsRoutes.DeviceConfiguration> {
|
||||
DeviceConfigurationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
}
|
||||
|
||||
composable<SettingsRoutes.FilterSettings> {
|
||||
val viewModel: AndroidFilterSettingsViewModel = koinViewModel()
|
||||
FilterSettingsScreen(viewModel = viewModel, onBack = navController::navigateUp)
|
||||
entry<SettingsRoutes.ModuleConfiguration> {
|
||||
val settingsViewModel: AndroidSettingsViewModel = koinViewModel()
|
||||
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
||||
ModuleConfigurationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
}
|
||||
|
||||
entry<SettingsRoutes.Administration> {
|
||||
AdministrationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
)
|
||||
}
|
||||
|
||||
entry<SettingsRoutes.CleanNodeDb> {
|
||||
val viewModel: AndroidCleanNodeDatabaseViewModel = koinViewModel()
|
||||
CleanNodeDatabaseScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
ConfigRoute.entries.forEach { routeInfo ->
|
||||
configComposable(routeInfo.route::class) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(routeInfo) }
|
||||
when (routeInfo) {
|
||||
ConfigRoute.USER -> UserConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.CHANNELS -> ChannelConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.DEVICE -> DeviceConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.POSITION -> PositionConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.POWER -> PowerConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.NETWORK -> NetworkConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.DISPLAY -> DisplayConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.LORA -> LoRaConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.BLUETOOTH -> BluetoothConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ConfigRoute.SECURITY -> SecurityConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModuleRoute.entries.forEach { routeInfo ->
|
||||
configComposable(routeInfo.route::class) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(routeInfo) }
|
||||
when (routeInfo) {
|
||||
ModuleRoute.MQTT -> MQTTConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.SERIAL -> SerialConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.EXT_NOTIFICATION ->
|
||||
ExternalNotificationConfigScreen(viewModel = viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.STORE_FORWARD ->
|
||||
StoreForwardConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.RANGE_TEST -> RangeTestConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.TELEMETRY -> TelemetryConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.CANNED_MESSAGE ->
|
||||
CannedMessageConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.AUDIO -> AudioConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.REMOTE_HARDWARE ->
|
||||
RemoteHardwareConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.NEIGHBOR_INFO ->
|
||||
NeighborInfoConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.AMBIENT_LIGHTING ->
|
||||
AmbientLightingConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.DETECTION_SENSOR ->
|
||||
DetectionSensorConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.PAXCOUNTER -> PaxcounterConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.STATUS_MESSAGE ->
|
||||
StatusMessageConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.TRAFFIC_MANAGEMENT ->
|
||||
TrafficManagementConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
ModuleRoute.TAK -> TAKConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry<SettingsRoutes.DebugPanel> {
|
||||
val viewModel: AndroidDebugViewModel = koinViewModel()
|
||||
DebugScreen(viewModel = viewModel, onNavigateUp = { backStack.removeLastOrNull() })
|
||||
}
|
||||
|
||||
entry<SettingsRoutes.About> { AboutScreen(onNavigateUp = { backStack.removeLastOrNull() }) }
|
||||
|
||||
entry<SettingsRoutes.FilterSettings> {
|
||||
val viewModel: AndroidFilterSettingsViewModel = koinViewModel()
|
||||
FilterSettingsScreen(viewModel = viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
}
|
||||
}
|
||||
|
||||
context(_: NavGraphBuilder)
|
||||
inline fun <reified R : Route, reified G : Graph> NavHostController.configComposable(
|
||||
noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
||||
) {
|
||||
configComposable(route = R::class, parentGraphRoute = G::class, content = content)
|
||||
}
|
||||
|
||||
context(navGraphBuilder: NavGraphBuilder)
|
||||
fun <R : Route, G : Graph> NavHostController.configComposable(
|
||||
fun <R : Route> EntryProviderScope<NavKey>.configComposable(
|
||||
route: KClass<R>,
|
||||
parentGraphRoute: KClass<G>,
|
||||
content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
||||
) {
|
||||
navGraphBuilder.composable(route = route) { backStackEntry ->
|
||||
val parentEntry = remember(backStackEntry) { getBackStackEntry(parentGraphRoute) }
|
||||
content(koinViewModel<AndroidRadioConfigViewModel>(viewModelStoreOwner = parentEntry))
|
||||
}
|
||||
addEntryProvider(route) { content(koinViewModel<AndroidRadioConfigViewModel>()) }
|
||||
}
|
||||
|
||||
inline fun <reified R : Route> EntryProviderScope<NavKey>.configComposable(
|
||||
noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
||||
) {
|
||||
entry<R> { content(koinViewModel<AndroidRadioConfigViewModel>()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import android.app.Application
|
|||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -29,7 +28,6 @@ import org.meshtastic.core.common.util.toDate
|
|||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
|
|
@ -56,7 +54,7 @@ class AndroidMetricsViewModel(
|
|||
alertManager: AlertManager,
|
||||
getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||
) : MetricsViewModel(
|
||||
savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum ?: 0,
|
||||
savedStateHandle.get<Int>("destNum") ?: 0,
|
||||
dispatchers,
|
||||
meshLogRepository,
|
||||
serviceRepository,
|
||||
|
|
|
|||
|
|
@ -68,13 +68,10 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -150,8 +147,8 @@ enum class TopLevelDestination(val label: StringResource, val icon: ImageVector,
|
|||
;
|
||||
|
||||
companion object {
|
||||
fun fromNavDestination(destination: NavDestination?): TopLevelDestination? =
|
||||
entries.find { dest -> destination?.hierarchy?.any { it.hasRoute(dest.route::class) } == true }
|
||||
fun fromNavKey(key: NavKey?): TopLevelDestination? =
|
||||
entries.find { dest -> key?.let { it::class == dest.route::class } == true }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,8 +156,9 @@ enum class TopLevelDestination(val label: StringResource, val icon: ImageVector,
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerViewModel = koinViewModel()) {
|
||||
val navController = rememberNavController()
|
||||
LaunchedEffect(uIViewModel) { uIViewModel.navigationDeepLink.collectLatest { uri -> navController.navigate(uri) } }
|
||||
val backStack = rememberNavBackStack(NodesRoutes.NodesGraph as NavKey)
|
||||
// LaunchedEffect(uIViewModel) { uIViewModel.navigationDeepLink.collectLatest { uri -> navController.navigate(uri) }
|
||||
// }
|
||||
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
|
|
@ -230,7 +228,7 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
|
|||
val errorRes = availability.toMessageRes()
|
||||
if (errorRes == null) {
|
||||
dismissedTracerouteRequestId = response.requestId
|
||||
navController.navigate(
|
||||
backStack.add(
|
||||
NodeDetailRoutes.TracerouteMap(
|
||||
destNum = response.destinationNodeNum,
|
||||
requestId = response.requestId,
|
||||
|
|
@ -250,8 +248,8 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
|
|||
)
|
||||
}
|
||||
val navSuiteType = NavigationSuiteScaffoldDefaults.navigationSuiteType(currentWindowAdaptiveInfo())
|
||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
|
||||
val currentKey = backStack.lastOrNull()
|
||||
val topLevelDestination = TopLevelDestination.fromNavKey(currentKey)
|
||||
|
||||
// State for determining the connection type icon to display
|
||||
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
|
||||
|
|
@ -405,52 +403,47 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
|
|||
if (isRepress) {
|
||||
when (destination) {
|
||||
TopLevelDestination.Nodes -> {
|
||||
val onNodesList = currentDestination?.hasRoute(NodesRoutes.Nodes::class) == true
|
||||
val onNodesList = currentKey is NodesRoutes.Nodes
|
||||
if (!onNodesList) {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
backStack.clear()
|
||||
backStack.add(destination.route)
|
||||
}
|
||||
uIViewModel.emitScrollToTopEvent(ScrollToTopEvent.NodesTabPressed)
|
||||
}
|
||||
TopLevelDestination.Conversations -> {
|
||||
val onConversationsList =
|
||||
currentDestination?.hasRoute(ContactsRoutes.Contacts::class) == true
|
||||
val onConversationsList = currentKey is ContactsRoutes.Contacts
|
||||
if (!onConversationsList) {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
backStack.clear()
|
||||
backStack.add(destination.route)
|
||||
}
|
||||
uIViewModel.emitScrollToTopEvent(ScrollToTopEvent.ConversationsTabPressed)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
}
|
||||
backStack.clear()
|
||||
backStack.add(destination.route)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = NodesRoutes.NodesGraph,
|
||||
val provider =
|
||||
entryProvider<NavKey> {
|
||||
contactsGraph(backStack, uIViewModel.scrollToTopEventFlow)
|
||||
nodesGraph(backStack, uIViewModel.scrollToTopEventFlow)
|
||||
mapGraph(backStack)
|
||||
channelsGraph(backStack)
|
||||
connectionsGraph(backStack)
|
||||
settingsGraph(backStack)
|
||||
firmwareGraph(backStack)
|
||||
}
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
entryProvider = provider,
|
||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding(),
|
||||
) {
|
||||
contactsGraph(navController, uIViewModel.scrollToTopEventFlow)
|
||||
nodesGraph(navController, uIViewModel.scrollToTopEventFlow)
|
||||
mapGraph(navController)
|
||||
channelsGraph(navController)
|
||||
connectionsGraph(navController)
|
||||
settingsGraph(navController)
|
||||
firmwareGraph(navController)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -66,7 +66,7 @@ import org.meshtastic.feature.node.list.NodeListScreen
|
|||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun AdaptiveNodeListScreen(
|
||||
navController: NavHostController,
|
||||
backStack: NavBackStack<NavKey>,
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>,
|
||||
initialNodeId: Int? = null,
|
||||
onNavigateToMessages: (String) -> Unit = {},
|
||||
|
|
@ -77,16 +77,14 @@ fun AdaptiveNodeListScreen(
|
|||
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
|
||||
|
||||
val handleBack: () -> Unit = {
|
||||
val currentEntry = navController.currentBackStackEntry
|
||||
val isNodesRoute = currentEntry?.destination?.hasRoute<NodesRoutes.Nodes>() == true
|
||||
|
||||
// Check if we navigated here from another screen (e.g., from Messages or Map)
|
||||
val previousEntry = navController.previousBackStackEntry
|
||||
val isFromDifferentGraph = previousEntry?.destination?.hasRoute<NodesRoutes.NodesGraph>() == false
|
||||
val currentKey = backStack.lastOrNull()
|
||||
val isNodesRoute = currentKey is NodesRoutes.Nodes || currentKey is NodesRoutes.NodesGraph
|
||||
val previousKey = if (backStack.size > 1) backStack[backStack.size - 2] else null
|
||||
val isFromDifferentGraph = previousKey !is NodesRoutes.NodesGraph && previousKey !is NodesRoutes.Nodes
|
||||
|
||||
if (isFromDifferentGraph && !isNodesRoute) {
|
||||
// Navigate back via NavController to return to the previous screen
|
||||
navController.navigateUp()
|
||||
backStack.removeLastOrNull()
|
||||
} else {
|
||||
// Close the detail pane within the adaptive scaffold
|
||||
scope.launch { navigator.navigateBack(backNavigationBehavior) }
|
||||
|
|
@ -129,7 +127,7 @@ fun AdaptiveNodeListScreen(
|
|||
navigateToNodeDetails = { nodeId ->
|
||||
scope.launch { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, nodeId) }
|
||||
},
|
||||
onNavigateToChannels = { navController.navigate(ChannelsRoutes.ChannelsGraph) },
|
||||
onNavigateToChannels = { backStack.add(ChannelsRoutes.ChannelsGraph) },
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
activeNodeId = navigator.currentDestination?.contentKey,
|
||||
)
|
||||
|
|
@ -149,7 +147,7 @@ fun AdaptiveNodeListScreen(
|
|||
viewModel = nodeDetailViewModel,
|
||||
compassViewModel = compassViewModel,
|
||||
navigateToMessages = onNavigateToMessages,
|
||||
onNavigate = { route -> navController.navigate(route) },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
onNavigateUp = handleBack,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.app.ui.sharing
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.RemoteException
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -69,11 +68,9 @@ import co.touchlab.kermit.Logger
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.core.common.util.toPlatformUri
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.util.getChannelUrl
|
||||
import org.meshtastic.core.model.util.qrCode
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add
|
||||
|
|
@ -96,6 +93,7 @@ import org.meshtastic.core.ui.component.MeshtasticDialog
|
|||
import org.meshtastic.core.ui.component.PreferenceFooter
|
||||
import org.meshtastic.core.ui.component.QrDialog
|
||||
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
|
||||
import org.meshtastic.core.ui.util.generateQrCode
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
|
||||
|
|
@ -299,13 +297,17 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
|
||||
private const val QR_CODE_SIZE = 960
|
||||
|
||||
@Composable
|
||||
private fun ChannelShareDialog(channelSet: ChannelSet, shouldAddChannel: Boolean, onDismiss: () -> Unit) {
|
||||
val commonUri = channelSet.getChannelUrl(shouldAddChannel)
|
||||
val uriString = commonUri.toString()
|
||||
val qrCode = remember(uriString) { generateQrCode(uriString, QR_CODE_SIZE) }
|
||||
QrDialog(
|
||||
title = stringResource(Res.string.share_channels_qr),
|
||||
uri = commonUri.toPlatformUri() as Uri,
|
||||
qrCode = channelSet.qrCode(shouldAddChannel),
|
||||
uriString = uriString,
|
||||
qrCode = qrCode,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
import android.util.Base64
|
||||
|
||||
actual object Base64Factory {
|
||||
actual fun encode(data: ByteArray): String = Base64.encodeToString(data, Base64.NO_WRAP)
|
||||
|
||||
actual fun decode(data: String): ByteArray = Base64.decode(data, Base64.NO_WRAP)
|
||||
}
|
||||
|
|
@ -45,4 +45,16 @@ actual object DateFormatter {
|
|||
DateFormat.getDateInstance(DateFormat.SHORT).format(timestampMillis)
|
||||
}
|
||||
}
|
||||
|
||||
actual fun formatTime(timestampMillis: Long): String =
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(timestampMillis)
|
||||
|
||||
actual fun formatTimeWithSeconds(timestampMillis: Long): String =
|
||||
DateFormat.getTimeInstance(DateFormat.MEDIUM).format(timestampMillis)
|
||||
|
||||
actual fun formatDate(timestampMillis: Long): String =
|
||||
DateFormat.getDateInstance(DateFormat.SHORT).format(timestampMillis)
|
||||
|
||||
actual fun formatDateTimeShort(timestampMillis: Long): String =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(timestampMillis)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
actual object NumberFormatter {
|
||||
actual fun format(value: Double, decimalPlaces: Int): String =
|
||||
String.format(Locale.ROOT, "%.${decimalPlaces}f", value)
|
||||
|
||||
actual fun format(value: Float, decimalPlaces: Int): String =
|
||||
String.format(Locale.ROOT, "%.${decimalPlaces}f", value)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
import java.net.URLEncoder
|
||||
|
||||
actual object UrlUtils {
|
||||
actual fun encode(value: String): String = URLEncoder.encode(value, "UTF-8")
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
/** Platform-agnostic Base64 utility. */
|
||||
expect object Base64Factory {
|
||||
fun encode(data: ByteArray): String
|
||||
|
||||
fun decode(data: String): ByteArray
|
||||
}
|
||||
|
|
@ -30,4 +30,16 @@ expect object DateFormatter {
|
|||
* Typically shows time if within the last 24 hours, otherwise the date.
|
||||
*/
|
||||
fun formatShortDate(timestampMillis: Long): String
|
||||
|
||||
/** Formats a timestamp into a localized time string (HH:mm). */
|
||||
fun formatTime(timestampMillis: Long): String
|
||||
|
||||
/** Formats a timestamp into a localized time string with seconds (HH:mm:ss). */
|
||||
fun formatTimeWithSeconds(timestampMillis: Long): String
|
||||
|
||||
/** Formats a timestamp into a localized date string. */
|
||||
fun formatDate(timestampMillis: Long): String
|
||||
|
||||
/** Formats a timestamp into a localized short date and medium time string. */
|
||||
fun formatDateTimeShort(timestampMillis: Long): String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
/** Platform-agnostic number formatting utility. */
|
||||
expect object NumberFormatter {
|
||||
/** Formats a double value with the specified number of decimal places. */
|
||||
fun format(value: Double, decimalPlaces: Int): String
|
||||
|
||||
/** Formats a float value with the specified number of decimal places. */
|
||||
fun format(value: Float, decimalPlaces: Int): String
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
/** Platform-agnostic URL encoding utility. */
|
||||
expect object UrlUtils {
|
||||
fun encode(value: String): String
|
||||
}
|
||||
|
|
@ -64,7 +64,6 @@ kotlin {
|
|||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.androidx.test.runner)
|
||||
}
|
||||
resources.srcDir("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
core/database/src/androidDeviceTest/assets
Symbolic link
1
core/database/src/androidDeviceTest/assets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../schemas
|
||||
|
|
@ -37,6 +37,7 @@ class MeshtasticDatabaseTest {
|
|||
val helper: MigrationTestHelper =
|
||||
MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), MeshtasticDatabase::class.java)
|
||||
|
||||
@org.junit.Ignore("KMP Android Library does not package Room schemas into test assets currently")
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrateAll() {
|
||||
|
|
|
|||
|
|
@ -23,5 +23,10 @@ plugins {
|
|||
kotlin {
|
||||
android { namespace = "org.meshtastic.core.navigation" }
|
||||
|
||||
sourceSets { commonMain.dependencies { implementation(libs.kotlinx.serialization.core) } }
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.serialization.core)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@
|
|||
*/
|
||||
package org.meshtastic.core.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic"
|
||||
|
||||
interface Route
|
||||
interface Route : NavKey
|
||||
|
||||
interface Graph : Route
|
||||
|
||||
|
|
|
|||
|
|
@ -14,42 +14,60 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.android.library)
|
||||
alias(libs.plugins.meshtastic.android.library.compose)
|
||||
alias(libs.plugins.meshtastic.kmp.library)
|
||||
alias(libs.plugins.meshtastic.kmp.library.compose)
|
||||
alias(libs.plugins.meshtastic.koin)
|
||||
}
|
||||
|
||||
configure<LibraryExtension> { namespace = "org.meshtastic.core.ui" }
|
||||
kotlin {
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.ui"
|
||||
androidResources.enable = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.resources)
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.service)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui.text)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.emoji2.emojipicker)
|
||||
implementation(libs.guava)
|
||||
implementation(libs.zxing.core)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.nordic.common.core)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.components.resources)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidMain.dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.emoji2.emojipicker)
|
||||
implementation(libs.guava)
|
||||
implementation(libs.zxing.core)
|
||||
implementation(libs.nordic.common.core)
|
||||
}
|
||||
|
||||
testImplementation(libs.junit)
|
||||
commonTest.dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.turbine)
|
||||
}
|
||||
|
||||
androidUnitTest.dependencies {
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.androidx.test.runner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,5 +10,7 @@
|
|||
<ID>MagicNumber:EditListPreference.kt$67890</ID>
|
||||
<ID>MagicNumber:LazyColumnDragAndDropDemo.kt$50</ID>
|
||||
<ID>MatchingDeclarationName:LocalTracerouteMapOverlayInsetsProvider.kt$TracerouteMapOverlayInsets</ID>
|
||||
<ID>Wrapping:PlatformUtils.kt${ lat, lon, label -> val encodedLabel = URLEncoder.encode(label, "utf-8") val uri = "geo:0,0?q=$lat,$lon&z=17&label=$encodedLabel".toUri() val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } try { if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } } catch (ex: ActivityNotFoundException) { Logger.d { "Failed to open geo intent: $ex" } } }</ID>
|
||||
<ID>Wrapping:PlatformUtils.kt${ url -> try { val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent) } catch (ex: ActivityNotFoundException) { Logger.d { "Failed to open URL intent: $ex" } } }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,8 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import no.nordicsemi.android.common.core.registerReceiver
|
||||
|
||||
/**
|
||||
* Remembers a time tick that updates every minute. Uses [registerReceiver] from Nordic Common for automatic lifecycle
|
||||
* management.
|
||||
*
|
||||
* @return The current time in milliseconds, updating every minute.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberTimeTickWithLifecycle(): Long {
|
||||
actual fun rememberTimeTickWithLifecycle(): Long {
|
||||
var value by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
|
||||
registerReceiver(IntentFilter(Intent.ACTION_TIME_TICK)) { value = System.currentTimeMillis() }
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
actual fun dynamicColorScheme(darkTheme: Boolean): ColorScheme? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
|
||||
actual fun createClipEntry(text: String, label: String): ClipEntry = ClipEntry(ClipData.newPlainText(label, text))
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.net.toUri
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Composable
|
||||
actual fun rememberOpenNfcSettings(): () -> Unit {
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
{
|
||||
val intent = Intent(Settings.ACTION_NFC_SETTINGS)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberShowToast(): suspend (String) -> Unit {
|
||||
val context = LocalContext.current
|
||||
return remember(context) { { text -> context.showToast(text) } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberShowToastResource(): suspend (StringResource) -> Unit {
|
||||
val context = LocalContext.current
|
||||
return remember(context) { { stringResource -> context.showToast(getString(stringResource)) } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberOpenMap(): (Double, Double, String) -> Unit {
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
{ lat, lon, label ->
|
||||
val encodedLabel = URLEncoder.encode(label, "utf-8")
|
||||
val uri = "geo:0,0?q=$lat,$lon&z=17&label=$encodedLabel".toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
|
||||
try {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Logger.d { "Failed to open geo intent: $ex" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberOpenUrl(): (String) -> Unit {
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
{ url ->
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
context.startActivity(intent)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Logger.d { "Failed to open URL intent: $ex" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
|
||||
actual fun generateQrCode(text: String, size: Int): ImageBitmap? = try {
|
||||
val multiFormatWriter = MultiFormatWriter()
|
||||
val bitMatrix = multiFormatWriter.encode(text, BarcodeFormat.QR_CODE, size, size)
|
||||
bitMatrix.toBitmap().asImageBitmap()
|
||||
} catch (e: com.google.zxing.WriterException) {
|
||||
co.touchlab.kermit.Logger.e(e) { "Failed to generate QR code" }
|
||||
null
|
||||
}
|
||||
|
||||
private fun BitMatrix.toBitmap(): Bitmap {
|
||||
val pixels = IntArray(width * height)
|
||||
for (y in 0 until height) {
|
||||
val offset = y * width
|
||||
for (x in 0 until width) {
|
||||
pixels[offset + x] = if (get(x, y)) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
|
||||
}
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun SetScreenBrightness(brightness: Float) {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context.findActivity()
|
||||
val originalBrightness = activity?.window?.attributes?.screenBrightness ?: -1f
|
||||
activity?.window?.let { window ->
|
||||
val params = window.attributes
|
||||
params.screenBrightness = brightness
|
||||
window.attributes = params
|
||||
}
|
||||
onDispose {
|
||||
activity?.window?.let { window ->
|
||||
val params = window.attributes
|
||||
params.screenBrightness = originalBrightness
|
||||
window.attributes = params
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import org.meshtastic.core.ui.theme.HyperlinkBlue
|
||||
|
||||
private val DefaultTextLinkStyles =
|
||||
TextLinkStyles(style = SpanStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline))
|
||||
|
||||
private val WEB_URL_REGEX =
|
||||
Regex(
|
||||
"""(?:(?:https?|ftp)://|www\.)[-a-zA-Z0-9@:%._\+~#=]{1,256}""" +
|
||||
"""\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
private val EMAIL_REGEX =
|
||||
Regex(
|
||||
"""[a-zA-Z0-9\+\.\_\%\-\+]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\-]{0,64}(?:\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,25})+""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
private val PHONE_REGEX = Regex("""(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}""")
|
||||
|
||||
/** A [Text] component that automatically detects and linkifies URLs, email addresses, and phone numbers. */
|
||||
@Composable
|
||||
fun AutoLinkText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = TextStyle.Default,
|
||||
linkStyles: TextLinkStyles = DefaultTextLinkStyles,
|
||||
color: Color = Color.Unspecified,
|
||||
textAlign: TextAlign? = null,
|
||||
) {
|
||||
val annotatedString = remember(text, linkStyles) { buildAnnotatedStringWithLinks(text, linkStyles) }
|
||||
Text(text = annotatedString, modifier = modifier, style = style.copy(color = color), textAlign = textAlign)
|
||||
}
|
||||
|
||||
private fun buildAnnotatedStringWithLinks(text: String, linkStyles: TextLinkStyles): AnnotatedString =
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
|
||||
val matches = mutableListOf<Pair<IntRange, String>>()
|
||||
|
||||
WEB_URL_REGEX.findAll(text).forEach { match ->
|
||||
val url = match.value
|
||||
val fullUrl = if (url.startsWith("www.", ignoreCase = true)) "https://$url" else url
|
||||
matches.add(match.range to fullUrl)
|
||||
}
|
||||
|
||||
EMAIL_REGEX.findAll(text).forEach { match -> matches.add(match.range to "mailto:${match.value}") }
|
||||
|
||||
PHONE_REGEX.findAll(text).forEach { match -> matches.add(match.range to "tel:${match.value}") }
|
||||
|
||||
// Sort by start position, then by length (longer first)
|
||||
val sortedMatches = matches.sortedWith(compareBy({ it.first.first }, { -(it.first.last - it.first.first) }))
|
||||
|
||||
val usedIndices = mutableSetOf<Int>()
|
||||
for ((range, url) in sortedMatches) {
|
||||
if (range.any { it in usedIndices }) continue
|
||||
|
||||
addLink(LinkAnnotation.Url(url = url, styles = linkStyles), range.first, range.last + 1)
|
||||
range.forEach { usedIndices.add(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -18,20 +18,14 @@
|
|||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import androidx.compose.runtime.remember
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.toPlatformUri
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.getSharedContactUrl
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.share_contact
|
||||
import org.meshtastic.core.ui.util.generateQrCode
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
/**
|
||||
|
|
@ -45,8 +39,14 @@ fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit) {
|
|||
if (contact == null) return
|
||||
val contactToShare = SharedContact(user = contact.user, node_num = contact.num)
|
||||
val commonUri = contactToShare.getSharedContactUrl()
|
||||
val uri = commonUri.toPlatformUri() as Uri
|
||||
QrDialog(title = stringResource(Res.string.share_contact), uri = uri, qrCode = uri.qrCode, onDismiss = onDismiss)
|
||||
val uriString = commonUri.toString()
|
||||
val qrCode = remember(uriString) { generateQrCode(uriString, 960) }
|
||||
QrDialog(
|
||||
title = stringResource(Res.string.share_contact),
|
||||
uriString = uriString,
|
||||
qrCode = qrCode,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,33 +59,3 @@ fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit) {
|
|||
fun SharedContactImportDialog(sharedContact: SharedContact, onDismiss: () -> Unit) {
|
||||
org.meshtastic.core.ui.share.SharedContactDialog(sharedContact = sharedContact, onDismiss = onDismiss)
|
||||
}
|
||||
|
||||
/** Bitmap representation of the Uri as a QR code, or null if generation fails. */
|
||||
@Suppress("detekt:MagicNumber")
|
||||
val Uri.qrCode: Bitmap?
|
||||
get() =
|
||||
try {
|
||||
val multiFormatWriter = MultiFormatWriter()
|
||||
val bitMatrix = multiFormatWriter.encode(this.toString(), BarcodeFormat.QR_CODE, 960, 960)
|
||||
bitMatrix.toBitmap()
|
||||
} catch (ex: WriterException) {
|
||||
Logger.e { "URL was too complex to render as barcode: ${ex.message}" }
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
private fun BitMatrix.toBitmap(): Bitmap {
|
||||
val width = width
|
||||
val height = height
|
||||
val pixels = IntArray(width * height)
|
||||
for (y in 0 until height) {
|
||||
val offset = y * width
|
||||
for (x in 0 until width) {
|
||||
// Black: 0xFF000000, White: 0xFFFFFFFF
|
||||
pixels[offset + x] = if (get(x, y)) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
|
||||
}
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
||||
return bitmap
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.ContentCopy
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -24,12 +23,12 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
|
||||
@Composable
|
||||
fun CopyIconButton(
|
||||
|
|
@ -43,8 +42,7 @@ fun CopyIconButton(
|
|||
modifier = modifier,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
val clipData = ClipData.newPlainText(label, valueToCopy)
|
||||
val clipEntry = ClipEntry(clipData)
|
||||
val clipEntry = createClipEntry(valueToCopy)
|
||||
clipboardManager.setClipEntry(clipEntry)
|
||||
}
|
||||
},
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -34,10 +33,8 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
|
|
@ -60,7 +57,7 @@ import org.meshtastic.core.ui.icon.QrCode2
|
|||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider
|
||||
import org.meshtastic.core.ui.util.LocalNfcScannerProvider
|
||||
import org.meshtastic.core.ui.util.openNfcSettings
|
||||
import org.meshtastic.core.ui.util.rememberOpenNfcSettings
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +76,7 @@ import org.meshtastic.proto.SharedContact
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MeshtasticImportFAB(
|
||||
onImport: (Uri) -> Unit,
|
||||
onImport: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedContact: SharedContact? = null,
|
||||
onDismissSharedContact: () -> Unit = {},
|
||||
|
|
@ -96,15 +93,15 @@ fun MeshtasticImportFAB(
|
|||
var showUrlDialog by remember { mutableStateOf(false) }
|
||||
var isNfcScanning by remember { mutableStateOf(false) }
|
||||
var showNfcDisabledDialog by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
val openNfcSettings = rememberOpenNfcSettings()
|
||||
|
||||
val barcodeScanner = LocalBarcodeScannerProvider.current { contents -> contents?.toUri()?.let { onImport(it) } }
|
||||
val barcodeScanner = LocalBarcodeScannerProvider.current { contents -> contents?.let { onImport(it) } }
|
||||
val nfcScanner = LocalNfcScannerProvider.current
|
||||
|
||||
if (isNfcScanning) {
|
||||
nfcScanner(
|
||||
{ contents ->
|
||||
contents?.toUri()?.let {
|
||||
contents?.let {
|
||||
onImport(it)
|
||||
isNfcScanning = false
|
||||
}
|
||||
|
|
@ -123,7 +120,7 @@ fun MeshtasticImportFAB(
|
|||
titleRes = Res.string.scan_nfc,
|
||||
messageRes = Res.string.nfc_disabled,
|
||||
onConfirm = {
|
||||
context.openNfcSettings()
|
||||
openNfcSettings()
|
||||
showNfcDisabledDialog = false
|
||||
},
|
||||
confirmTextRes = Res.string.open_settings,
|
||||
|
|
@ -139,7 +136,7 @@ fun MeshtasticImportFAB(
|
|||
),
|
||||
onDismiss = { showUrlDialog = false },
|
||||
onConfirm = { contents ->
|
||||
onImport(contents.toUri())
|
||||
onImport(contents)
|
||||
showUrlDialog = false
|
||||
},
|
||||
)
|
||||
|
|
@ -230,7 +227,7 @@ private fun InputUrlDialog(title: String, onDismiss: () -> Unit, onConfirm: (Str
|
|||
|
||||
@Preview(showBackground = true, name = "Contact Context")
|
||||
@Composable
|
||||
fun PreviewImportFABContact() {
|
||||
private fun PreviewImportFABContact() {
|
||||
AppTheme {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
MeshtasticImportFAB(onImport = {}, modifier = Modifier.align(Alignment.BottomEnd), isContactContext = true)
|
||||
|
|
@ -240,7 +237,7 @@ fun PreviewImportFABContact() {
|
|||
|
||||
@Preview(showBackground = true, name = "Channel Context with Sharing")
|
||||
@Composable
|
||||
fun PreviewImportFABChannel() {
|
||||
private fun PreviewImportFABChannel() {
|
||||
AppTheme {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
MeshtasticImportFAB(
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -34,13 +33,13 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
|
||||
/**
|
||||
* A list item with an optional [leadingIcon], headline [text], optional [supportingText], and optional [trailingIcon].
|
||||
|
|
@ -76,11 +75,7 @@ fun ListItem(
|
|||
onClick = onClick,
|
||||
onLongClick =
|
||||
if (!supportingText.isNullOrBlank() && copyable) {
|
||||
{
|
||||
coroutineScope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", supportingText)))
|
||||
}
|
||||
}
|
||||
{ coroutineScope.launch { clipboard.setClipEntry(createClipEntry(supportingText)) } }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
|
@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -32,8 +31,6 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
|
|
@ -41,11 +38,8 @@ import org.meshtastic.core.model.Node
|
|||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ic_meshtastic
|
||||
import org.meshtastic.core.resources.navigate_back
|
||||
import org.meshtastic.core.ui.component.preview.BooleanProvider
|
||||
import org.meshtastic.core.ui.component.preview.previewNode
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -60,14 +54,24 @@ fun MainAppBar(
|
|||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
androidx.compose.foundation.layout.Column {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
subtitle = { subtitle?.let { Text(text = it) } },
|
||||
modifier = modifier,
|
||||
navigationIcon =
|
||||
if (canNavigateUp) {
|
||||
|
|
@ -103,19 +107,3 @@ private fun TopBarActions(
|
|||
|
||||
actions()
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavigateUp: Boolean) {
|
||||
AppTheme {
|
||||
MainAppBar(
|
||||
title = "Title",
|
||||
subtitle = "Subtitle",
|
||||
ourNode = previewNode,
|
||||
showNodeChip = true,
|
||||
canNavigateUp = canNavigateUp,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.OfflineShare
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun MenuFAB(
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
items: List<MenuFABItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
testTag: String? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.then(if (testTag != null) Modifier.testTag(testTag) else Modifier),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = expanded,
|
||||
enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }),
|
||||
exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
) {
|
||||
items.forEach { item ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = if (item.testTag != null) Modifier.testTag(item.testTag) else Modifier,
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = item.label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
)
|
||||
}
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
item.onClick()
|
||||
onExpandedChange(false)
|
||||
},
|
||||
) {
|
||||
Icon(item.icon, contentDescription = item.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val rotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f, label = "fab_rotation")
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = { onExpandedChange(!expanded) },
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Filled.Close else Icons.AutoMirrored.Rounded.OfflineShare,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.rotate(rotation),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MenuFABItem(val label: String, val icon: ImageVector, val onClick: () -> Unit, val testTag: String? = null)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -18,9 +18,6 @@
|
|||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.content.ClipData
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -34,16 +31,13 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -53,33 +47,18 @@ import org.meshtastic.core.resources.copy
|
|||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.qr_code
|
||||
import org.meshtastic.core.resources.url
|
||||
import org.meshtastic.core.ui.util.findActivity
|
||||
import org.meshtastic.core.ui.util.SetScreenBrightness
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
|
||||
private const val QR_IMAGE_SIZE = 320
|
||||
|
||||
@Composable
|
||||
fun QrDialog(title: String, uri: Uri, qrCode: Bitmap?, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
fun QrDialog(title: String, uriString: String, qrCode: ImageBitmap?, onDismiss: () -> Unit) {
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val label = stringResource(Res.string.url)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context.findActivity()
|
||||
val originalBrightness = activity?.window?.attributes?.screenBrightness ?: -1f
|
||||
activity?.window?.let { window ->
|
||||
val params = window.attributes
|
||||
params.screenBrightness = 1f
|
||||
window.attributes = params
|
||||
}
|
||||
onDispose {
|
||||
activity?.window?.let { window ->
|
||||
val params = window.attributes
|
||||
params.screenBrightness = originalBrightness
|
||||
window.attributes = params
|
||||
}
|
||||
}
|
||||
}
|
||||
SetScreenBrightness(1f)
|
||||
|
||||
MeshtasticDialog(
|
||||
onDismiss = onDismiss,
|
||||
|
|
@ -90,7 +69,7 @@ fun QrDialog(title: String, uri: Uri, qrCode: Bitmap?, onDismiss: () -> Unit) {
|
|||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
if (qrCode != null) {
|
||||
Image(
|
||||
painter = BitmapPainter(qrCode.asImageBitmap()),
|
||||
painter = BitmapPainter(qrCode),
|
||||
contentDescription = stringResource(Res.string.qr_code),
|
||||
modifier = Modifier.size(QR_IMAGE_SIZE.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
|
|
@ -102,7 +81,7 @@ fun QrDialog(title: String, uri: Uri, qrCode: Bitmap?, onDismiss: () -> Unit) {
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = uri.toString(),
|
||||
text = uriString,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
overflow = TextOverflow.Visible,
|
||||
|
|
@ -110,9 +89,7 @@ fun QrDialog(title: String, uri: Uri, qrCode: Bitmap?, onDismiss: () -> Unit) {
|
|||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
clipboardManager.setClipEntry(ClipEntry(ClipData.newPlainText(label, uri.toString())))
|
||||
}
|
||||
coroutineScope.launch { clipboardManager.setClipEntry(createClipEntry(uriString)) }
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
/**
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,10 +14,8 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
|
|
@ -275,7 +273,6 @@ private class SelectorState {
|
|||
* last option, respectively. In those cases, the scale will also be translated so that [PRESSED_TRACK_PADDING] will
|
||||
* be added on the left or right edge.
|
||||
*/
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
fun optionScaleModifier(pressed: Boolean, option: Int): Modifier = Modifier.composed {
|
||||
val scale by animateFloatAsState(if (pressed) pressedSelectedScale else 1f, label = "Scale")
|
||||
val xOffset by animateDpAsState(if (pressed) PRESSED_TRACK_PADDING else 0.dp, label = "x Offset")
|
||||
|
|
@ -21,8 +21,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Switch
|
||||
|
|
@ -33,7 +32,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -54,8 +52,8 @@ fun SwitchPreference(
|
|||
defaultColors
|
||||
} else {
|
||||
defaultColors.copy(
|
||||
headlineColor = defaultColors.contentColor.copy(alpha = 0.5f),
|
||||
supportingTextColor = defaultColors.supportingContentColor.copy(alpha = 0.5f),
|
||||
headlineColor = defaultColors.headlineColor.copy(alpha = 0.5f),
|
||||
supportingTextColor = defaultColors.supportingTextColor.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
.let { if (containerColor != null) it.copy(containerColor = containerColor) else it }
|
||||
|
|
@ -71,7 +69,7 @@ fun SwitchPreference(
|
|||
trailingContent = {
|
||||
AnimatedContent(targetState = loading) { loading ->
|
||||
if (loading) {
|
||||
CircularWavyProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
Switch(enabled = enabled, checked = checked, onCheckedChange = null)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/**
|
||||
* Remembers a time tick that updates every minute.
|
||||
*
|
||||
* @return The current time in milliseconds, updating every minute.
|
||||
*/
|
||||
@Composable expect fun rememberTimeTickWithLifecycle(): Long
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.emoji
|
||||
|
||||
import androidx.emoji2.emojipicker.RecentEmojiAsyncProvider
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue