Compare commits

...

5 commits

Author SHA1 Message Date
Rukira
17ba07deef Random small improvements 2026-03-04 15:33:29 +00:00
Rukira
d738b133d0 Run on startup 2026-03-04 15:17:57 +00:00
Rukira
4036febb70 Run on startup 2026-03-04 15:13:18 +00:00
Rukira
44656c81d5 Performance 2026-03-04 15:00:31 +00:00
Rukira
923835203a Make it pretty 2026-03-04 14:40:04 +00:00
14 changed files with 492 additions and 35 deletions

View file

@ -0,0 +1,94 @@
name: Bug Report
about: Something not working right? Let us know so we can fix it!
title: "[Bug] "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report this! The more detail you can give, the faster we can track it down.
Don't worry about being too technical — just describe what happened in your own words.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Tell us what went wrong. What did you see or experience?
placeholder: "e.g. The app froze after I clicked Save, and my backup never ran."
validations:
required: true
- type: textarea
id: expected
attributes:
label: What did you expect to happen?
placeholder: "e.g. I expected my settings to save and the backup to start at the scheduled time."
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: |
Walk us through exactly what you did before the problem showed up.
Try to be specific — even small details can help! Number each step.
placeholder: |
1. Opened WoW Backup
2. Went to Settings
3. Changed the backup time to 5:00 PM
4. Clicked Save
5. The app froze and stopped responding
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: How often does this happen?
options:
- Every time
- Sometimes
- It only happened once
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- macOS
- Windows
validations:
required: true
- type: input
id: app-version
attributes:
label: App version
description: You can find this in the app's title bar or About screen.
placeholder: "e.g. 1.0.0"
- type: textarea
id: logs
attributes:
label: Log file
description: |
Logs help us understand what went wrong behind the scenes. Here's how to find them:
**macOS:** Open Finder, press **Cmd+Shift+G**, and paste: `~/Library/Application Support/WoWBackup/logs`
**Windows:** Press **Win+R**, and paste: `%APPDATA%\WoWBackup\logs`
You can also open this folder from the app: go to **Settings** and click the **Open Logs** button at the bottom.
Attach the most recent `wowbackup.log` file by dragging it into this text box.
placeholder: Drag your log file here, or paste any relevant log lines.
- type: textarea
id: extra
attributes:
label: Anything else?
description: Screenshots, screen recordings, or any other context that might help.

View file

@ -29,10 +29,38 @@ kotlin {
implementation(libs.cardiologist)
implementation(libs.kotlin.logging.jvm)
implementation(libs.logback.classic)
implementation(libs.material.kolor)
}
}
}
val appVersion = "0.1.0"
val generatedSrcDir = layout.buildDirectory.dir("generated/src/jvmMain/kotlin")
val generateBuildConfig by tasks.registering {
val outputDir = generatedSrcDir
val version = appVersion
outputs.dir(outputDir)
doLast {
val dir = outputDir.get().asFile.resolve("com/rukira/wowbackup")
dir.mkdirs()
dir.resolve("BuildConfig.kt").writeText(
"""
|package com.rukira.wowbackup
|
|object BuildConfig {
| const val VERSION = "$version"
|}
""".trimMargin()
)
}
}
kotlin.sourceSets.named("jvmMain") {
kotlin.srcDir(generateBuildConfig)
}
compose.desktop {
application {
mainClass = "com.rukira.wowbackup.MainKt"
@ -42,7 +70,7 @@ compose.desktop {
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.rukira.wowbackup"
packageVersion = "1.0.0"
packageVersion = appVersion
}
}
}

View file

