Make it pretty

This commit is contained in:
Rukira 2026-03-04 14:40:04 +00:00
parent 5cdcc9a490
commit 923835203a
8 changed files with 144 additions and 2 deletions

View file

@ -29,6 +29,7 @@ kotlin {
implementation(libs.cardiologist) implementation(libs.cardiologist)
implementation(libs.kotlin.logging.jvm) implementation(libs.kotlin.logging.jvm)
implementation(libs.logback.classic) implementation(libs.logback.classic)
implementation(libs.material.kolor)
} }
} }
} }

View file

@ -8,22 +8,28 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.rukira.wowbackup.config.ConfigManager
import com.rukira.wowbackup.ui.Screen import com.rukira.wowbackup.ui.Screen
import com.rukira.wowbackup.ui.config.ConfigScreen import com.rukira.wowbackup.ui.config.ConfigScreen
import com.rukira.wowbackup.ui.config.ConfigViewModel import com.rukira.wowbackup.ui.config.ConfigViewModel
import com.rukira.wowbackup.ui.status.StatusScreen import com.rukira.wowbackup.ui.status.StatusScreen
import com.rukira.wowbackup.ui.status.StatusViewModel import com.rukira.wowbackup.ui.status.StatusViewModel
import com.rukira.wowbackup.ui.theme.WoWBackupTheme
@Composable @Composable
fun App( fun App(
currentScreen: Screen, currentScreen: Screen,
onNavigate: (Screen) -> Unit, onNavigate: (Screen) -> Unit,
) { ) {
MaterialTheme { val config by ConfigManager.config.collectAsState()
WoWBackupTheme(themeMode = config.themeMode, accentColor = config.accentColor) {
Surface(modifier = Modifier.fillMaxSize()) { Surface(modifier = Modifier.fillMaxSize()) {
when (currentScreen) { when (currentScreen) {
Screen.STATUS -> { Screen.STATUS -> {
@ -48,6 +54,7 @@ fun App(
} }
} }
@Composable @Composable
private fun PlaceholderScreen(title: String, subtitle: String) { private fun PlaceholderScreen(title: String, subtitle: String) {
Column( Column(

View file

@ -1,8 +1,26 @@
package com.rukira.wowbackup.config package com.rukira.wowbackup.config
import androidx.compose.ui.graphics.Color
import kotlinx.datetime.LocalTime import kotlinx.datetime.LocalTime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
enum class ThemeMode { SYSTEM, LIGHT, DARK }
@Serializable
enum class AccentColor(val displayName: String, private val colorValue: Long) {
PURPLE("Purple", 0xFF7C4DFF),
BLUE("Blue", 0xFF448AFF),
TEAL("Teal", 0xFF1DE9B6),
GREEN("Green", 0xFF69F0AE),
ORANGE("Orange", 0xFFFF9100),
RED("Red", 0xFFFF5252),
PINK("Pink", 0xFFFF4081),
INDIGO("Indigo", 0xFF536DFE);
val seedColor: Color get() = Color(colorValue)
}
@Serializable @Serializable
data class AppConfig( data class AppConfig(
val wowInstallPath: String? = null, val wowInstallPath: String? = null,
@ -15,6 +33,8 @@ data class AppConfig(
val compressionEnabled: Boolean = false, val compressionEnabled: Boolean = false,
val notificationsEnabled: Boolean = true, val notificationsEnabled: Boolean = true,
val runAtStartup: Boolean = false, val runAtStartup: Boolean = false,
val themeMode: ThemeMode = ThemeMode.SYSTEM,
val accentColor: AccentColor = AccentColor.PURPLE,
) { ) {
val isConfigured: Boolean val isConfigured: Boolean
get() = !wowInstallPath.isNullOrBlank() && !backupPath.isNullOrBlank() get() = !wowInstallPath.isNullOrBlank() && !backupPath.isNullOrBlank()

View file

@ -1,6 +1,10 @@
package com.rukira.wowbackup.ui.config package com.rukira.wowbackup.ui.config
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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -8,8 +12,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
@ -17,6 +23,9 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -26,7 +35,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.rukira.wowbackup.config.AccentColor
import com.rukira.wowbackup.config.ThemeMode
import com.rukira.wowbackup.ui.components.ConfirmationDialog import com.rukira.wowbackup.ui.components.ConfirmationDialog
import com.rukira.wowbackup.ui.components.TimePicker import com.rukira.wowbackup.ui.components.TimePicker
import com.rukira.wowbackup.ui.components.pickFolder import com.rukira.wowbackup.ui.components.pickFolder
@ -185,6 +197,64 @@ fun ConfigScreen(
description = "Launch WoW Backup automatically when you log in.", 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 === // === Footer ===
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
HorizontalDivider() HorizontalDivider()

View file

@ -1,8 +1,10 @@
package com.rukira.wowbackup.ui.config package com.rukira.wowbackup.ui.config
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.rukira.wowbackup.config.AccentColor
import com.rukira.wowbackup.config.AppConfig import com.rukira.wowbackup.config.AppConfig
import com.rukira.wowbackup.config.ConfigManager import com.rukira.wowbackup.config.ConfigManager
import com.rukira.wowbackup.config.ThemeMode
import com.rukira.wowbackup.platform.WoWLocations import com.rukira.wowbackup.platform.WoWLocations
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -134,6 +136,20 @@ class ConfigViewModel : ViewModel() {
} }
} }
fun updateThemeMode(mode: ThemeMode) {
_state.update {
it.copy(config = it.config.copy(themeMode = mode))
}
ConfigManager.save(_state.value.config)
}
fun updateAccentColor(color: AccentColor) {
_state.update {
it.copy(config = it.config.copy(accentColor = color))
}
ConfigManager.save(_state.value.config)
}
fun detectWoWLocation() { fun detectWoWLocation() {
val detected = WoWLocations.findWoWInstall() val detected = WoWLocations.findWoWInstall()
if (detected != null) { if (detected != null) {

View file

@ -205,7 +205,7 @@ fun StatusScreen(
}, },
enabled = uiState.backupPath != null, enabled = uiState.backupPath != null,
) { ) {
Text("Open Folder") Text("Open Backups Folder")
} }
OutlinedButton(onClick = onNavigateToRestore, enabled = false) { OutlinedButton(onClick = onNavigateToRestore, enabled = false) {
Text("Restore (Coming Soon)") Text("Restore (Coming Soon)")

View file

@ -0,0 +1,26 @@
package com.rukira.wowbackup.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import com.materialkolor.DynamicMaterialTheme
import com.rukira.wowbackup.config.AccentColor
import com.rukira.wowbackup.config.ThemeMode
@Composable
fun WoWBackupTheme(
themeMode: ThemeMode = ThemeMode.SYSTEM,
accentColor: AccentColor = AccentColor.PURPLE,
content: @Composable () -> Unit,
) {
val useDarkTheme = when (themeMode) {
ThemeMode.SYSTEM -> isSystemInDarkTheme()
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
}
DynamicMaterialTheme(
seedColor = accentColor.seedColor,
useDarkTheme = useDarkTheme,
content = content,
)
}

View file

@ -8,6 +8,7 @@ kotlinx-serialization = "1.10.0"
cardiologist = "0.8.0" cardiologist = "0.8.0"
kotlin-logging = "7.0.3" kotlin-logging = "7.0.3"
logback = "1.5.18" logback = "1.5.18"
materialKolor = "2.0.0"
[libraries] [libraries]
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
@ -18,6 +19,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
cardiologist = { module = "io.github.kevincianfarini.cardiologist:cardiologist", version.ref = "cardiologist" } cardiologist = { module = "io.github.kevincianfarini.cardiologist:cardiologist", version.ref = "cardiologist" }
kotlin-logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" } kotlin-logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }