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:
James Rich 2026-03-10 12:29:47 -05:00 committed by GitHub
parent b1070321fe
commit d076361c55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
245 changed files with 3106 additions and 1748 deletions

View file

@ -36,7 +36,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -47,7 +46,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
@ -57,9 +55,7 @@ import org.meshtastic.core.resources.debug_logs_export
import org.meshtastic.core.resources.debug_search_clear
import org.meshtastic.core.resources.debug_search_next
import org.meshtastic.core.resources.debug_search_prev
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchMatch
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchState
@Composable
@ -234,71 +230,3 @@ fun DebugSearchStateWithViewModel(
onExportLogs = onExportLogs,
)
}
@PreviewLightDark
@Composable
private fun DebugSearchBarEmptyPreview() {
AppTheme {
Surface {
Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
DebugSearchBar(
searchState = SearchState(),
onSearchTextChange = {},
onNextMatch = {},
onPreviousMatch = {},
onClearSearch = {},
)
}
}
}
}
@PreviewLightDark
@Composable
@Suppress("detekt:MagicNumber") // fake data
private fun DebugSearchBarWithTextPreview() {
AppTheme {
Surface {
Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
DebugSearchBar(
searchState =
SearchState(
searchText = "test message",
currentMatchIndex = 2,
allMatches = List(5) { SearchMatch(it, 0, 10, "message") },
hasMatches = true,
),
onSearchTextChange = {},
onNextMatch = {},
onPreviousMatch = {},
onClearSearch = {},
)
}
}
}
}
@PreviewLightDark
@Composable
@Suppress("detekt:MagicNumber") // fake data
private fun DebugSearchBarWithMatchesPreview() {
AppTheme {
Surface {
Row(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
DebugSearchBar(
searchState =
SearchState(
searchText = "error",
currentMatchIndex = 0,
allMatches = List(3) { SearchMatch(it, 0, 5, "message") },
hasMatches = true,
),
onSearchTextChange = {},
onNextMatch = {},
onPreviousMatch = {},
onClearSearch = {},
)
}
}
}
}

View file

@ -18,7 +18,6 @@ package org.meshtastic.feature.settings.radio
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -154,7 +153,7 @@ private fun NodesDeletionPreview(nodesToDelete: List<Node>) {
stringResource(Res.string.nodes_queued_for_deletion, nodesToDelete.size),
modifier = Modifier.padding(bottom = 16.dp),
)
FlowRow(
androidx.compose.foundation.layout.FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.Center,

View file

@ -38,7 +38,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
@ -63,8 +62,6 @@ import org.meshtastic.core.resources.radio_configuration
import org.meshtastic.core.resources.reboot
import org.meshtastic.core.resources.shutdown
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.feature.settings.component.ExpressiveSection
import org.meshtastic.feature.settings.navigation.ConfigRoute
@ -221,31 +218,11 @@ enum class AdminRoute(val icon: ImageVector, val title: StringResource) {
NODEDB_RESET(Icons.Rounded.Storage, Res.string.nodedb_reset),
}
@Preview(showBackground = true)
@Composable
private fun RadioSettingsScreenPreview() = AppTheme {
RadioConfigItemList(
state = RadioConfigState(isLocal = true, connected = true),
isManaged = false,
onNavigate = { _ -> },
)
}
@Composable
private fun ManagedMessage() {
Text(
text = stringResource(Res.string.message_device_managed),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.StatusRed,
)
}
@Preview(showBackground = true)
@Composable
private fun RadioSettingsScreenManagedPreview() = AppTheme {
RadioConfigItemList(
state = RadioConfigState(isLocal = true, connected = true),
isManaged = true,
onNavigate = { _ -> },
color = MaterialTheme.colorScheme.error,
)
}

View file

@ -19,7 +19,6 @@ package org.meshtastic.feature.settings.radio
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -45,7 +44,6 @@ import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Position
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.LocationRepository
@ -126,9 +124,7 @@ open class RadioConfigViewModel(
toggleHomoglyphEncodingUseCase()
}
private val destNum =
savedStateHandle.get<Int>("destNum")
?: runCatching { savedStateHandle.toRoute<SettingsRoutes.Settings>().destNum }.getOrNull()
private val destNum = savedStateHandle.get<Int>("destNum")
private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<Node?>

View file

@ -47,7 +47,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -57,7 +56,6 @@ import org.meshtastic.core.model.Channel
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.add
import org.meshtastic.core.resources.cancel
import org.meshtastic.core.resources.channel_name
import org.meshtastic.core.resources.channels
import org.meshtastic.core.resources.press_and_drag
import org.meshtastic.core.resources.send
@ -301,21 +299,3 @@ private fun determineLocationSharingChannel(capabilities: Capabilities, settings
}
return output
}
@Preview(showBackground = true)
@Composable
private fun ChannelConfigScreenPreview() {
ChannelConfigScreen(
title = "Channels",
onBack = {},
settingsList =
listOf(
ChannelSettings(psk = Channel.default.settings.psk, name = Channel.default.name),
ChannelSettings(name = stringResource(Res.string.channel_name)),
),
loraConfig = Channel.default.loraConfig,
firmwareVersion = "1.3.2",
enabled = true,
onPositiveClicked = {},
)
}

View file

