Config screen

This commit is contained in:
Rukira 2026-03-04 14:19:19 +00:00
parent ac88e62abe
commit 4b3c512a9d
28 changed files with 2015 additions and 59 deletions

View file

@ -0,0 +1,154 @@
# Feature 0: Foundation
## Context
Before implementing any user-facing features, we need shared infrastructure: package structure, platform abstraction, config persistence, and logging. Every subsequent feature depends on this.
## Dependencies
- None (this is the base layer)
- **Depended on by**: All other features (1-5)
## New Dependencies to Add
### gradle/libs.versions.toml
| Library | Coordinate | Version | Purpose |
|---------|-----------|---------|---------|
| kotlinx-serialization-json | org.jetbrains.kotlinx:kotlinx-serialization-json | 1.10.0 | JSON config persistence |
| kotlin-logging-jvm | io.github.oshai:kotlin-logging-jvm | 7.0.3 | Kotlin-idiomatic logging |
| logback-classic | ch.qos.logback:logback-classic | 1.5.18 | Logging backend with file rotation |
### Plugins
| Plugin | ID | Version |
|--------|----|---------|
| Kotlin Serialization | org.jetbrains.kotlin.plugin.serialization | (uses kotlin version) |
## Package Structure
```
com.rukira.wowbackup/
├── main.kt # Entry point (existing, to be updated)
├── App.kt # Root composable (existing, to be gutted)
├── platform/
│ ├── Platform.kt # OS enum + detection
│ ├── AppDirectories.kt # Platform-specific app data paths
│ └── WoWLocations.kt # Default WoW install detection
├── config/
│ ├── AppConfig.kt # @Serializable data class
│ └── ConfigManager.kt # Read/write JSON config file
└── logging/
└── LoggingSetup.kt # Logback initialization
```
## Implementation Steps
### Step 1: Add dependencies
**Files to modify:**
- `gradle/libs.versions.toml` — add versions, libraries, and serialization plugin
- `build.gradle.kts` (root) — register serialization plugin
- `composeApp/build.gradle.kts` — apply serialization plugin, add new deps to jvmMain
- `settings.gradle.kts` — rename root project from "MyApplication" to "WoWBackup"
### Step 2: Platform abstraction
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/platform/Platform.kt`
```kotlin
enum class OS { Windows, Mac, Linux, Other }
object Platform {
val current: OS = System.getProperty("os.name").lowercase().let { name ->
when {
"mac" in name -> OS.Mac
"win" in name -> OS.Windows
"nux" in name || "nix" in name -> OS.Linux
else -> OS.Other
}
}
}
```
**Files to delete:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/Platform.kt` (old template file)
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/Greeting.kt` (template boilerplate)
### Step 3: App directories
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/platform/AppDirectories.kt`
Resolves platform-specific app data directory:
- macOS: `~/Library/Application Support/WoWBackup/`
- Windows: `%APPDATA%/WoWBackup/`
- Linux: `$XDG_CONFIG_HOME/WoWBackup/` (fallback `~/.config/WoWBackup/`)
Creates directory on first access if it doesn't exist.
### Step 4: WoW install detection
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/platform/WoWLocations.kt`
Scans known default install paths:
- macOS: `/Applications/World of Warcraft/`, `~/Applications/World of Warcraft/`
- Windows: `C:\Program Files (x86)\World of Warcraft\`, `C:\Program Files\World of Warcraft\`
Validates by checking for WTF folder, WoW executable, or .app bundle.
Returns `File?` — null if not found.
### Step 5: Config data model + persistence
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/config/AppConfig.kt`
```kotlin
@Serializable
data class AppConfig(
val wowInstallPath: String? = null,
val backupPath: String? = null,
val backupTimeOfDay: LocalTime = LocalTime(hour = 3, minute = 0), // daily at 3:00 AM
val backupHistoryCount: Int = 5,
val forceBackupWhileRunning: Boolean = false,
val backupWtf: Boolean = true,
val backupInterface: Boolean = true,
val compressionEnabled: Boolean = false,
val notificationsEnabled: Boolean = true,
val runAtStartup: Boolean = false,
)
```
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/config/ConfigManager.kt`
Responsibilities:
- Load config from `<appDataDir>/config.json` (returns defaults if file missing)
- Save config to `<appDataDir>/config.json` (pretty-printed JSON)
- Expose config as `StateFlow<AppConfig>` for reactive UI updates
- `isConfigured: Boolean` — true when wowInstallPath and backupPath are set
- Thread-safe reads/writes
### Step 6: Logging setup
**Files to create:**
- `composeApp/src/jvmMain/resources/logback.xml`
Configuration:
- Console appender (for development)
- Rolling file appender to `<appDataDir>/logs/wowbackup.log`
- Daily rotation + 10MB max size per file
- 30 days retention / 100MB total cap
- Pattern: `%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n`
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/logging/LoggingSetup.kt`
Programmatically sets the log directory to the platform-specific app data path at startup (since logback.xml can't reference runtime-resolved paths directly).
### Step 7: Update entry point
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt`
- Initialize logging on startup
- Load config via ConfigManager
- Update window title to "WoW Backup"
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt`
- Strip template boilerplate
- Simple placeholder screen showing config status ("Configured" / "Not configured")
## Verification
1. `./gradlew composeApp:run` — app launches, shows placeholder UI
2. Check `~/Library/Application Support/WoWBackup/` (macOS) exists after launch
3. Check `config.json` is created with defaults after first run
4. Check `logs/wowbackup.log` exists and contains startup log entries
5. If WoW is installed in default location, verify auto-detection logs the path

View file

@ -0,0 +1,104 @@
# Feature 1: System Tray Icon
## Context
The app lives primarily in the system tray. The window is secondary — it opens for configuration/status and closes back to tray. This is the shell that all UI features plug into.
## Dependencies
- **Depends on**: Feature 0 (Foundation) — logging, platform detection
- **Depended on by**: Features 2-5 (all UI features are accessed through the tray)
## Approach
Use Compose Desktop's built-in `Tray` composable (`androidx.compose.ui.window.Tray`). No extra libraries needed — it supports icons, tooltips, context menus, and notifications natively.
## App Lifecycle Model
The app always runs with a tray icon visible. The main window is shown/hidden based on user interaction. Closing the window hides it to tray — it does not exit the app. Exiting is only via the tray menu "Quit" item.
## Implementation Steps
### Step 1: Create a tray icon asset
**Files to create:**
- `composeApp/src/jvmMain/resources/tray-icon.png` (32x32, simple recognizable icon)
For macOS dark mode support, add JVM arg: `-Dapple.awt.enableTemplateImages=true`
### Step 2: Restructure main.kt for tray-first lifecycle
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt`
```kotlin
fun main() = application {
// Initialize logging (from Feature 0)
var isWindowVisible by remember { mutableStateOf(true) }
// Which screen to show: STATUS (default) or CONFIG
var currentScreen by remember { mutableStateOf(Screen.STATUS) }
Tray(
icon = painterResource("tray-icon.png"),
tooltip = "WoW Backup",
onAction = { isWindowVisible = true }, // click tray -> show window
menu = {
Item("Status", onClick = {
currentScreen = Screen.STATUS
isWindowVisible = true
})
Item("Settings", onClick = {
currentScreen = Screen.CONFIG
isWindowVisible = true
})
Separator()
Item("Quit", onClick = ::exitApplication)
}
)
if (isWindowVisible) {
Window(
onCloseRequest = { isWindowVisible = false }, // close -> hide to tray
title = "WoW Backup",
) {
App(currentScreen)
}
}
}
```
### Step 3: Define screen navigation enum
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/Screen.kt`
```kotlin
enum class Screen { STATUS, CONFIG, RESTORE }
```
### Step 4: Update App.kt for screen routing
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt`
Strip all template code. Route to placeholder screens based on `currentScreen`:
- `Screen.STATUS` -> "Status screen placeholder"
- `Screen.CONFIG` -> "Config screen placeholder"
- `Screen.RESTORE` -> "Restore screen placeholder"
### Step 5: Auto-show config if not configured
In `main.kt`, on startup check `ConfigManager.isConfigured`. If false, set `currentScreen = Screen.CONFIG` and `isWindowVisible = true`.
### Step 6: macOS JVM args
**Files to modify:**
- `composeApp/build.gradle.kts` — add to desktop application config:
```kotlin
compose.desktop {
application {
jvmArgs("-Dapple.awt.enableTemplateImages=true")
}
}
```
## Verification
1. `./gradlew composeApp:run` — app launches with tray icon visible
2. Tray icon shows context menu with Status, Settings, Quit
3. Clicking "Status" or "Settings" opens the window to the correct placeholder
4. Closing the window hides it (app stays running in tray)
5. Clicking tray icon again re-shows the window
6. "Quit" exits the app fully
7. On first run (no config), window auto-opens to config screen

View file

@ -0,0 +1,113 @@
# Feature 2: Configuration Screen
## Context
The configuration screen is the primary setup interface. It appears automatically on first run (when config is incomplete) and is accessible anytime via the tray menu. All backup behavior depends on the values set here.
## Dependencies
- **Depends on**: Feature 0 (ConfigManager, AppConfig), Feature 1 (screen routing, tray menu)
- **Depended on by**: Feature 3 (Backup — uses all config values), Feature 4 (Status — shows config warnings)
## Layout
Single scrollable form with section headers. All options visible on one page.
## File Dialogs
Native OS dialogs via `java.awt.FileDialog` (preferred on macOS) or `javax.swing.JFileChooser` (Windows fallback). Folder-selection mode.
## Implementation Steps
### Step 1: Native file/folder picker utility
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/components/NativeFolderPicker.kt`
A utility function that opens a native folder-selection dialog and returns `File?`:
- macOS: Use `java.awt.FileDialog` with `System.setProperty("apple.awt.fileDialogForDirectories", "true")` to enable folder mode
- Windows/Linux: Use `JFileChooser` with `DIRECTORIES_ONLY` mode
- Runs on the AWT event thread to avoid threading issues
### Step 2: Create the ConfigViewModel
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigViewModel.kt`
Responsibilities:
- Holds a mutable copy of `AppConfig` as UI state
- Validates inputs (e.g., paths exist, time is valid)
- `save()` — writes to ConfigManager
- `detectWoWLocation()` — calls `WoWLocations.findWoWInstall()` and pre-fills if found
- Exposes validation errors per field
### Step 3: Build the ConfigScreen composable
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/config/ConfigScreen.kt`
Single scrollable `Column` with these sections:
#### Section: WoW Installation
- **WoW install location**: Text field (read-only) + "Browse" button (opens native folder picker)
- "Auto-detect" button that scans default locations
- Validation: show error if path doesn't exist or doesn't look like a WoW install
#### Section: Backup Settings
- **Backup destination**: Text field (read-only) + "Browse" button
- **Backup schedule**: Time picker for daily backup time (hour:minute)
- **Backup history**: Dropdown/spinner for how many backups to keep (1-30, default 5)
- **Force backup**: Checkbox "Allow backup while WoW is running"
- When toggled ON, show a confirmation dialog warning that backing up while WoW runs may capture inconsistent state
#### Section: Folders to Backup
- **WTF folder**: Checkbox (default on) + description text:
"Contains account settings, keybinds, macros, and addon saved variables."
- **Interface folder**: Checkbox (default on) + description text:
"Contains installed addons and their configuration."
#### Section: Options
- **Compression**: Checkbox + description text:
"Compress backups to save disk space. Slightly slower backup/restore."
- **Notifications**: Checkbox
"Show system notifications for backup events."
- **Run at startup**: Checkbox
"Launch WoW Backup automatically when you log in."
#### Footer
- **Save** button (primary) — validates, saves config, shows success feedback
- **Cancel** button — reverts changes, closes window (or navigates to status if first-time setup is incomplete, stays on config)
### Step 4: Time picker component
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/components/TimePicker.kt`
Simple hour:minute selector. Two dropdowns (0-23 hours, 0-59 minutes in 15-min increments) or a pair of number fields. Keep it minimal.
### Step 5: Confirmation dialog for "Force backup"
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/components/ConfirmationDialog.kt`
Reusable Material3 `AlertDialog` component. Used here for the force-backup warning, and later by the Restore feature (Feature 5).
### Step 6: Wire into App.kt routing
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt`
Replace config placeholder with `ConfigScreen(viewModel, onNavigateToStatus)`.
### Step 7: Run-at-startup implementation (deferred)
This is platform-specific and non-trivial:
- macOS: LaunchAgent plist in `~/Library/LaunchAgents/`
- Windows: Registry key or Start Menu shortcut in Startup folder
**This will be implemented as a sub-task within this feature**, but the checkbox should be present and functional. The actual OS integration can be a follow-up if it proves complex.
## Validation Rules
- WoW path: must exist, must contain WTF/ or Interface/ or a WoW executable
- Backup path: must exist and be writable
- At least one folder (WTF or Interface) must be selected
- Backup history count: 1-30
## Verification
1. Config screen renders with all sections
2. "Browse" buttons open native OS folder picker
3. "Auto-detect" finds WoW if installed in default location
4. Toggling "Force backup" shows confirmation dialog
5. "Save" persists to `config.json`, "Cancel" reverts
6. Validation errors display inline for invalid paths
7. On first run, config screen opens automatically
8. After saving valid config, navigates to status screen

View file

@ -0,0 +1,144 @@
# Feature 3: Backup
## Context
This is the core functionality — automatically backing up WoW's WTF and Interface folders on a daily schedule. The backup runs in the background, respects configuration, and notifies the user of results.
## Dependencies
- **Depends on**: Feature 0 (ConfigManager, logging, platform), Feature 2 (config must be complete)
- **Depended on by**: Feature 4 (Status — displays backup state), Feature 5 (Restore — reads backup history)
## Key Decisions
- **Compression**: ZIP format using `java.util.zip` (no extra deps)
- **Process detection**: `ProcessHandle.allProcesses()` to check if WoW is running
- **Scheduling**: Coroutine-based scheduler, checks every minute if it's time to run
- **Backup naming**: Timestamped folders like `2024-01-15_03-00-00/` (or `.zip`)
## Implementation Steps
### Step 1: WoW process detection
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/WoWProcessDetector.kt`
```kotlin
object WoWProcessDetector {
fun isWoWRunning(): Boolean {
return ProcessHandle.allProcesses()
.filter { it.isAlive }
.anyMatch { handle ->
val name = handle.info().command().orElse("").lowercase()
name.contains("world of warcraft") || name.contains("wow.exe") || name.contains("wow-64")
}
}
}
```
### Step 2: File copy engine
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupEngine.kt`
Responsibilities:
- Copy selected folders (WTF, Interface) from WoW install to backup destination
- Support two modes: plain copy and ZIP compression
- Report progress: total files count, files copied so far, current file name
- Cancellable via coroutine cancellation
- Return a `BackupResult` (success/failure with details)
Progress model:
```kotlin
data class BackupProgress(
val totalFiles: Int,
val completedFiles: Int,
val currentFile: String,
)
sealed class BackupResult {
data class Success(val backupPath: File, val sizeBytes: Long, val durationMs: Long) : BackupResult()
data class Failure(val reason: String, val exception: Throwable? = null) : BackupResult()
}
```
For plain copy:
- Walk source directories, replicate structure in `<backupDir>/<timestamp>/`
For ZIP:
- Walk source directories, write to `<backupDir>/<timestamp>.zip` using `ZipOutputStream`
### Step 3: Backup history management
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupHistory.kt`
Responsibilities:
- List existing backups in the backup directory (sorted by date, newest first)
- Parse timestamps from folder/zip names
- Prune old backups exceeding `backupHistoryCount`
- Return list of `BackupEntry` for the Status and Restore screens
```kotlin
data class BackupEntry(
val path: File,
val timestamp: LocalDateTime,
val isCompressed: Boolean,
val sizeBytes: Long,
)
```
### Step 4: Backup scheduler
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupScheduler.kt`
Coroutine-based scheduler that:
- Runs in a `CoroutineScope` tied to the application lifecycle
- Checks every 60 seconds if the current time matches `backupTimeOfDay` (within a 1-minute window)
- Tracks whether today's backup has already run (to avoid duplicate runs)
- Before starting backup, checks:
1. Config is complete (`ConfigManager.isConfigured`)
2. WoW is not running (unless `forceBackupWhileRunning` is true)
- Delegates to `BackupEngine` for the actual work
- After backup, calls `BackupHistory.pruneOldBackups()`
- Exposes state as `StateFlow<SchedulerState>`:
```kotlin
data class SchedulerState(
val lastBackupTime: LocalDateTime? = null,
val lastBackupResult: BackupResult? = null,
val nextBackupTime: LocalDateTime? = null,
val isRunning: Boolean = false,
val currentProgress: BackupProgress? = null,
val isWoWRunning: Boolean = false,
)
```
### Step 5: Notifications integration
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/BackupNotifier.kt`
Uses `TrayState.sendNotification()` (from Compose Desktop's Tray) to send native notifications:
- Backup started: "Backup in progress..."
- Backup completed: "Backup complete (X files, Y MB)"
- Backup failed: "Backup failed: <reason>"
- Backup skipped (WoW running): "Backup skipped — WoW is running"
Only sends if `notificationsEnabled` is true in config.
### Step 6: Wire scheduler into app lifecycle
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt`
Start `BackupScheduler` when app launches. Stop when app exits. Pass `TrayState` to `BackupNotifier`.
### Step 7: Manual backup trigger
Add a "Backup Now" item to the tray context menu that triggers an immediate backup (bypassing the schedule, still respecting the WoW-running check).
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/main.kt` — add menu item
## Verification
1. Set backup time to 1 minute from now, verify backup runs automatically
2. Verify WTF and Interface folders are copied correctly to backup destination
3. With compression enabled, verify a valid `.zip` is created
4. With WoW running and force=false, verify backup is skipped with notification
5. With WoW running and force=true, verify backup proceeds
6. Verify old backups are pruned when exceeding history count
7. "Backup Now" from tray menu triggers immediate backup
8. Notifications appear for start/complete/fail (when enabled)
9. Check logs for detailed backup operation entries

View file

@ -0,0 +1,94 @@
# Feature 4: Status Screen
## Context
The status screen is the default view when the user clicks the tray icon. It gives a quick overview of backup health and provides shortcuts to key actions.
## Dependencies
- **Depends on**: Feature 0 (config), Feature 1 (screen routing), Feature 3 (SchedulerState, BackupHistory)
- **Depended on by**: Feature 5 (Restore — navigated to from status screen)
## Implementation Steps
### Step 1: Create the StatusViewModel
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusViewModel.kt`
Collects state from:
- `BackupScheduler.state` (StateFlow) — last backup, next backup, running status, progress, WoW status
- `ConfigManager.config` (StateFlow) — to check if configured
- `BackupHistory.listBackups()` — for backup count summary
Exposes a single `StatusUiState`:
```kotlin
data class StatusUiState(
val isConfigured: Boolean,
val lastBackupTime: String?, // human-readable relative time ("2 hours ago")
val lastBackupStatus: String?, // "Success" or "Failed: <reason>"
val nextBackupTime: String?, // "Today at 03:00" or "Tomorrow at 03:00"
val isWoWRunning: Boolean,
val forceBackupEnabled: Boolean,
val isBackupRunning: Boolean,
val backupProgress: BackupProgress?,
val totalBackups: Int,
)
```
### Step 2: Build the StatusScreen composable
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/status/StatusScreen.kt`
Layout (Material3 Card-based):
#### Missing Config Warning (if !isConfigured)
- Warning banner: "Configuration incomplete. Set up WoW Backup to get started."
- "Open Settings" button
#### Last Backup Card
- Icon + "Last backup: 2 hours ago" (or "Never" if no backups)
- Status badge: green "Success" / red "Failed: reason"
#### Next Backup Card
- "Next backup: Today at 03:00"
- If WoW is running: yellow indicator "WoW is running" + note about force backup status
#### Backup Progress (if running)
- `LinearProgressIndicator` with file count ("142 / 350 files")
- Current file name (truncated)
#### Action Buttons Row
- **Backup Now** — triggers immediate backup (disabled if already running)
- **Open Backup Folder** — opens backup directory in OS file manager
- **Settings** — navigates to config screen
- **Restore** — navigates to restore screen
### Step 3: "Open folder" utility
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/platform/DesktopActions.kt`
```kotlin
object DesktopActions {
fun openFolder(path: File) {
Desktop.getDesktop().open(path)
}
}
```
Uses `java.awt.Desktop.open()` — works cross-platform.
### Step 4: Wire into App.kt routing
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt`
Replace status placeholder with `StatusScreen(viewModel, onNavigateToConfig, onNavigateToRestore)`.
## Verification
1. Status screen shows "Never backed up" on first run
2. After a successful backup, shows correct time and "Success" status
3. After a failed backup, shows failure reason in red
4. While backup is running, progress bar and file count update live
5. "Backup Now" triggers immediate backup
6. "Open Backup Folder" opens the correct directory in Finder/Explorer
7. "Settings" navigates to config screen
8. "Restore" navigates to restore screen
9. Missing config warning shows when config is incomplete
10. WoW running indicator updates in real-time

View file

@ -0,0 +1,121 @@
# Feature 5: Restore
## Context
Allows users to restore a previous backup, replacing their current WoW configuration. This is a destructive operation (overwrites current WTF/Interface folders), so it requires clear confirmation and progress feedback.
## Dependencies
- **Depends on**: Feature 0 (config, platform), Feature 1 (screen routing), Feature 3 (BackupHistory, BackupEntry)
- **Depended on by**: None (final feature)
## Implementation Steps
### Step 1: Restore engine
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/backup/RestoreEngine.kt`
Responsibilities:
- Given a `BackupEntry`, restore its contents to the WoW install directory
- Handle both plain folder backups and ZIP backups
- Process:
1. Verify source backup exists
2. Verify WoW install path exists
3. Delete existing WTF/ and/or Interface/ in WoW install (only the ones present in the backup)
4. Copy/extract backup contents to WoW install location
- Report progress: total files, files restored, current file
- Cancellable via coroutine cancellation
- Return `RestoreResult` (success/failure)
```kotlin
data class RestoreProgress(
val totalFiles: Int,
val completedFiles: Int,
val currentFile: String,
)
sealed class RestoreResult {
data class Success(val restoredFiles: Int, val durationMs: Long) : RestoreResult()
data class Failure(val reason: String, val exception: Throwable? = null) : RestoreResult()
}
```
### Step 2: RestoreViewModel
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/restore/RestoreViewModel.kt`
State:
```kotlin
data class RestoreUiState(
val backups: List<BackupEntry>, // available backups
val selectedBackup: BackupEntry?, // user selection
val showConfirmDialog: Boolean,
val isRestoring: Boolean,
val progress: RestoreProgress?,
val result: RestoreResult?,
)
```
Actions:
- `selectBackup(entry)` — select a backup from the list
- `confirmRestore()` — show confirmation dialog
- `startRestore()` — begin restore process
- `cancelRestore()` — cancel in-progress restore
- `dismiss()` — clear result and return to list
### Step 3: Build the RestoreScreen composable
**Files to create:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/ui/restore/RestoreScreen.kt`
Layout:
#### Header
- "Restore Backup" title
- "Open Backup Folder" button
#### Backup List
- Scrollable list of all existing backups
- Each row shows:
- Date/time in human-readable format ("January 15, 2024 at 3:00 AM")
- Size (e.g., "142 MB")
- Type badge: "Compressed" or "Folder"
- "Restore" button
- Empty state: "No backups found. Run a backup first."
#### Confirmation Dialog (reuses ConfirmationDialog from Feature 2)
- Title: "Restore Backup?"
- Message: "This will replace your current WoW settings (WTF and Interface folders) with the backup from [date]. This action cannot be undone. Make sure WoW is not running."
- Buttons: "Cancel" / "Restore" (destructive style)
#### Restore Progress (replaces list while restoring)
- `LinearProgressIndicator` with percentage
- File count: "142 / 350 files"
- Current file name
- Estimated time remaining (based on average file copy speed)
- "Cancel" button
#### Result
- Success: "Restore complete. X files restored."
- Failure: "Restore failed: <reason>". Show "Try Again" button.
### Step 4: Wire into App.kt routing
**Files to modify:**
- `composeApp/src/jvmMain/kotlin/com/rukira/wowbackup/App.kt`
Replace restore placeholder with `RestoreScreen(viewModel, onNavigateToStatus)`.
## Edge Cases
- WoW is running during restore → warn user but allow (they confirmed)
- Backup file is corrupted/missing → show clear error
- Insufficient disk space → detect before starting, show error
- Restore cancelled mid-way → partial state warning: "Restore was cancelled. Your WoW configuration may be in an incomplete state."
## Verification
1. Restore screen lists all existing backups with correct dates and sizes
2. Empty state shows when no backups exist
3. Selecting "Restore" shows confirmation dialog with correct date
4. Cancelling dialog returns to list without action
5. Confirming starts restore with live progress indicator
6. Cancel button during restore stops the operation
7. Successful restore copies all files to WoW install directory
8. ZIP backups are extracted correctly
9. Folder backups are copied correctly
10. Error states display clearly (corrupted backup, missing paths)