diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 38ea3abce..268948990 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -226,7 +226,6 @@ dependencies {
implementation(libs.coil.svg)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
implementation(libs.zxing.android.embedded) { isTransitive = false }
- implementation(libs.zxing.core)
implementation(libs.androidx.core.splashscreen)
implementation(libs.kotlinx.serialization.json)
implementation(libs.org.eclipse.paho.client.mqttv3)
@@ -243,11 +242,9 @@ dependencies {
fdroidImplementation(libs.osmdroid.android)
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
- androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.hilt.android.testing)
- testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.junit)
dokkaPlugin(libs.dokka.android.documentation.plugin)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index dce8363ba..08e59c7ed 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -93,7 +93,6 @@ import com.geeksville.mesh.navigation.mapGraph
import com.geeksville.mesh.navigation.nodesGraph
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.MeshService
-import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.ConnectionsNavIcon
import com.geeksville.mesh.ui.metrics.annotateTraceroute
@@ -118,6 +117,7 @@ import org.meshtastic.core.ui.icon.Map
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.core.ui.icon.Settings
+import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
index eae8e27dd..19efd669e 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
@@ -89,7 +89,6 @@ import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -104,12 +103,13 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.AdaptiveTwoPane
+import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.PreferenceFooter
+import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
-import org.meshtastic.feature.settings.radio.component.ChannelSelection
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ChannelProtos
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 999ce4f9b..78d30c693 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -44,4 +44,9 @@ dependencies {
implementation(libs.zxing.core)
implementation(libs.zxing.android.embedded)
implementation(libs.timber)
+
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
+
+ androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+ androidTestImplementation(libs.androidx.test.runner)
}
diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index 22891816e..259bc856c 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -23,6 +23,8 @@
MagicNumber:EditListPreference.kt$67890
MagicNumber:LazyColumnDragAndDropDemo.kt$50
ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane
+ ModifierMissing:ChannelItem.kt$ChannelItem
+ ModifierMissing:ChannelSelection.kt$ChannelSelection
ModifierMissing:ContactSharing.kt$SharedContactDialog
ModifierMissing:EmojiPicker.kt$EmojiPicker
ModifierMissing:EmojiPicker.kt$EmojiPickerDialog
@@ -48,6 +50,7 @@
ModifierReused:TextDividerPreference.kt$Row(modifier = modifier.fillMaxWidth().padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon(trailingIcon, "trailingIcon", modifier = modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) } }
MultipleEmitters:PreferenceCategory.kt$PreferenceCategory
ParameterNaming:BitwisePreference.kt$onItemSelected
+ ParameterNaming:ChannelSelection.kt$onSelected
ParameterNaming:ContactSharing.kt$onSharedContactRequested
ParameterNaming:DropDownPreference.kt$onItemSelected
ParameterNaming:EditIPv4Preference.kt$onValueChanged
diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt b/core/ui/src/androidTest/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialogTest.kt
similarity index 98%
rename from app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt
rename to core/ui/src/androidTest/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialogTest.kt
index 3d06e5cce..a0dfdd6f5 100644
--- a/app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt
+++ b/core/ui/src/androidTest/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialogTest.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.compose
+package org.meshtastic.core.ui.qr
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
@@ -23,7 +23,6 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
-import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
new file mode 100644
index 000000000..94a9a84d1
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2025 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 .
+ */
+
+package org.meshtastic.core.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ChannelItem(
+ index: Int,
+ title: String,
+ enabled: Boolean,
+ onClick: () -> Unit = {},
+ content: @Composable RowScope.() -> Unit,
+) {
+ val fontColor = if (index == 0) MaterialTheme.colorScheme.primary else Color.Unspecified
+ Card(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp).clickable(enabled = enabled) { onClick() }) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp),
+ ) {
+ AssistChip(onClick = onClick, label = { Text(text = "$index", color = fontColor) })
+ Text(
+ text = title,
+ modifier = Modifier.weight(1f),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ style = MaterialTheme.typography.bodyLarge,
+ color = fontColor,
+ )
+ content()
+ }
+ }
+}
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelSelection.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelSelection.kt
new file mode 100644
index 000000000..0f99a2379
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelSelection.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025 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 .
+ */
+
+package org.meshtastic.core.ui.component
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Checkbox
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.meshtastic.core.model.Channel
+
+@Composable
+fun ChannelSelection(
+ index: Int,
+ title: String,
+ enabled: Boolean,
+ isSelected: Boolean,
+ onSelected: (Boolean) -> Unit,
+ channel: Channel,
+) = ChannelItem(index = index, title = title, enabled = enabled) {
+ SecurityIcon(channel)
+ Spacer(modifier = Modifier.width(10.dp))
+ Checkbox(enabled = enabled, checked = isSelected, onCheckedChange = onSelected)
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
similarity index 99%
rename from app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
index 7260e6297..5c68d0dec 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.common.components
+package org.meshtastic.core.ui.qr
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -53,7 +53,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
-import org.meshtastic.feature.settings.radio.component.ChannelSelection
+import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
import org.meshtastic.proto.channelSet
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt
similarity index 98%
rename from app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt
rename to core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt
index 99a8d0efa..0636464bd 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeViewModel.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.ui.common.components
+package org.meshtastic.core.ui.qr
import android.os.RemoteException
import androidx.lifecycle.ViewModel
diff --git a/feature/settings/detekt-baseline.xml b/feature/settings/detekt-baseline.xml
index fdf5301fa..e763187c5 100644
--- a/feature/settings/detekt-baseline.xml
+++ b/feature/settings/detekt-baseline.xml
@@ -33,7 +33,6 @@
MagicNumber:EditChannelDialog.kt$16
MagicNumber:EditChannelDialog.kt$32
MagicNumber:PacketResponseStateDialog.kt$100
- ModifierMissing:ChannelSettingsItemList.kt$ChannelSelection
ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen
ModifierMissing:MapReportingPreference.kt$MapReportingPreference
ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen
@@ -48,7 +47,6 @@
MultipleEmitters:RadioConfig.kt$RadioConfigItemList
NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)
ParameterNaming:ChannelSettingsItemList.kt$onPositiveClicked
- ParameterNaming:ChannelSettingsItemList.kt$onSelected
ParameterNaming:CleanNodeDatabaseScreen.kt$onCheckedChanged
ParameterNaming:CleanNodeDatabaseScreen.kt$onDaysChanged
ParameterNaming:MapReportingPreference.kt$onMapReportingEnabledChanged
diff --git a/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt b/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt
index e6ce58fae..c63da2c4a 100644
--- a/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt
+++ b/feature/settings/src/androidTest/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreferenceTest.kt
@@ -18,7 +18,6 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.layout.Column
-import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
@@ -61,7 +60,6 @@ class MapReportingPreferenceTest {
publishIntervalSecs = positionReportingInterval,
onPublishIntervalSecsChanged = positionReportingIntervalChanged,
enabled = true,
- focusManager = LocalFocusManager.current,
)
}
}
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ChannelSettingsItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ChannelSettingsItemList.kt
index 17b089b26..a0b91b319 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ChannelSettingsItemList.kt
+++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ChannelSettingsItemList.kt
@@ -22,13 +22,11 @@ import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -40,13 +38,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Add
import androidx.compose.material.icons.twotone.Close
-import androidx.compose.material3.AssistChip
-import androidx.compose.material3.Card
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -59,11 +53,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -72,6 +64,7 @@ import androidx.navigation.NavController
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.ChannelItem
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.PreferenceFooter
import org.meshtastic.core.ui.component.SecurityIcon
@@ -83,34 +76,6 @@ import org.meshtastic.proto.ChannelProtos.ChannelSettings
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
import org.meshtastic.proto.channelSettings
-@Composable
-private fun ChannelItem(
- index: Int,
- title: String,
- enabled: Boolean,
- onClick: () -> Unit = {},
- content: @Composable RowScope.() -> Unit,
-) {
- val fontColor = if (index == 0) MaterialTheme.colorScheme.primary else Color.Unspecified
- Card(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp).clickable(enabled = enabled) { onClick() }) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp),
- ) {
- AssistChip(onClick = onClick, label = { Text(text = "$index", color = fontColor) })
- Text(
- text = title,
- modifier = Modifier.weight(1f),
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- style = MaterialTheme.typography.bodyLarge,
- color = fontColor,
- )
- content()
- }
- }
-}
-
@Composable
private fun ChannelCard(
index: Int,
@@ -154,20 +119,6 @@ private fun ChannelCard(
}
}
-@Composable
-fun ChannelSelection(
- index: Int,
- title: String,
- enabled: Boolean,
- isSelected: Boolean,
- onSelected: (Boolean) -> Unit,
- channel: Channel,
-) = ChannelItem(index = index, title = title, enabled = enabled) {
- SecurityIcon(channel)
- Spacer(modifier = Modifier.width(10.dp))
- Checkbox(enabled = enabled, checked = isSelected, onCheckedChange = onSelected)
-}
-
@Composable
fun ChannelConfigScreen(navController: NavController, viewModel: RadioConfigViewModel) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()