From b7bff1a7bd3170c8c6e288445b93d8b054ac4bef Mon Sep 17 00:00:00 2001 From: Rukira Date: Tue, 25 Aug 2026 11:32:19 +0100 Subject: [PATCH] Sticky buttons in Settings --- .../wowbackup/ui/config/ConfigScreen.kt | 416 +++++++++--------- 1 file changed, 211 insertions(+), 205 deletions(-) diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigScreen.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigScreen.kt index 63922c0..95dd10d 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigScreen.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigScreen.kt @@ -63,214 +63,234 @@ fun ConfigScreen( var showForceBackupDialog by remember { mutableStateOf(false) } val scrollState = rememberScrollState() - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(24.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text("Settings", style = MaterialTheme.typography.headlineMedium) + Column(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text("Settings", style = MaterialTheme.typography.headlineMedium) - // === WoW Installation === - SectionHeader("WoW Installation") + // === WoW Installation === + SectionHeader("WoW Installation") - PathField( - label = "WoW install location", - value = config.wowInstallPath ?: "", - error = errors["wowPath"], - onBrowse = { - val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) }) - if (dir != null) viewModel.updateWoWPath(dir.absolutePath) - }, - ) + PathField( + label = "WoW install location", + value = config.wowInstallPath ?: "", + error = errors["wowPath"], + onBrowse = { + val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) }) + if (dir != null) viewModel.updateWoWPath(dir.absolutePath) + }, + ) - // WoW path warnings (e.g. missing WTF/Interface, permission issues) - uiState.warnings["wowPath"]?.forEach { warning -> - Text( - warning, - color = MaterialTheme.colorScheme.tertiary, - style = MaterialTheme.typography.bodySmall, - ) - } - - // WoW validation summary when valid - uiState.wowValidation?.let { validation -> - if (validation.isValid) { - val wtfStatus = when { - validation.wtfReadable -> "WTF folder: found" - validation.hasWtf -> "WTF folder: found (not readable)" - else -> "WTF folder: not found" + // WoW path warnings (e.g. missing WTF/Interface, permission issues) + uiState.warnings["wowPath"]?.forEach { warning -> + Text( + warning, + color = MaterialTheme.colorScheme.tertiary, + style = MaterialTheme.typography.bodySmall, + ) } - val interfaceStatus = when { - validation.interfaceReadable -> "Interface folder: found" - validation.hasInterface -> "Interface folder: found (not readable)" - else -> "Interface folder: not found" + + // WoW validation summary when valid + uiState.wowValidation?.let { validation -> + if (validation.isValid) { + val wtfStatus = when { + validation.wtfReadable -> "WTF folder: found" + validation.hasWtf -> "WTF folder: found (not readable)" + else -> "WTF folder: not found" + } + val interfaceStatus = when { + validation.interfaceReadable -> "Interface folder: found" + validation.hasInterface -> "Interface folder: found (not readable)" + else -> "Interface folder: not found" + } + Text( + "$wtfStatus | $interfaceStatus", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } + + OutlinedButton(onClick = { viewModel.detectWoWLocation() }) { + Text("Auto-detect") + } + + // === Backup Settings === + SectionHeader("Backup Settings") + + PathField( + label = "Backup destination", + value = config.backupPath ?: "", + error = errors["backupPath"], + onBrowse = { + val dir = pickFolder("Select Backup Destination", config.backupPath?.let { File(it) }) + if (dir != null) viewModel.updateBackupPath(dir.absolutePath) + }, + ) + + Text("Daily backup time", style = MaterialTheme.typography.bodyMedium) + TimePicker( + value = config.backupTimeOfDay, + onValueChange = { viewModel.updateBackupTime(it) }, + modifier = Modifier.width(240.dp), + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Backups to keep:", style = MaterialTheme.typography.bodyMedium) + Spacer(Modifier.width(8.dp)) + HistoryCountSelector( + value = config.backupHistoryCount, + onValueChange = { viewModel.updateBackupHistoryCount(it) }, + ) + } + + CheckboxRow( + checked = config.forceBackupWhileRunning, + onCheckedChange = { checked -> + if (checked) { + showForceBackupDialog = true + } else { + viewModel.updateForceBackup(false) + } + }, + label = "Allow backup while WoW is running", + ) + + // === Folders to Backup === + SectionHeader("Folders to Backup") + + if (errors.containsKey("folders")) { + Text(errors["folders"]!!, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) + } + + CheckboxRow( + checked = config.backupWtf, + onCheckedChange = { viewModel.updateBackupWtf(it) }, + label = "WTF", + description = "Contains account settings, keybinds, macros, and addon saved variables.", + ) + + CheckboxRow( + checked = config.backupInterface, + onCheckedChange = { viewModel.updateBackupInterface(it) }, + label = "Interface", + description = "Contains installed addons.", + ) + + // === Options === + SectionHeader("Options") + + CheckboxRow( + checked = config.compressionEnabled, + onCheckedChange = { viewModel.updateCompression(it) }, + label = "Compression", + description = "Compress backups to save disk space. Slightly slower backup/restore.", + ) + + CheckboxRow( + checked = config.notificationsEnabled, + onCheckedChange = { viewModel.updateNotifications(it) }, + label = "Notifications", + description = "Show system notifications for backup events.", + ) + + CheckboxRow( + checked = config.runAtStartup, + onCheckedChange = { viewModel.updateRunAtStartup(it) }, + label = "Run at startup", + description = "Launch WoW Backup automatically when you log in.", + ) + + // === Appearance === + SectionHeader("Appearance") + + Text("Theme", style = MaterialTheme.typography.bodyMedium) + SingleChoiceSegmentedButtonRow { + ThemeMode.entries.forEachIndexed { index, mode -> + SegmentedButton( + selected = config.themeMode == mode, + onClick = { viewModel.updateThemeMode(mode) }, + shape = SegmentedButtonDefaults.itemShape(index, ThemeMode.entries.size), + ) { + Text( + when (mode) { + ThemeMode.SYSTEM -> "System" + ThemeMode.LIGHT -> "Light" + ThemeMode.DARK -> "Dark" + }, + ) + } + } + } + + Text("Accent color", style = MaterialTheme.typography.bodyMedium) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AccentColor.entries.forEach { color -> + val isSelected = config.accentColor == color + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(color.seedColor, CircleShape) + .then( + if (isSelected) { + Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape) + } else { + Modifier + }, + ) + .clickable { viewModel.updateAccentColor(color) }, + contentAlignment = Alignment.Center, + ) { + if (isSelected) { + Text( + "\u2713", + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.labelLarge, + ) + } + } + } + } + Text( - "$wtfStatus | $interfaceStatus", + "Theme changes apply immediately.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } - OutlinedButton(onClick = { viewModel.detectWoWLocation() }) { - Text("Auto-detect") - } - - // === Backup Settings === - SectionHeader("Backup Settings") - - PathField( - label = "Backup destination", - value = config.backupPath ?: "", - error = errors["backupPath"], - onBrowse = { - val dir = pickFolder("Select Backup Destination", config.backupPath?.let { File(it) }) - if (dir != null) viewModel.updateBackupPath(dir.absolutePath) - }, - ) - - Text("Daily backup time", style = MaterialTheme.typography.bodyMedium) - TimePicker( - value = config.backupTimeOfDay, - onValueChange = { viewModel.updateBackupTime(it) }, - modifier = Modifier.width(240.dp), - ) - - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Backups to keep:", style = MaterialTheme.typography.bodyMedium) - Spacer(Modifier.width(8.dp)) - HistoryCountSelector( - value = config.backupHistoryCount, - onValueChange = { viewModel.updateBackupHistoryCount(it) }, + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), + adapter = rememberScrollbarAdapter(scrollState), + style = ScrollbarStyle( + minimalHeight = 48.dp, + thickness = 8.dp, + shape = RoundedCornerShape(4.dp), + hoverDurationMillis = 300, + unhoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), + hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + ), ) } - CheckboxRow( - checked = config.forceBackupWhileRunning, - onCheckedChange = { checked -> - if (checked) { - showForceBackupDialog = true - } else { - viewModel.updateForceBackup(false) - } - }, - label = "Allow backup while WoW is running", - ) - - // === Folders to Backup === - SectionHeader("Folders to Backup") - - if (errors.containsKey("folders")) { - Text(errors["folders"]!!, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) - } - - CheckboxRow( - checked = config.backupWtf, - onCheckedChange = { viewModel.updateBackupWtf(it) }, - label = "WTF", - description = "Contains account settings, keybinds, macros, and addon saved variables.", - ) - - CheckboxRow( - checked = config.backupInterface, - onCheckedChange = { viewModel.updateBackupInterface(it) }, - label = "Interface", - description = "Contains installed addons.", - ) - - // === Options === - SectionHeader("Options") - - CheckboxRow( - checked = config.compressionEnabled, - onCheckedChange = { viewModel.updateCompression(it) }, - label = "Compression", - description = "Compress backups to save disk space. Slightly slower backup/restore.", - ) - - CheckboxRow( - checked = config.notificationsEnabled, - onCheckedChange = { viewModel.updateNotifications(it) }, - label = "Notifications", - description = "Show system notifications for backup events.", - ) - - CheckboxRow( - checked = config.runAtStartup, - onCheckedChange = { viewModel.updateRunAtStartup(it) }, - label = "Run at startup", - description = "Launch WoW Backup automatically when you log in.", - ) - - // === Appearance === - SectionHeader("Appearance") - - Text("Theme", style = MaterialTheme.typography.bodyMedium) - SingleChoiceSegmentedButtonRow { - ThemeMode.entries.forEachIndexed { index, mode -> - SegmentedButton( - selected = config.themeMode == mode, - onClick = { viewModel.updateThemeMode(mode) }, - shape = SegmentedButtonDefaults.itemShape(index, ThemeMode.entries.size), - ) { - Text( - when (mode) { - ThemeMode.SYSTEM -> "System" - ThemeMode.LIGHT -> "Light" - ThemeMode.DARK -> "Dark" - }, - ) - } - } - } - - Text("Accent color", style = MaterialTheme.typography.bodyMedium) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - AccentColor.entries.forEach { color -> - val isSelected = config.accentColor == color - Box( - modifier = Modifier - .size(36.dp) - .clip(CircleShape) - .background(color.seedColor, CircleShape) - .then( - if (isSelected) { - Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape) - } else { - Modifier - }, - ) - .clickable { viewModel.updateAccentColor(color) }, - contentAlignment = Alignment.Center, - ) { - if (isSelected) { - Text( - "\u2713", - color = MaterialTheme.colorScheme.surface, - style = MaterialTheme.typography.labelLarge, - ) - } - } - } - } - - Text( - "Theme changes apply immediately.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - // === Footer === - Spacer(Modifier.height(8.dp)) + // === Sticky Footer === HorizontalDivider() - Spacer(Modifier.height(8.dp)) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { @@ -303,20 +323,6 @@ fun ConfigScreen( } } - VerticalScrollbar( - modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), - adapter = rememberScrollbarAdapter(scrollState), - style = ScrollbarStyle( - minimalHeight = 48.dp, - thickness = 8.dp, - shape = RoundedCornerShape(4.dp), - hoverDurationMillis = 300, - unhoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), - hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - ), - ) - } - // Force backup confirmation dialog if (showForceBackupDialog) { ConfirmationDialog(