From 923835203aa7551c4596dfaba0609d7a77758fed Mon Sep 17 00:00:00 2001 From: Rukira Date: Wed, 4 Mar 2026 14:40:04 +0000 Subject: [PATCH] Make it pretty --- composeApp/build.gradle.kts | 1 + .../kotlin/com/rukira/wowbackup/App.kt | 9 ++- .../com/rukira/wowbackup/config/AppConfig.kt | 20 ++++++ .../wowbackup/ui/config/ConfigScreen.kt | 70 +++++++++++++++++++ .../wowbackup/ui/config/ConfigViewModel.kt | 16 +++++ .../wowbackup/ui/status/StatusScreen.kt | 2 +- .../com/rukira/wowbackup/ui/theme/Theme.kt | 26 +++++++ gradle/libs.versions.toml | 2 + 8 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/theme/Theme.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index ea1e38a..7f8ac7e 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -29,6 +29,7 @@ kotlin { implementation(libs.cardiologist) implementation(libs.kotlin.logging.jvm) implementation(libs.logback.classic) + implementation(libs.material.kolor) } } } diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt index e053ea8..f417ce0 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt @@ -8,22 +8,28 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.rukira.wowbackup.config.ConfigManager import com.rukira.wowbackup.ui.Screen import com.rukira.wowbackup.ui.config.ConfigScreen import com.rukira.wowbackup.ui.config.ConfigViewModel import com.rukira.wowbackup.ui.status.StatusScreen import com.rukira.wowbackup.ui.status.StatusViewModel +import com.rukira.wowbackup.ui.theme.WoWBackupTheme @Composable fun App( currentScreen: Screen, onNavigate: (Screen) -> Unit, ) { - MaterialTheme { + val config by ConfigManager.config.collectAsState() + + WoWBackupTheme(themeMode = config.themeMode, accentColor = config.accentColor) { Surface(modifier = Modifier.fillMaxSize()) { when (currentScreen) { Screen.STATUS -> { @@ -48,6 +54,7 @@ fun App( } } + @Composable private fun PlaceholderScreen(title: String, subtitle: String) { Column( diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/config/AppConfig.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/config/AppConfig.kt index 65ddf1d..ae7649e 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/config/AppConfig.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/config/AppConfig.kt @@ -1,8 +1,26 @@ package com.rukira.wowbackup.config +import androidx.compose.ui.graphics.Color import kotlinx.datetime.LocalTime 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 data class AppConfig( val wowInstallPath: String? = null, @@ -15,6 +33,8 @@ data class AppConfig( val compressionEnabled: Boolean = false, val notificationsEnabled: Boolean = true, val runAtStartup: Boolean = false, + val themeMode: ThemeMode = ThemeMode.SYSTEM, + val accentColor: AccentColor = AccentColor.PURPLE, ) { val isConfigured: Boolean get() = !wowInstallPath.isNullOrBlank() && !backupPath.isNullOrBlank() 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 a5ef35a..593c80c 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 @@ -1,6 +1,10 @@ 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Checkbox @@ -17,6 +23,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton 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.runtime.Composable import androidx.compose.runtime.collectAsState @@ -26,7 +35,10 @@ 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.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.TimePicker import com.rukira.wowbackup.ui.components.pickFolder @@ -185,6 +197,64 @@ fun ConfigScreen( 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() diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigViewModel.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigViewModel.kt index 345ca6d..c2fb730 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigViewModel.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigViewModel.kt @@ -1,8 +1,10 @@ package com.rukira.wowbackup.ui.config import androidx.lifecycle.ViewModel +import com.rukira.wowbackup.config.AccentColor import com.rukira.wowbackup.config.AppConfig import com.rukira.wowbackup.config.ConfigManager +import com.rukira.wowbackup.config.ThemeMode import com.rukira.wowbackup.platform.WoWLocations import io.github.oshai.kotlinlogging.KotlinLogging 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() { val detected = WoWLocations.findWoWInstall() if (detected != null) { diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusScreen.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusScreen.kt index e26d6a6..5a77680 100644 --- a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusScreen.kt +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusScreen.kt @@ -205,7 +205,7 @@ fun StatusScreen( }, enabled = uiState.backupPath != null, ) { - Text("Open Folder") + Text("Open Backups Folder") } OutlinedButton(onClick = onNavigateToRestore, enabled = false) { Text("Restore (Coming Soon)") diff --git a/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/theme/Theme.kt b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/theme/Theme.kt new file mode 100644 index 0000000..9b4c7e7 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/theme/Theme.kt @@ -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, + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0c7f93..9e629fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ kotlinx-serialization = "1.10.0" cardiologist = "0.8.0" kotlin-logging = "7.0.3" logback = "1.5.18" +materialKolor = "2.0.0" [libraries] 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" } 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" } +material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }