diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt index ac16394..71af790 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt @@ -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(null) } - var logFiles by remember { mutableStateOf>(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() - } }, ) } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/screens/SettingsScreen.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/screens/SettingsScreen.kt index b13ae83..88c1b67 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/screens/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/screens/SettingsScreen.kt @@ -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, - 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) +} diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt index ad3fd5c..8f4bcaa 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt @@ -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(