mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
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:
@@ -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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
when (pane) {
|
||||
SettingsPane.Overview -> SettingsOverview(
|
||||
coreState = coreState,
|
||||
themeMode = themeMode,
|
||||
onOpenAppearance = { onPaneSelected(SettingsPane.Appearance) },
|
||||
onOpenAbout = { onPaneSelected(SettingsPane.About) },
|
||||
largeTitle = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
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) },
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user