@ -26,14 +26,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.delete
import org.meshtastic.core.ui.component.ChannelItem
import org.meshtastic.core.ui.component.SecurityIcon
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
@ -79,20 +77,3 @@ internal fun ChannelCard(
)
}
}
@Preview
@Composable
private fun ChannelCardPreview() {
AppTheme {
ChannelCard(
index = 0,
title = "Medium Fast",
enabled = true,
channelSettings = ChannelSettings(uplink_enabled = true, downlink_enabled = true),
loraConfig = Config.LoRaConfig(),
onEditClick = {},
onDeleteClick = {},
sharesLocation = true,
)
}
}

View file

@ -24,7 +24,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@ -32,7 +31,6 @@ import org.meshtastic.core.resources.channels
import org.meshtastic.core.resources.freq
import org.meshtastic.core.resources.slot
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.theme.AppTheme
@Composable
internal fun ChannelConfigHeader(frequency: Float, slot: Int) {
@ -48,9 +46,3 @@ internal fun ChannelConfigHeader(frequency: Float, slot: Int) {
}
}
}
@Preview
@Composable
private fun ChannelConfigHeaderPreview() {
AppTheme { ChannelConfigHeader(frequency = 913.125f, slot = 45) }
}

View file

@ -37,7 +37,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
@ -169,9 +168,3 @@ private fun IconDefinitions() {
}
}
}
@Preview
@Composable
private fun PreviewChannelLegendDialog() {
ChannelLegendDialog(capabilities = Capabilities("2.6.10")) {}
}

View file

@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Channel
@ -142,13 +141,3 @@ fun EditChannelDialog(
},
)
}
@Preview(showBackground = true)
@Composable
private fun EditChannelDialogPreview() {
EditChannelDialog(
channelSettings = ChannelSettings(psk = Channel.default.settings.psk, name = Channel.default.name),
onAddClick = {},
onDismissRequest = {},
)
}

View file

@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
@ -130,14 +129,3 @@ fun EditDeviceProfileDialog(
},
)
}
@Preview(showBackground = true)
@Composable
private fun EditDeviceProfileDialogPreview() {
EditDeviceProfileDialog(
title = "Export configuration",
deviceProfile = DeviceProfile(),
onConfirm = {},
onDismiss = {},
)
}

View file

@ -34,7 +34,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.util.DistanceUnit
@ -143,7 +142,6 @@ fun MapReportingPreference(
}
}
@Preview(showBackground = true)
@Composable
fun MapReportingPreview() {
MapReportingPreference(

View file

@ -16,7 +16,6 @@
*/
package org.meshtastic.feature.settings.radio.component
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -37,7 +36,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource
@ -54,13 +52,17 @@ private const val AUTO_DISMISS_DELAY_MS = 1500L
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit = {}, onComplete: () -> Unit = {}) {
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
fun <T> PacketResponseStateDialog(
state: ResponseState<T>,
onDismiss: () -> Unit = {},
onComplete: () -> Unit = {},
onBack: () -> Unit = {},
) {
LaunchedEffect(state) {
if (state is ResponseState.Success) {
delay(AUTO_DISMISS_DELAY_MS)
onDismiss()
backDispatcher?.onBackPressed()
onBack()
}
}
@ -93,7 +95,7 @@ fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit
if (state !is ResponseState.Loading) {
{
onDismiss()
backDispatcher?.onBackPressed()
onBack()
}
} else {
null
@ -176,23 +178,3 @@ private fun ErrorContent(state: ResponseState.Error) {
)
}
}
@Preview(showBackground = true)
@Composable
private fun PacketResponseStateDialogLoadingPreview() {
PacketResponseStateDialog(state = ResponseState.Loading(total = 17, completed = 5))
}
@Preview(showBackground = true)
@Composable
private fun PacketResponseStateDialogSuccessPreview() {
PacketResponseStateDialog(state = ResponseState.Success(Unit))
}
@Preview(showBackground = true)
@Composable
private fun PacketResponseStateDialogErrorPreview() {
PacketResponseStateDialog(
state = ResponseState.Error(org.meshtastic.core.resources.UiText.DynamicString("Failed to send packet")),
)
}

View file

@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Node
@ -40,8 +39,6 @@ import org.meshtastic.core.resources.send
import org.meshtastic.core.resources.shutdown_node_name
import org.meshtastic.core.resources.shutdown_warning
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.User
@Composable
fun ShutdownConfirmationDialog(
@ -91,11 +88,3 @@ private fun ShutdownDialogContent(nodeLongName: String, isShutdown: Boolean) {
}
}
}
@Preview
@Composable
private fun ShutdownConfirmationDialogPreview() {
val mockNode = Node(num = 123, user = User(long_name = "Rooftop Router Node", short_name = "ROOF"))
AppTheme { ShutdownConfirmationDialog(title = "Shutdown?", node = mockNode, onDismiss = {}, onConfirm = {}) }
}

View file

@ -20,13 +20,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.cancel
import org.meshtastic.core.resources.send
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun WarningDialog(
@ -49,9 +47,3 @@ fun WarningDialog(
dismissText = stringResource(Res.string.cancel),
)
}
@Preview
@Composable
private fun WarningDialogPreview() {
AppTheme { WarningDialog(title = "Factory Reset?", onDismiss = {}, onConfirm = {}) }
}