@ -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(

View file

@ -17,12 +17,29 @@ class BackupEntry(
val sizeBytes: Long by lazy { sizeComputer() }
}
data class BackupMetrics(val fileCount: Int, val sizeBytes: Long)
object BackupHistory {
// Used internally for parsing — java.time formatter since kotlinx.datetime
// doesn't have custom format patterns built in
private val JAVA_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
/**
* Counts backups without parsing timestamps or sorting just matches the naming pattern.
*/
fun countBackups(backupDir: File): Int {
if (!backupDir.exists() || !backupDir.isDirectory) return 0
return backupDir.listFiles()?.count { file ->
val name = file.nameWithoutExtension
val isZip = file.extension.equals("zip", ignoreCase = true) && file.isFile
val isDir = file.isDirectory
if (!isZip && !isDir) return@count false
try { java.time.LocalDateTime.parse(name, JAVA_TIMESTAMP_FORMAT); true }
catch (_: DateTimeParseException) { false }
} ?: 0
}
/**
* Lists all existing backups in the backup directory, sorted newest first.
*/
@ -37,13 +54,18 @@ object BackupHistory {
}
/**
* Returns the file count for a backup entry without computing its full size.
* Computes file count and size in a single pass over the backup tree.
*/
fun fileCount(entry: BackupEntry): Int {
fun computeMetrics(entry: BackupEntry): BackupMetrics {
return if (entry.isCompressed) {
java.util.zip.ZipFile(entry.path).use { it.size() }
java.util.zip.ZipFile(entry.path).use { zip ->
BackupMetrics(zip.size(), entry.path.length())
}
} else {
entry.path.walkTopDown().count { it.isFile }
var count = 0
var size = 0L
entry.path.walkTopDown().filter { it.isFile }.forEach { count++; size += it.length() }
BackupMetrics(count, size)
}
}

View file

@ -46,6 +46,19 @@ object BackupScheduler {
if (schedulerJob?.isActive == true) return
logger.info { "Backup scheduler started" }
// Eagerly set timestamps so StatusScreen has data immediately.
// Only set lastBackupTime and nextBackupTime here (cheap).
// The full lastBackupResult (fileCount, sizeBytes) is filled by the async path
// since fileCount() walks the directory tree and would block the main thread.
val config = ConfigManager.config.value
if (config.isConfigured && config.backupPath != null) {
val backups = BackupHistory.listBackups(File(config.backupPath))
if (backups.isNotEmpty()) {
_state.update { it.copy(lastBackupTime = backups.first().timestamp) }
}
updateNextBackupTime(TimeZone.currentSystemDefault())
}
schedulerJob = scope.launch {
// Collect progress from BackupEngine
launch {
@ -65,23 +78,23 @@ object BackupScheduler {
}
// On first config emission, restore last backup info from disk
if (_state.value.lastBackupTime == null && config.backupPath != null) {
if (_state.value.lastBackupResult == null && config.backupPath != null) {
val backups = BackupHistory.listBackups(File(config.backupPath))
if (backups.isNotEmpty()) {
val latest = backups.first()
val fileCount = BackupHistory.fileCount(latest)
val metrics = BackupHistory.computeMetrics(latest)
_state.update {
it.copy(
lastBackupTime = latest.timestamp,
lastBackupResult = BackupResult.Success(
backupPath = latest.path,
fileCount = fileCount,
sizeBytes = latest.sizeBytes,
fileCount = metrics.fileCount,
sizeBytes = metrics.sizeBytes,
durationMs = 0,
),
)
}
logger.info { "Restored last backup info from disk: ${latest.path.name} ($fileCount files)" }
logger.info { "Restored last backup info from disk: ${latest.path.name} (${metrics.fileCount} files)" }
}
}

View file

@ -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()

View file

@ -11,7 +11,6 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import androidx.compose.ui.window.TrayState
import androidx.compose.ui.window.rememberTrayState
import com.rukira.wowbackup.backup.BackupNotifier
import com.rukira.wowbackup.backup.BackupScheduler
@ -19,6 +18,7 @@ import com.rukira.wowbackup.config.ConfigManager
import com.rukira.wowbackup.logging.LoggingSetup
import com.rukira.wowbackup.platform.OS
import com.rukira.wowbackup.platform.Platform
import com.rukira.wowbackup.platform.StartupManager
import com.rukira.wowbackup.platform.WoWLocations
import com.rukira.wowbackup.ui.Screen
import com.rukira.wowbackup.ui.trayIconPainter
@ -34,19 +34,21 @@ fun main() {
ConfigManager.load()
logger.info { "Config loaded. Configured: ${ConfigManager.isConfigured}" }
// Sync OS startup registration with persisted config
StartupManager.setEnabled(ConfigManager.config.value.runAtStartup)
val detectedWoW = WoWLocations.findWoWInstall()
if (detectedWoW != null) {
logger.info { "Auto-detected WoW at: $detectedWoW" }
}
val startScreen = if (ConfigManager.isConfigured) Screen.STATUS else Screen.CONFIG
val isConfigured = ConfigManager.isConfigured
// Start backup scheduler
BackupScheduler.start()
application {
var isWindowVisible by remember { mutableStateOf(true) }
var currentScreen by remember { mutableStateOf(startScreen) }
var currentScreen by remember { mutableStateOf(if (isConfigured) null else Screen.CONFIG) }
val trayState = rememberTrayState()
// Wire up notifications
@ -56,20 +58,10 @@ fun main() {
state = trayState,
icon = trayIconPainter(),
tooltip = "WoW Backup",
onAction = { isWindowVisible = true },
onAction = { currentScreen = Screen.STATUS },
menu = {
Item("Status", onClick = {
currentScreen = Screen.STATUS
isWindowVisible = true
})
Item("Settings", onClick = {
currentScreen = Screen.CONFIG
isWindowVisible = true
})
Separator()
Item("Backup Now", onClick = {
BackupScheduler.triggerBackupNow()
})
Item("Status", onClick = { currentScreen = Screen.STATUS })
Item("Settings", onClick = { currentScreen = Screen.CONFIG })
Separator()
Item("Quit", onClick = {
BackupScheduler.stop()
@ -78,8 +70,9 @@ fun main() {
},
)
if (isWindowVisible) {
val windowState = remember(isWindowVisible) {
val activeScreen = currentScreen
if (activeScreen != null) {
val windowState = remember(activeScreen) {
WindowState(
size = DpSize(480.dp, 620.dp),
position = trayAlignedPosition(),
@ -87,13 +80,13 @@ fun main() {
}
Window(
onCloseRequest = { isWindowVisible = false },
onCloseRequest = { currentScreen = null },
title = "WoW Backup",
state = windowState,
resizable = true,
) {
App(
currentScreen = currentScreen,
currentScreen = activeScreen,
onNavigate = { currentScreen = it },
)
}

View file

@ -0,0 +1,127 @@
package com.rukira.wowbackup.platform
import io.github.oshai.kotlinlogging.KotlinLogging
import java.io.File
private val logger = KotlinLogging.logger {}
object StartupManager {
private const val APP_ID = "com.rukira.wowbackup"
fun setEnabled(enabled: Boolean) {
val appPath = getAppPath()
if (appPath == null) {
logger.warn { "Cannot register startup: not running from a packaged app (dev mode?)" }
return
}
logger.info { "Setting launch-at-startup to $enabled (appPath=$appPath)" }
when (Platform.current) {
OS.Mac -> if (enabled) enableMac(appPath) else disableMac()
OS.Windows -> if (enabled) enableWindows(appPath) else disableWindows()
else -> logger.warn { "Launch at startup is not supported on ${Platform.current}" }
}
}
private fun getAppPath(): String? = when (Platform.current) {
OS.Mac -> {
// java.home is e.g. /Applications/com.rukira.wowbackup.app/Contents/runtime/Contents/Home
val javaHome = System.getProperty("java.home") ?: return null
val marker = ".app/Contents"
val idx = javaHome.indexOf(marker)
if (idx < 0) null else javaHome.substring(0, idx + ".app".length)
}
OS.Windows -> {
ProcessHandle.current().info().command().orElse(null)?.takeIf { it.endsWith(".exe", ignoreCase = true) }
}
else -> null
}
// --- macOS: LaunchAgent plist ---
private val plistFile: File
get() = File(System.getProperty("user.home"), "Library/LaunchAgents/$APP_ID.plist")
private fun enableMac(appPath: String) {
val plist = """
|<?xml version="1.0" encoding="UTF-8"?>
|<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
| "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|<plist version="1.0">
|<dict>
| <key>Label</key>
| <string>$APP_ID</string>
| <key>ProgramArguments</key>
| <array>
| <string>open</string>
| <string>-a</string>
| <string>$appPath</string>
| </array>
| <key>RunAtLoad</key>
| <true/>
|</dict>
|</plist>
""".trimMargin()
try {
plistFile.parentFile.mkdirs()
plistFile.writeText(plist)
logger.info { "LaunchAgent plist written to ${plistFile.absolutePath}" }
} catch (e: Exception) {
logger.error(e) { "Failed to write LaunchAgent plist" }
}
}
private fun disableMac() {
try {
if (plistFile.exists()) {
plistFile.delete()
logger.info { "LaunchAgent plist removed" }
}
} catch (e: Exception) {
logger.error(e) { "Failed to remove LaunchAgent plist" }
}
}
// --- Windows: Registry ---
private const val REG_KEY = """HKCU\Software\Microsoft\Windows\CurrentVersion\Run"""
private const val REG_VALUE = "WoWBackup"
private fun enableWindows(appPath: String) {
try {
val process = ProcessBuilder(
"reg", "add", REG_KEY, "/v", REG_VALUE, "/d", appPath, "/f"
).redirectErrorStream(true).start()
val exitCode = process.waitFor()
if (exitCode == 0) {
logger.info { "Registry startup entry added" }
} else {
val output = process.inputStream.bufferedReader().readText()
logger.error { "reg add failed (exit $exitCode): $output" }
}
} catch (e: Exception) {
logger.error(e) { "Failed to add registry startup entry" }
}
}
private fun disableWindows() {
try {
val process = ProcessBuilder(
"reg", "delete", REG_KEY, "/v", REG_VALUE, "/f"
).redirectErrorStream(true).start()
val exitCode = process.waitFor()
if (exitCode == 0) {
logger.info { "Registry startup entry removed" }
} else {
val output = process.inputStream.bufferedReader().readText()
// Exit code 1 with "not found" is expected when disabling and key doesn't exist
logger.warn { "reg delete exited $exitCode: $output" }
}
} catch (e: Exception) {
logger.error(e) { "Failed to remove registry startup entry" }
}
}
}

View file

@ -1,15 +1,26 @@
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
import androidx.compose.foundation.layout.fillMaxHeight
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.ScrollbarStyle
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
@ -17,6 +28,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 +40,12 @@ 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.platform.AppDirectories
import com.rukira.wowbackup.platform.DesktopActions
import com.rukira.wowbackup.ui.components.ConfirmationDialog
import com.rukira.wowbackup.ui.components.TimePicker
import com.rukira.wowbackup.ui.components.pickFolder
@ -42,11 +61,13 @@ fun ConfigScreen(
val config = uiState.config
val errors = uiState.errors
var showForceBackupDialog by remember { mutableStateOf(false) }
val scrollState = rememberScrollState()
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.verticalScroll(scrollState)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
@ -185,6 +206,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()
@ -195,6 +274,10 @@ fun ConfigScreen(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(onClick = { DesktopActions.openFolder(AppDirectories.logsDir) }) {
Text("Open Logs")
}
Spacer(Modifier.weight(1f))
if (uiState.saveSuccess) {
Text(
"Saved!",
@ -220,6 +303,20 @@ 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(

View file

@ -1,8 +1,11 @@
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.StartupManager
import com.rukira.wowbackup.platform.WoWLocations
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.MutableStateFlow
@ -134,6 +137,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) {
@ -154,6 +171,7 @@ class ConfigViewModel : ViewModel() {
}
ConfigManager.save(_state.value.config)
StartupManager.setEnabled(_state.value.config.runAtStartup)
_state.update { it.copy(errors = emptyMap(), saveSuccess = true) }
logger.info { "Configuration saved" }
return true

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.rukira.wowbackup.BuildConfig
import com.rukira.wowbackup.backup.BackupScheduler
import com.rukira.wowbackup.platform.DesktopActions
import java.io.File
@ -205,11 +206,20 @@ fun StatusScreen(
},
enabled = uiState.backupPath != null,
) {
Text("Open Folder")
Text("Open Backups Folder")
}
OutlinedButton(onClick = onNavigateToRestore, enabled = false) {
Text("Restore (Coming Soon)")
}
}
Spacer(Modifier.weight(1f))
Text(
"WoW Backup v${BuildConfig.VERSION}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth(),
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
)
}
}

View file

@ -43,7 +43,7 @@ class StatusViewModel : ViewModel() {
BackupScheduler.state.map { it.lastBackupResult }.distinctUntilChanged(),
) { backupPath, _ ->
if (backupPath != null) {
BackupHistory.listBackups(File(backupPath)).size
BackupHistory.countBackups(File(backupPath))
} else {
0
}

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"
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" }