Sticky buttons in Settings

This commit is contained in:
Rukira 2026-08-25 11:32:19 +01:00
parent 59b296d04d
commit 846794fa36

View file

@ -63,214 +63,234 @@ fun ConfigScreen(
var showForceBackupDialog by remember { mutableStateOf(false) } var showForceBackupDialog by remember { mutableStateOf(false) }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .weight(1f)
.verticalScroll(scrollState) .fillMaxWidth(),
.padding(24.dp), ) {
verticalArrangement = Arrangement.spacedBy(16.dp), Column(
) { modifier = Modifier
Text("Settings", style = MaterialTheme.typography.headlineMedium) .fillMaxSize()
.verticalScroll(scrollState)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text("Settings", style = MaterialTheme.typography.headlineMedium)
// === WoW Installation === // === WoW Installation ===
SectionHeader("WoW Installation") SectionHeader("WoW Installation")
PathField( PathField(
label = "WoW install location", label = "WoW install location",
value = config.wowInstallPath ?: "", value = config.wowInstallPath ?: "",
error = errors["wowPath"], error = errors["wowPath"],
onBrowse = { onBrowse = {
val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) }) val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) })
if (dir != null) viewModel.updateWoWPath(dir.absolutePath) if (dir != null) viewModel.updateWoWPath(dir.absolutePath)
}, },
) )
// WoW path warnings (e.g. missing WTF/Interface, permission issues) // WoW path warnings (e.g. missing WTF/Interface, permission issues)
uiState.warnings["wowPath"]?.forEach { warning -> uiState.warnings["wowPath"]?.forEach { warning ->
Text( Text(
warning, warning,
color = MaterialTheme.colorScheme.tertiary, color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.bodySmall, 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"
} }
val interfaceStatus = when {
validation.interfaceReadable -> "Interface folder: found" // WoW validation summary when valid
validation.hasInterface -> "Interface folder: found (not readable)" uiState.wowValidation?.let { validation ->
else -> "Interface folder: not found" 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( Text(
"$wtfStatus | $interfaceStatus", "Theme changes apply immediately.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
}
OutlinedButton(onClick = { viewModel.detectWoWLocation() }) { VerticalScrollbar(
Text("Auto-detect") modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
} adapter = rememberScrollbarAdapter(scrollState),
style = ScrollbarStyle(
// === Backup Settings === minimalHeight = 48.dp,
SectionHeader("Backup Settings") thickness = 8.dp,
shape = RoundedCornerShape(4.dp),
PathField( hoverDurationMillis = 300,
label = "Backup destination", unhoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
value = config.backupPath ?: "", hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
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( // === Sticky Footer ===
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))
HorizontalDivider() HorizontalDivider()
Spacer(Modifier.height(8.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically, 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 // Force backup confirmation dialog
if (showForceBackupDialog) { if (showForceBackupDialog) {
ConfirmationDialog( ConfirmationDialog(