feat(ui): overhaul settings screen with device info and theme selection

Refactored SettingsScreen to display device information and allow user to choose between System, Light, and Dark themes. Updated AppUiState to remove unused diagnostics visibility flag.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-07-07 16:55:33 +02:00
parent 8c84314528
commit 51a7734b38
3 changed files with 578 additions and 103 deletions

View File

@@ -17,7 +17,6 @@ import com.vnidrop.app.core.PickedShareFile
import com.vnidrop.app.core.rememberShareFilePicker
import com.vnidrop.app.core.sharePickedFile
import com.vnidrop.app.logging.AppLogger
import com.vnidrop.app.logging.LogFileInfo
import com.vnidrop.app.platform.PlatformSystemAppearance
import com.vnidrop.app.ui.navigation.AppDestination
import com.vnidrop.app.ui.screens.ReceiveScreen
@@ -39,11 +38,10 @@ fun App() {
val repository = remember { CoreRepository() }
val coreState by repository.state.collectAsState()
var appState by remember { mutableStateOf(AppUiState()) }
var appDataDir by remember { mutableStateOf(platform.defaultCoreDataDir) }
val appDataDir = platform.defaultCoreDataDir
var sendState by remember { mutableStateOf(SendUiState()) }
var receiveState by remember { mutableStateOf(ReceiveUiState(outputDirectory = platform.defaultReceiveDir)) }
var selectedFile by remember { mutableStateOf<PickedShareFile?>(null) }
var logFiles by remember { mutableStateOf<List<LogFileInfo>>(emptyList()) }
val scope = rememberCoroutineScope()
val clipboard = LocalClipboardManager.current
val picker = rememberShareFilePicker(
@@ -51,19 +49,18 @@ fun App() {
AppLogger.info("file-picker", "file selected", mapOf("name" to file.displayName))
selectedFile = file
sendState = sendState.withSelectedFile(file)
logFiles = AppLogger.listLogFiles()
},
onError = { error ->
AppLogger.warn("file-picker", "file picker error", mapOf("reason" to error))
scope.launch { repository.setError(error) }
logFiles = AppLogger.listLogFiles()
},
)
LaunchedEffect(Unit) {
AppLogger.initialize(appDataDir)
AppLogger.info("lifecycle", "app started", mapOf("platform" to platform.name))
logFiles = AppLogger.listLogFiles()
AppLogger.info("core", "automatic initialize requested", mapOf("appDataDir" to appDataDir))
repository.initialize(appDataDir)
}
LaunchedEffect(coreState.lastShare?.transferId) {
@@ -74,14 +71,14 @@ fun App() {
PlatformSystemAppearance(isDarkTheme)
LaunchedEffect(isDarkTheme) {
AppLogger.info("appearance", "system appearance synchronized", mapOf("dark" to isDarkTheme.toString()))
logFiles = AppLogger.listLogFiles()
}
VniDropTheme(isDarkTheme = isDarkTheme) {
BoxWithConstraints {
val windowClass = windowClassFor(maxWidth.value)
AppShell(
selectedDestination = appState.destination,
windowClass = windowClassFor(maxWidth.value),
windowClass = windowClass,
onDestinationSelected = { appState = appState.copy(destination = it) },
) {
when (appState.destination) {
@@ -92,7 +89,6 @@ fun App() {
onSelectFile = {
AppLogger.info("file-picker", "open share file picker")
picker.pickFile()
logFiles = AppLogger.listLogFiles()
},
onCreateShare = {
scope.launch {
@@ -105,13 +101,11 @@ fun App() {
sharePickedFile(repository, file, sendState.transferName, sendState.senderName)
}
sendState = sendState.copy(isSharing = false)
logFiles = AppLogger.listLogFiles()
}
},
onCopyTicket = { ticket ->
AppLogger.info("send", "ticket copied")
clipboard.setText(AnnotatedString(ticket))
logFiles = AppLogger.listLogFiles()
},
onUseLocally = { ticket ->
receiveState = receiveState.copy(ticket = ticket)
@@ -139,33 +133,17 @@ fun App() {
receiveState = receiveState.copy(isReceiving = true)
repository.receive(receiveState.ticket, receiveState.outputDirectory, receiveState.receiverName)
receiveState = receiveState.copy(isReceiving = false)
logFiles = AppLogger.listLogFiles()
}
},
)
AppDestination.Settings -> SettingsScreen(
platformName = platform.name,
appDataDir = appDataDir,
onAppDataDirChange = { appDataDir = it },
deviceInfo = platform.deviceInfo,
coreState = coreState,
themeMode = appState.themeMode,
windowClass = windowClass,
onThemeModeChange = {
AppLogger.info("appearance", "theme mode changed", mapOf("mode" to it.name))
appState = appState.copy(themeMode = it)
logFiles = AppLogger.listLogFiles()
},
diagnosticsVisible = appState.diagnosticsVisible,
onDiagnosticsVisibleChange = { appState = appState.copy(diagnosticsVisible = it) },
logDirectory = AppLogger.logDirectory,
logFiles = logFiles,
onRefreshLogs = { logFiles = AppLogger.listLogFiles() },
onInitialize = {
scope.launch {
AppLogger.initialize(appDataDir)
AppLogger.info("core", "initialize requested", mapOf("appDataDir" to appDataDir))
repository.initialize(appDataDir)
logFiles = AppLogger.listLogFiles()
}
},
)
}

View File

@@ -1,116 +1,614 @@
package com.vnidrop.app.ui.screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vnidrop.app.DeviceInfo
import com.vnidrop.app.core.CoreUiState
import com.vnidrop.app.logging.LogFileInfo
import com.vnidrop.app.ui.components.AppCard
import com.vnidrop.app.ui.components.Field
import com.vnidrop.app.ui.components.MetadataRow
import com.vnidrop.app.ui.components.PrimaryButton
import com.vnidrop.app.ui.components.SecondaryButton
import com.vnidrop.app.ui.state.WindowClass
import com.vnidrop.app.ui.theme.LocalVniDropColors
import com.vnidrop.app.ui.theme.ThemeMode
import org.jetbrains.compose.resources.stringResource
import vnidrop.shared.generated.resources.Res
import vnidrop.shared.generated.resources.about_bug_report
import vnidrop.shared.generated.resources.about_privacy
import vnidrop.shared.generated.resources.about_title
import vnidrop.shared.generated.resources.appearance_auto_description
import vnidrop.shared.generated.resources.appearance_dark_mode
import vnidrop.shared.generated.resources.appearance_light_mode
import vnidrop.shared.generated.resources.appearance_mode_title
import vnidrop.shared.generated.resources.appearance_system_mode
import vnidrop.shared.generated.resources.appearance_title
import vnidrop.shared.generated.resources.button_hide_event_log
import vnidrop.shared.generated.resources.button_initialize_core
import vnidrop.shared.generated.resources.button_refresh_logs
import vnidrop.shared.generated.resources.button_show_event_log
import vnidrop.shared.generated.resources.diagnostics_hint
import vnidrop.shared.generated.resources.diagnostics_title
import vnidrop.shared.generated.resources.field_core_data_directory
import vnidrop.shared.generated.resources.metadata_log_directory
import vnidrop.shared.generated.resources.metadata_platform
import vnidrop.shared.generated.resources.metadata_status
import vnidrop.shared.generated.resources.no_logs
import vnidrop.shared.generated.resources.battery_level_title
import vnidrop.shared.generated.resources.core_status_ready
import vnidrop.shared.generated.resources.device_model_title
import vnidrop.shared.generated.resources.device_name_title
import vnidrop.shared.generated.resources.network_title
import vnidrop.shared.generated.resources.node_title
import vnidrop.shared.generated.resources.not_initialized
import vnidrop.shared.generated.resources.settings_subtitle
import vnidrop.shared.generated.resources.os_version_title
import vnidrop.shared.generated.resources.settings_title
import vnidrop.shared.generated.resources.value_unavailable
import vnidrop.shared.generated.resources.version_title
private enum class SettingsPane {
Overview,
Appearance,
About,
}
@Composable
fun SettingsScreen(
platformName: String,
appDataDir: String,
onAppDataDirChange: (String) -> Unit,
deviceInfo: DeviceInfo,
coreState: CoreUiState,
themeMode: ThemeMode,
windowClass: WindowClass,
onThemeModeChange: (ThemeMode) -> Unit,
) {
var pane by remember { mutableStateOf(SettingsPane.Overview) }
when (windowClass) {
WindowClass.Desktop -> DesktopSettings(
selectedPane = pane,
onPaneSelected = { pane = it },
deviceInfo = deviceInfo,
coreState = coreState,
themeMode = themeMode,
onThemeModeChange = onThemeModeChange,
)
else -> MobileSettings(
pane = pane,
onPaneSelected = { pane = it },
deviceInfo = deviceInfo,
coreState = coreState,
themeMode = themeMode,
onThemeModeChange = onThemeModeChange,
)
}
}
@Composable
private fun MobileSettings(
pane: SettingsPane,
onPaneSelected: (SettingsPane) -> Unit,
deviceInfo: DeviceInfo,
coreState: CoreUiState,
themeMode: ThemeMode,
onThemeModeChange: (ThemeMode) -> Unit,
diagnosticsVisible: Boolean,
onDiagnosticsVisibleChange: (Boolean) -> Unit,
logDirectory: String?,
logFiles: List<LogFileInfo>,
onRefreshLogs: () -> Unit,
onInitialize: () -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
ScreenHeader(stringResource(Res.string.settings_title), stringResource(Res.string.settings_subtitle))
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
ErrorSection(coreState)
AppCard(title = stringResource(Res.string.node_title)) {
MetadataRow(stringResource(Res.string.metadata_platform), platformName)
MetadataRow(stringResource(Res.string.metadata_status), coreState.status)
Field(value = appDataDir, onValueChange = onAppDataDirChange, label = stringResource(Res.string.field_core_data_directory))
PrimaryButton(stringResource(Res.string.button_initialize_core), onClick = onInitialize)
}
AppCard(title = stringResource(Res.string.appearance_title)) {
ThemeMode.entries.forEach { mode ->
ThemeModeRow(
mode = mode,
selected = themeMode == mode,
onSelected = { onThemeModeChange(mode) },
)
}
}
AppCard(title = stringResource(Res.string.diagnostics_title)) {
SecondaryButton(
text = if (diagnosticsVisible) stringResource(Res.string.button_hide_event_log) else stringResource(Res.string.button_show_event_log),
onClick = { onDiagnosticsVisibleChange(!diagnosticsVisible) },
when (pane) {
SettingsPane.Overview -> SettingsOverview(
coreState = coreState,
themeMode = themeMode,
onOpenAppearance = { onPaneSelected(SettingsPane.Appearance) },
onOpenAbout = { onPaneSelected(SettingsPane.About) },
largeTitle = true,
)
SettingsPane.Appearance -> AppearanceSettings(
themeMode = themeMode,
onThemeModeChange = onThemeModeChange,
onBack = { onPaneSelected(SettingsPane.Overview) },
showBack = true,
)
SettingsPane.About -> AboutSettings(
deviceInfo = deviceInfo,
coreState = coreState,
onBack = { onPaneSelected(SettingsPane.Overview) },
showBack = true,
)
SecondaryButton(text = stringResource(Res.string.button_refresh_logs), onClick = onRefreshLogs)
MetadataRow(stringResource(Res.string.metadata_log_directory), logDirectory ?: stringResource(Res.string.not_initialized))
if (logFiles.isEmpty()) {
EmptyText(stringResource(Res.string.no_logs))
} else {
logFiles.forEach { file ->
MetadataRow(file.name, "${file.sizeBytes} bytes")
}
}
EmptyText(stringResource(Res.string.diagnostics_hint))
}
if (diagnosticsVisible) {
DiagnosticsPanel(events = coreState.events)
}
}
}
@Composable
private fun ThemeModeRow(
mode: ThemeMode,
selected: Boolean,
onSelected: () -> Unit,
private fun DesktopSettings(
selectedPane: SettingsPane,
onPaneSelected: (SettingsPane) -> Unit,
deviceInfo: DeviceInfo,
coreState: CoreUiState,
themeMode: ThemeMode,
onThemeModeChange: (ThemeMode) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(24.dp),
verticalAlignment = Alignment.Top,
) {
Column(
modifier = Modifier.widthIn(min = 280.dp, max = 340.dp),
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
SettingsOverview(
coreState = coreState,
themeMode = themeMode,
onOpenAppearance = { onPaneSelected(SettingsPane.Appearance) },
onOpenAbout = { onPaneSelected(SettingsPane.About) },
largeTitle = false,
selectedPane = selectedPane,
)
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
when (selectedPane) {
SettingsPane.Overview,
SettingsPane.Appearance -> AppearanceSettings(
themeMode = themeMode,
onThemeModeChange = onThemeModeChange,
onBack = {},
showBack = false,
)
SettingsPane.About -> AboutSettings(
deviceInfo = deviceInfo,
coreState = coreState,
onBack = {},
showBack = false,
)
}
}
}
}
@Composable
private fun SettingsOverview(
coreState: CoreUiState,
themeMode: ThemeMode,
onOpenAppearance: () -> Unit,
onOpenAbout: () -> Unit,
largeTitle: Boolean,
selectedPane: SettingsPane = SettingsPane.Overview,
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
if (largeTitle) {
SettingsLargeTitle(stringResource(Res.string.settings_title))
} else {
Text(stringResource(Res.string.settings_title), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
}
SettingsGroup {
SettingsRow(
icon = SettingsIcons.Sun,
title = stringResource(Res.string.appearance_title),
value = themeMode.displayName(),
selected = selectedPane == SettingsPane.Appearance,
onClick = onOpenAppearance,
)
}
SettingsGroup {
SettingsRow(
icon = SettingsIcons.Node,
title = stringResource(Res.string.node_title),
value = if (coreState.isInitialized) stringResource(Res.string.core_status_ready) else stringResource(Res.string.not_initialized),
iconTone = IconTone.Neutral,
)
SettingsRow(
icon = SettingsIcons.Info,
title = stringResource(Res.string.about_title),
selected = selectedPane == SettingsPane.About,
onClick = onOpenAbout,
)
}
}
}
@Composable
private fun AppearanceSettings(
themeMode: ThemeMode,
onThemeModeChange: (ThemeMode) -> Unit,
onBack: () -> Unit,
showBack: Boolean,
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
SettingsTopBar(title = stringResource(Res.string.appearance_title), onBack = onBack, showBack = showBack)
Text(
text = stringResource(Res.string.appearance_mode_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
)
ThemeChoice(
icon = SettingsIcons.Device,
title = stringResource(Res.string.appearance_system_mode),
description = stringResource(Res.string.appearance_auto_description),
selected = themeMode == ThemeMode.System,
onClick = { onThemeModeChange(ThemeMode.System) },
)
ThemeChoice(
icon = SettingsIcons.Moon,
title = stringResource(Res.string.appearance_dark_mode),
selected = themeMode == ThemeMode.Dark,
onClick = { onThemeModeChange(ThemeMode.Dark) },
)
ThemeChoice(
icon = SettingsIcons.Sun,
title = stringResource(Res.string.appearance_light_mode),
selected = themeMode == ThemeMode.Light,
onClick = { onThemeModeChange(ThemeMode.Light) },
)
}
}
@Composable
private fun AboutSettings(
deviceInfo: DeviceInfo,
coreState: CoreUiState,
onBack: () -> Unit,
showBack: Boolean,
) {
val unavailable = stringResource(Res.string.value_unavailable)
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
SettingsTopBar(title = stringResource(Res.string.about_title), onBack = onBack, showBack = showBack)
SettingsGroup {
SettingsRow(icon = SettingsIcons.Document, title = stringResource(Res.string.about_privacy), iconTone = IconTone.Neutral)
SettingsRow(icon = SettingsIcons.Bug, title = stringResource(Res.string.about_bug_report), iconTone = IconTone.Neutral)
}
PlainInfoSection {
PlainInfoItem(stringResource(Res.string.version_title), "0.1.0")
PlainInfoItem(stringResource(Res.string.device_name_title), deviceInfo.deviceName.orUnavailable(unavailable))
PlainInfoItem(stringResource(Res.string.device_model_title), deviceInfo.deviceModel.orUnavailable(unavailable))
PlainInfoItem(stringResource(Res.string.os_version_title), deviceInfo.operatingSystem)
PlainInfoItem(stringResource(Res.string.network_title), deviceInfo.network.orUnavailable(unavailable))
PlainInfoItem(stringResource(Res.string.battery_level_title), deviceInfo.batteryLevel.orUnavailable(unavailable))
PlainInfoItem(
title = stringResource(Res.string.node_title),
value = if (coreState.isInitialized) {
stringResource(Res.string.core_status_ready)
} else {
stringResource(Res.string.not_initialized)
},
)
}
}
}
@Composable
private fun SettingsLargeTitle(title: String) {
Text(
text = title,
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
)
}
@Composable
private fun SettingsTopBar(title: String, onBack: () -> Unit, showBack: Boolean) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
if (showBack) {
Icon(
imageVector = SettingsIcons.Back,
contentDescription = null,
tint = LocalVniDropColors.current.foregroundDefault,
modifier = Modifier
.size(30.dp)
.clickable(onClick = onBack)
.padding(3.dp),
)
}
Text(title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
}
}
@Composable
private fun SettingsGroup(content: @Composable ColumnScope.() -> Unit) {
val colors = LocalVniDropColors.current
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = colors.backgroundSurface200),
) {
Column(modifier = Modifier.padding(vertical = 4.dp), content = content)
}
}
@Composable
private fun PlainInfoSection(content: @Composable ColumnScope.() -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(18.dp),
content = content,
)
}
@Composable
private fun PlainInfoItem(title: String, value: String) {
val colors = LocalVniDropColors.current
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = title,
color = colors.foregroundLighter,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Medium,
)
Text(
text = value,
color = colors.foregroundDefault,
style = MaterialTheme.typography.bodyLarge,
)
}
}
private fun String?.orUnavailable(fallback: String): String =
this?.takeIf { it.isNotBlank() } ?: fallback
@Composable
private fun SettingsRow(
icon: ImageVector,
title: String,
value: String? = null,
selected: Boolean = false,
iconTone: IconTone = IconTone.Brand,
onClick: (() -> Unit)? = null,
) {
val colors = LocalVniDropColors.current
val iconColor = when (iconTone) {
IconTone.Brand -> colors.brandLink
IconTone.Neutral -> colors.foregroundLighter
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.selectable(selected = selected, onClick = onSelected)
.padding(vertical = 8.dp),
.height(60.dp)
.background(if (selected) colors.backgroundSurface300 else Color.Transparent)
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected = selected, onClick = onSelected)
Text(mode.name)
Icon(icon, contentDescription = null, tint = iconColor, modifier = Modifier.size(22.dp))
Spacer(Modifier.width(14.dp))
Text(
text = title,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
value?.let {
Text(
text = it,
color = colors.foregroundLighter,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(Modifier.width(10.dp))
}
if (onClick != null) {
Icon(SettingsIcons.ChevronRight, contentDescription = null, tint = colors.foregroundLighter, modifier = Modifier.size(20.dp))
}
}
}
@Composable
private fun ThemeChoice(
icon: ImageVector,
title: String,
selected: Boolean,
onClick: () -> Unit,
description: String? = null,
) {
val colors = LocalVniDropColors.current
val shape = RoundedCornerShape(18.dp)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(62.dp)
.background(colors.backgroundSurface200, shape)
.border(
border = if (selected) BorderStroke(1.5.dp, colors.brandLink) else BorderStroke(1.dp, Color.Transparent),
shape = shape,
)
.clickable(onClick = onClick)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(icon, contentDescription = null, tint = colors.foregroundLight, modifier = Modifier.size(22.dp))
Spacer(Modifier.width(14.dp))
Text(
text = title,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
)
if (selected) {
Icon(SettingsIcons.Check, contentDescription = null, tint = colors.brandLink, modifier = Modifier.size(24.dp))
}
}
description?.let {
Text(
text = it,
color = colors.foregroundLighter,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
@Composable
private fun ThemeMode.displayName(): String =
when (this) {
ThemeMode.System -> stringResource(Res.string.appearance_system_mode)
ThemeMode.Light -> stringResource(Res.string.appearance_light_mode)
ThemeMode.Dark -> stringResource(Res.string.appearance_dark_mode)
}
private enum class IconTone {
Brand,
Neutral,
}
private object SettingsIcons {
val ChevronRight = lineIcon("ChevronRight") {
moveTo(9f, 18f)
lineTo(15f, 12f)
lineTo(9f, 6f)
}
val Back = lineIcon("Back") {
moveTo(19f, 12f)
lineTo(5f, 12f)
moveTo(12f, 19f)
lineTo(5f, 12f)
lineTo(12f, 5f)
}
val Check = lineIcon("Check") {
moveTo(20f, 6f)
lineTo(9f, 17f)
lineTo(4f, 12f)
}
val Sun = lineIcon("Sun") {
moveTo(12f, 4f)
lineTo(12f, 2f)
moveTo(12f, 22f)
lineTo(12f, 20f)
moveTo(4.93f, 4.93f)
lineTo(6.34f, 6.34f)
moveTo(17.66f, 17.66f)
lineTo(19.07f, 19.07f)
moveTo(2f, 12f)
lineTo(4f, 12f)
moveTo(20f, 12f)
lineTo(22f, 12f)
moveTo(4.93f, 19.07f)
lineTo(6.34f, 17.66f)
moveTo(17.66f, 6.34f)
lineTo(19.07f, 4.93f)
moveTo(16f, 12f)
arcTo(4f, 4f, 0f, true, true, 8f, 12f)
arcTo(4f, 4f, 0f, true, true, 16f, 12f)
}
val Moon = lineIcon("Moon") {
moveTo(21f, 12.79f)
arcTo(9f, 9f, 0f, true, true, 11.21f, 3f)
arcTo(7f, 7f, 0f, false, false, 21f, 12.79f)
}
val Device = lineIcon("Device") {
roundRect(7f, 2f, 10f, 20f, 2.5f)
moveTo(11f, 18f)
lineTo(13f, 18f)
}
val Info = lineIcon("Info") {
moveTo(12f, 16f)
lineTo(12f, 12f)
moveTo(12f, 8f)
lineTo(12.01f, 8f)
moveTo(21f, 12f)
arcTo(9f, 9f, 0f, true, true, 3f, 12f)
arcTo(9f, 9f, 0f, true, true, 21f, 12f)
}
val Bug = lineIcon("Bug") {
moveTo(8f, 2f)
lineTo(9.88f, 3.88f)
moveTo(16f, 2f)
lineTo(14.12f, 3.88f)
roundRect(7f, 6f, 10f, 14f, 5f)
moveTo(3f, 10f)
lineTo(7f, 10f)
moveTo(17f, 10f)
lineTo(21f, 10f)
moveTo(3f, 16f)
lineTo(7f, 16f)
moveTo(17f, 16f)
lineTo(21f, 16f)
moveTo(12f, 6f)
lineTo(12f, 20f)
}
val Document = lineIcon("Document") {
moveTo(14f, 2f)
lineTo(6f, 2f)
arcTo(2f, 2f, 0f, false, false, 4f, 4f)
lineTo(4f, 20f)
arcTo(2f, 2f, 0f, false, false, 6f, 22f)
lineTo(18f, 22f)
arcTo(2f, 2f, 0f, false, false, 20f, 20f)
lineTo(20f, 8f)
lineTo(14f, 2f)
moveTo(14f, 2f)
lineTo(14f, 8f)
lineTo(20f, 8f)
moveTo(8f, 13f)
lineTo(16f, 13f)
moveTo(8f, 17f)
lineTo(16f, 17f)
}
val Node = lineIcon("Node") {
roundRect(4f, 4f, 16f, 16f, 3f)
moveTo(9f, 9f)
lineTo(15f, 9f)
moveTo(9f, 13f)
lineTo(15f, 13f)
moveTo(9f, 17f)
lineTo(12f, 17f)
}
}
private fun lineIcon(name: String, block: androidx.compose.ui.graphics.vector.PathBuilder.() -> Unit): ImageVector =
ImageVector.Builder(name, 24.dp, 24.dp, 24f, 24f).apply {
path(
fill = SolidColor(Color.Transparent),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f,
strokeLineCap = StrokeCap.Round,
strokeLineJoin = StrokeJoin.Round,
pathFillType = PathFillType.NonZero,
pathBuilder = block,
)
}.build()
private fun androidx.compose.ui.graphics.vector.PathBuilder.roundRect(x: Float, y: Float, width: Float, height: Float, radius: Float) {
moveTo(x + radius, y)
lineTo(x + width - radius, y)
arcTo(radius, radius, 0f, false, true, x + width, y + radius)
lineTo(x + width, y + height - radius)
arcTo(radius, radius, 0f, false, true, x + width - radius, y + height)
lineTo(x + radius, y + height)
arcTo(radius, radius, 0f, false, true, x, y + height - radius)
lineTo(x, y + radius)
arcTo(radius, radius, 0f, false, true, x + radius, y)
}

View File

@@ -25,7 +25,6 @@ fun useBottomNavigation(windowClass: WindowClass): Boolean =
data class AppUiState(
val destination: AppDestination = AppDestination.Send,
val themeMode: ThemeMode = ThemeMode.System,
val diagnosticsVisible: Boolean = false,
)
data class SendUiState(