From e6919812074ab432ff8bbeca97f6ddbf5c3a7d62 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:18:23 -0400 Subject: [PATCH] Modularize `ScannedQrCodeDialog` (#3446) --- app/build.gradle.kts | 3 - .../main/java/com/geeksville/mesh/ui/Main.kt | 2 +- .../com/geeksville/mesh/ui/sharing/Channel.kt | 4 +- core/ui/build.gradle.kts | 5 ++ core/ui/detekt-baseline.xml | 3 + .../core/ui/qr}/ScannedQrCodeDialogTest.kt | 3 +- .../core/ui/component/ChannelItem.kt | 62 +++++++++++++++++++ .../core/ui/component/ChannelSelection.kt | 40 ++++++++++++ .../core/ui/qr}/ScannedQrCodeDialog.kt | 4 +- .../core/ui/qr}/ScannedQrCodeViewModel.kt | 2 +- feature/settings/detekt-baseline.xml | 2 - .../component/MapReportingPreferenceTest.kt | 2 - .../component/ChannelSettingsItemList.kt | 51 +-------------- 13 files changed, 118 insertions(+), 65 deletions(-) rename {app/src/androidTest/java/com/geeksville/mesh/compose => core/ui/src/androidTest/kotlin/org/meshtastic/core/ui/qr}/ScannedQrCodeDialogTest.kt (98%) create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ChannelSelection.kt rename {app/src/main/java/com/geeksville/mesh/ui/common/components => core/ui/src/main/kotlin/org/meshtastic/core/ui/qr}/ScannedQrCodeDialog.kt (99%) rename {app/src/main/java/com/geeksville/mesh/ui/common/components => core/ui/src/main/kotlin/org/meshtastic/core/ui/qr}/ScannedQrCodeViewModel.kt (98%) 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()