Compare commits
4 commits
195cbe8f7c
...
8b054cd33d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b054cd33d | ||
|
|
846794fa36 | ||
|
|
59b296d04d | ||
|
|
d07f0f31bd |
6 changed files with 240 additions and 272 deletions
|
|
@ -62,6 +62,7 @@ body:
|
|||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ body:
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ compose.desktop {
|
|||
application {
|
||||
mainClass = "com.rukira.wowbackup.MainKt"
|
||||
|
||||
jvmArgs("-Dapple.awt.enableTemplateImages=true")
|
||||
jvmArgs("-Dapple.awt.enableTemplateImages=true", "-Dapple.awt.UIElement=true")
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
|
|
@ -82,6 +82,12 @@ compose.desktop {
|
|||
macOS {
|
||||
bundleID = "com.rukira.wowbackup"
|
||||
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
||||
infoPlist {
|
||||
extraKeysRawXml = """
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
windows {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import java.awt.GraphicsEnvironment
|
|||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun main() {
|
||||
if (Platform.current == OS.Mac) {
|
||||
System.setProperty("apple.awt.UIElement", "true")
|
||||
}
|
||||
LoggingSetup.init()
|
||||
logger.info { "WoW Backup starting" }
|
||||
|
||||
|
|
|
|||
|
|
@ -63,214 +63,234 @@ fun ConfigScreen(
|
|||
var showForceBackupDialog by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
// === WoW Installation ===
|
||||
SectionHeader("WoW Installation")
|
||||
// === WoW Installation ===
|
||||
SectionHeader("WoW Installation")
|
||||
|
||||
PathField(
|
||||
label = "WoW install location",
|
||||
value = config.wowInstallPath ?: "",
|
||||
error = errors["wowPath"],
|
||||
onBrowse = {
|
||||
val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) })
|
||||
if (dir != null) viewModel.updateWoWPath(dir.absolutePath)
|
||||
},
|
||||
)
|
||||
PathField(
|
||||
label = "WoW install location",
|
||||
value = config.wowInstallPath ?: "",
|
||||
error = errors["wowPath"],
|
||||
onBrowse = {
|
||||
val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) })
|
||||
if (dir != null) viewModel.updateWoWPath(dir.absolutePath)
|
||||
},
|
||||
)
|
||||
|
||||
// WoW path warnings (e.g. missing WTF/Interface, permission issues)
|
||||
uiState.warnings["wowPath"]?.forEach { warning ->
|
||||
Text(
|
||||
warning,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
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"
|
||||
// WoW path warnings (e.g. missing WTF/Interface, permission issues)
|
||||
uiState.warnings["wowPath"]?.forEach { warning ->
|
||||
Text(
|
||||
warning,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
val interfaceStatus = when {
|
||||
validation.interfaceReadable -> "Interface folder: found"
|
||||
validation.hasInterface -> "Interface folder: found (not readable)"
|
||||
else -> "Interface folder: not found"
|
||||
|
||||
// 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"
|
||||
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(
|
||||
"$wtfStatus | $interfaceStatus",
|
||||
"Theme changes apply immediately.",
|
||||
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) },
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
"Theme changes apply immediately.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
// === Footer ===
|
||||
Spacer(Modifier.height(8.dp))
|
||||
// === Sticky Footer ===
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
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
|
||||
if (showForceBackupDialog) {
|
||||
ConfirmationDialog(
|
||||
|
|
|
|||
|
|
@ -84,18 +84,31 @@ Strip all template code. Route to placeholder screens based on `currentScreen`:
|
|||
### 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
|
||||
### Step 6: macOS JVM args and Dock hiding (LSUIElement)
|
||||
**Files to modify:**
|
||||
- `composeApp/build.gradle.kts` — add to desktop application config:
|
||||
|
||||
```kotlin
|
||||
compose.desktop {
|
||||
application {
|
||||
jvmArgs("-Dapple.awt.enableTemplateImages=true")
|
||||
jvmArgs("-Dapple.awt.enableTemplateImages=true", "-Dapple.awt.UIElement=true")
|
||||
|
||||
nativeDistributions {
|
||||
macOS {
|
||||
infoPlist {
|
||||
extraKeysRawXml = """
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also set `System.setProperty("apple.awt.UIElement", "true")` in `main.kt` for macOS so that the dock icon is suppressed during development runs.
|
||||
|
||||
## Verification
|
||||
1. `./gradlew composeApp:run` — app launches with tray icon visible
|
||||
2. Tray icon shows context menu with Status, Settings, Quit
|
||||
|
|
|
|||
|
|
@ -4,75 +4,13 @@ This document outlines the current state of the project, remaining core features
|
|||
|
||||
---
|
||||
|
||||
## 1. Immediate Priority: Feature 5 (Restore System)
|
||||
## Immediate Priority: Feature 5 (Restore System)
|
||||
|
||||
The core backup and scheduling pipeline is complete, but the restoration flow is currently a stub placeholder. Implementing Feature 5 is the final step to complete the original functional scope of `docs/masterplan.md`.
|
||||
|
||||
### Tasks:
|
||||
1. **Restore Engine (`RestoreEngine.kt`)**
|
||||
- Implement `restoreBackup(entry: BackupEntry, targetDir: File): RestoreResult`.
|
||||
- Support restoring from both timestamped directory structures and `.zip` archives (extracting using `ZipInputStream`).
|
||||
- Clean/delete existing target folders (`WTF` and/or `Interface`) safely before copy/extraction.
|
||||
- Implement live progress reporting (`RestoreProgress` with total files, completed count, current file name).
|
||||
- Ensure full cancellation support via Kotlin Coroutines.
|
||||
2. **Safety & Validation Mechanics**
|
||||
- Warn or guard if World of Warcraft is currently running during restoration.
|
||||
- Verify backup integrity and destination disk space before starting destructive deletion.
|
||||
- Create an automatic temporary safety snapshot of current `WTF`/`Interface` folders prior to overwrite.
|
||||
3. **Restore ViewModel (`RestoreViewModel.kt`)**
|
||||
- Expose `RestoreUiState` combining available backups list, selected entry, progress, and confirmation states.
|
||||
- Manage restore lifecycle (start, cancel, dismiss, error handling).
|
||||
4. **Restore Screen UI (`RestoreScreen.kt`)**
|
||||
- Replace placeholder with scrollable list of existing backups displaying date/time, archive size, and format badge.
|
||||
- Material 3 confirmation dialog detailing destructive overwrite.
|
||||
- Progress overlay with `LinearProgressIndicator`, active file, and cancellation button.
|
||||
- Empty state when no backups are present.
|
||||
5. **App Wiring**
|
||||
- Connect `Screen.RESTORE` in `App.kt` to `RestoreScreen`.
|
||||
- Enable the "Restore" button on `StatusScreen.kt`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Testing Infrastructure & Automated Test Suite
|
||||
|
||||
Currently, the project contains no automated test suite (`src/jvmTest` is empty). Adding tests will prevent regressions during future maintenance.
|
||||
|
||||
### Test Targets:
|
||||
1. **`BackupHistoryTest`**:
|
||||
- Timestamp parsing across formats and edge cases.
|
||||
- Backup listing and correct descending sort order.
|
||||
- Retention policy pruning (ensuring only oldest backups exceeding count are deleted).
|
||||
- Metrics computation for directory trees and ZIP files.
|
||||
2. **`BackupEngineTest` & `RestoreEngineTest`**:
|
||||
- File copy accuracy, recursive folder preservation, and attribute retention.
|
||||
- ZIP compression and decompression fidelity.
|
||||
- Cancellation responsiveness and cleanup of partial writes.
|
||||
3. **`ConfigManagerTest`**:
|
||||
- JSON serialization / deserialization defaults and edge cases.
|
||||
- Thread-safe updates and reactive `StateFlow` emissions.
|
||||
4. **`WoWLocationsTest`**:
|
||||
- Install directory heuristics and resolution from root `World of Warcraft` to `_retail_` or `_classic_`.
|
||||
- Missing folder detection and permission warnings.
|
||||
|
||||
---
|
||||
|
||||
## 3. Build Automation & Release Pipeline (Completed)
|
||||
|
||||
Automated multi-platform packaging and release pipeline implemented via GitHub Actions (`.github/workflows/release.yml`) and documented in `INSTALL.md` and `docs/plans/feature-6-release-pipeline.md`.
|
||||
|
||||
### Implemented:
|
||||
1. **GitHub Actions Release Pipeline**
|
||||
- Automated semantic patch version computation and git tagging on push to `main` and `workflow_dispatch`.
|
||||
- Matrix builds on macOS (`macos-latest`), Windows (`windows-latest`), and Linux (`ubuntu-latest`).
|
||||
2. **Automated Distribution Packaging & GitHub Releases**
|
||||
- Automated artifact generation and release publishing for `.dmg` (macOS), `.msi` (Windows), and `.deb` (Linux).
|
||||
- Dynamic version injection via `-PappVersion` in `composeApp/build.gradle.kts`.
|
||||
3. **Installation Documentation**
|
||||
- Detailed `INSTALL.md` guide covering installation and security prompts (Gatekeeper / SmartScreen) across all platforms.
|
||||
|
||||
---
|
||||
|
||||
## 4. Future Enhancements (Post-v1.0)
|
||||
## Future Enhancements (Post-v1.0)
|
||||
|
||||
1. **Multi-Flavour & Account Profile Support**
|
||||
- Support backing up multiple game versions simultaneously (`_retail_`, `_classic_`, `_classic_era_`, `_ptr_`).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue