Sticky buttons in Settings
This commit is contained in:
parent
59b296d04d
commit
846794fa36
1 changed files with 211 additions and 205 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue