Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4fb6a25be | ||
|
|
2eeebee36d | ||
|
|
616a7a9a95 | ||
|
|
d0887d88ba | ||
|
|
ae845d422c | ||
|
|
5c1b8b3cc8 | ||
|
|
3e2fcf87a8 | ||
|
|
03b90e2afe | ||
|
|
58f493e561 | ||
|
|
629ae6899a | ||
|
|
f6d754ff0a | ||
|
|
2ccdb02ba7 | ||
|
|
0784d7ce3f | ||
|
|
9a9b42a88c | ||
|
|
8bc24446fa | ||
|
|
fc4451bfbc | ||
|
|
6e5fd227bf | ||
|
|
e873f14a7b | ||
|
|
b7bff1a7bd | ||
|
|
7056758728 | ||
|
|
65962f3581 | ||
|
|
472654180a | ||
|
|
3a7a92800b | ||
|
|
892448c57f | ||
|
|
5be2247cb4 | ||
|
|
5b4798b7b2 | ||
|
|
88e64b25c8 | ||
|
|
0ea2067e42 | ||
|
|
033febece6 | ||
|
|
12e6a6f94b | ||
|
|
31a3cbc327 |
15 changed files with 353 additions and 315 deletions
|
|
@ -62,6 +62,7 @@ body:
|
||||||
options:
|
options:
|
||||||
- macOS
|
- macOS
|
||||||
- Windows
|
- Windows
|
||||||
|
- Linux
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|
@ -80,6 +81,7 @@ body:
|
||||||
Logs help us understand what went wrong behind the scenes. Here's how to find them:
|
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`
|
**macOS:** Open Finder, press **Cmd+Shift+G**, and paste: `~/Library/Application Support/WoWBackup/logs`
|
||||||
|
|
||||||
**Windows:** Press **Win+R**, and paste: `%APPDATA%\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.
|
You can also open this folder from the app: go to **Settings** and click the **Open Logs** button at the bottom.
|
||||||
|
|
|
||||||
32
.github/workflows/release.yml
vendored
32
.github/workflows/release.yml
vendored
|
|
@ -154,7 +154,7 @@ jobs:
|
||||||
#### Available Packages
|
#### Available Packages
|
||||||
- **macOS**: \`.dmg\` installer (Apple Silicon & Intel)
|
- **macOS**: \`.dmg\` installer (Apple Silicon & Intel)
|
||||||
- **Windows**: \`.msi\` Windows Installer
|
- **Windows**: \`.msi\` Windows Installer
|
||||||
- **Linux**: \`.deb\` package (Debian / Ubuntu)
|
- **Linux (Experimental)**: \`.deb\` package (Debian / Ubuntu)
|
||||||
|
|
||||||
---
|
---
|
||||||
For detailed setup and installation notes, please refer to [INSTALL.md](https://git.asarius.site/rukira/wow-backup/src/tag/${RELEASE_TAG}/INSTALL.md)."
|
For detailed setup and installation notes, please refer to [INSTALL.md](https://git.asarius.site/rukira/wow-backup/src/tag/${RELEASE_TAG}/INSTALL.md)."
|
||||||
|
|
@ -206,3 +206,33 @@ jobs:
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Forgejo release published successfully."
|
echo "Forgejo release published successfully."
|
||||||
|
|
||||||
|
- name: Update Homebrew Tap Cask
|
||||||
|
env:
|
||||||
|
FORGEJO_PAT: ${{ secrets.FORGEJO_PAT }}
|
||||||
|
RELEASE_VERSION: ${{ needs.bump-version.outputs.version }}
|
||||||
|
run: |
|
||||||
|
DMG_FILE=$(find dist -name "*.dmg" | head -n 1)
|
||||||
|
if [ -z "$DMG_FILE" ] || [ ! -f "$DMG_FILE" ]; then
|
||||||
|
echo "::error::No DMG file found in dist/ to update Homebrew Cask."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "$DMG_FILE" | awk '{print $1}')
|
||||||
|
echo "Calculated DMG SHA256: ${SHA256} for version ${RELEASE_VERSION}"
|
||||||
|
|
||||||
|
echo "Cloning homebrew-tap repository..."
|
||||||
|
git clone "https://rukira:${FORGEJO_PAT}@git.asarius.site/rukira/homebrew-tap.git" tap-repo
|
||||||
|
cd tap-repo
|
||||||
|
|
||||||
|
echo "Updating Casks/wow-backup.rb..."
|
||||||
|
sed -i "s/version \".*\"/version \"${RELEASE_VERSION}\"/" Casks/wow-backup.rb
|
||||||
|
sed -i "s/sha256 \".*\"/sha256 \"${SHA256}\"/" Casks/wow-backup.rb
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add Casks/wow-backup.rb
|
||||||
|
git commit -m "chore(cask): update wow-backup to v${RELEASE_VERSION}" || echo "No changes to commit"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
echo "Homebrew tap updated successfully."
|
||||||
|
|
|
||||||
72
INSTALL.md
72
INSTALL.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Download the appropriate installer for your platform from the **[latest release](https://git.asarius.site/rukira/wow-backup/releases)**:
|
Download the appropriate installer for your platform from the **[latest release](https://git.asarius.site/rukira/wow-backup/releases/latest)**:
|
||||||
|
|
||||||
| Platform | Package Format | Installer File Pattern |
|
| Platform | Package Format | Installer File Pattern |
|
||||||
|--------------------------|---|---|
|
|--------------------------|---|---|
|
||||||
|
|
@ -10,23 +10,7 @@ Download the appropriate installer for your platform from the **[latest release]
|
||||||
| **Windows** | Windows Installer (MSI) | `WoW Backup-<version>.msi` |
|
| **Windows** | Windows Installer (MSI) | `WoW Backup-<version>.msi` |
|
||||||
| **Linux** (Experimental) | Debian Package (DEB) | `wow-backup_<version>_amd64.deb` |
|
| **Linux** (Experimental) | Debian Package (DEB) | `wow-backup_<version>_amd64.deb` |
|
||||||
|
|
||||||
---
|
_Note: The Linux version is untested and experimental. Use at your own risk._
|
||||||
|
|
||||||
## macOS
|
|
||||||
|
|
||||||
1. Download the `.dmg` file from the latest release.
|
|
||||||
2. Double-click the `.dmg` file to mount the disk image.
|
|
||||||
3. Drag **WoW Backup.app** into your `/Applications` folder.
|
|
||||||
4. Eject the disk image.
|
|
||||||
|
|
||||||
### Unsigned Application / Gatekeeper Notice
|
|
||||||
Because WoW Backup is distributed without Apple Developer ID code-signing notarization, macOS Gatekeeper may show a warning on first launch (*"WoW Backup cannot be opened because the developer cannot be verified"* or *"Apple could not verify that WoW Backup is free of malware"*).
|
|
||||||
|
|
||||||
To launch the app for the first time:
|
|
||||||
1. Open **System Settings**
|
|
||||||
2. Navigate to **Security & Privacy**.
|
|
||||||
3. Click **Open Anyway** under **Allow apps downloaded from**.
|
|
||||||
4. Restart WoW Backup.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -44,6 +28,58 @@ If Windows Defender SmartScreen flags the installer (*"Windows protected your PC
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
### Option 1: Manual DMG Download
|
||||||
|
|
||||||
|
1. Download the `.dmg` file from the **[latest release](https://git.asarius.site/rukira/wow-backup/releases/latest)**.
|
||||||
|
2. Double-click the `.dmg` file to mount the disk image.
|
||||||
|
3. Drag **WoW Backup.app** into your `/Applications` folder.
|
||||||
|
4. Eject the disk image.
|
||||||
|
|
||||||
|
#### Unsigned Application / Gatekeeper Notice
|
||||||
|
Because WoW Backup is distributed without Apple Developer ID code-signing notarization, macOS Gatekeeper may show a warning on first launch (*"WoW Backup cannot be opened because the developer cannot be verified"* or *"Apple could not verify that WoW Backup is free of malware"*).
|
||||||
|
|
||||||
|
To launch the app:
|
||||||
|
|
||||||
|
- **Terminal (Fastest):** Run the following command to strip the quarantine attribute:
|
||||||
|
```shell
|
||||||
|
xattr -cr "/Applications/WoW Backup.app"
|
||||||
|
```
|
||||||
|
- **macOS System Settings:**
|
||||||
|
1. Open **System Settings** → **Privacy & Security**.
|
||||||
|
2. Scroll down to the **Security** section.
|
||||||
|
3. Click **Open Anyway** next to the WoW Backup notice.
|
||||||
|
4. Confirm and launch the app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Install via [Homebrew](https://brew.sh/)
|
||||||
|
|
||||||
|
**Install:**
|
||||||
|
1. Add the tap:
|
||||||
|
```shell
|
||||||
|
brew tap rukira/tap https://git.asarius.site/rukira/homebrew-tap.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install WoW Backup:
|
||||||
|
```shell
|
||||||
|
brew install --cask wow-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update:**
|
||||||
|
```shell
|
||||||
|
brew update
|
||||||
|
brew upgrade --cask wow-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Uninstall:**
|
||||||
|
```shell
|
||||||
|
brew uninstall --cask --zap wow-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Linux (Experimental)
|
## Linux (Experimental)
|
||||||
|
|
||||||
1. Download the `.deb` package from the latest release.
|
1. Download the `.deb` package from the latest release.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
# WoW Backup
|

|
||||||
|
|
||||||
A lightweight, native-feeling desktop application to automatically backup World of Warcraft configuration data (WTF and Interface directories).
|
|
||||||
|
|
||||||
World of Warcraft stores all UI configurations, macros, keybindings, and addon data in the `WTF/` and `Interface/` folders. Losing this data due to game corruption, accidental deletion, or addon errors can mean losing years of customized setups. **WoW Backup** runs discreetly in the system tray, and automatically performs scheduled, timestamped backups (optionally compressed as ZIP archives).
|
|
||||||
|
|
||||||
## Installation & Download
|
## Installation & Download
|
||||||
|
|
||||||
|
|
@ -75,4 +71,4 @@ The app uses a configuration file to store user preferences. This file and runti
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is released into the public domain. See the [LICENSE](LICENSE) file or [unlicense.org](https://unlicense.org/) for details.
|
This project is released into the public domain. See the [UNLICENSE](UNLICENSE) file or [unlicense.org](https://unlicense.org/) for details.
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ kotlin {
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(compose.runtime)
|
implementation(libs.compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(libs.compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(libs.compose.material3)
|
||||||
implementation(compose.ui)
|
implementation(libs.compose.ui)
|
||||||
implementation(compose.components.resources)
|
implementation(libs.compose.components.resources)
|
||||||
implementation(compose.components.uiToolingPreview)
|
implementation(libs.compose.components.uiToolingPreview)
|
||||||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +42,8 @@ val appVersion = providers.gradleProperty("appVersion").orNull?.takeIf { it.isNo
|
||||||
|
|
||||||
val generatedSrcDir = layout.buildDirectory.dir("generated/src/jvmMain/kotlin")
|
val generatedSrcDir = layout.buildDirectory.dir("generated/src/jvmMain/kotlin")
|
||||||
|
|
||||||
val generateBuildConfig by tasks.registering {
|
val generateBuildConfig = tasks.register("generateBuildConfig") {
|
||||||
|
description = "Generates BuildConfig.kt"
|
||||||
val outputDir = generatedSrcDir
|
val outputDir = generatedSrcDir
|
||||||
val version = appVersion
|
val version = appVersion
|
||||||
inputs.property("version", version)
|
inputs.property("version", version)
|
||||||
|
|
@ -70,18 +71,24 @@ compose.desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "com.rukira.wowbackup.MainKt"
|
mainClass = "com.rukira.wowbackup.MainKt"
|
||||||
|
|
||||||
jvmArgs("-Dapple.awt.enableTemplateImages=true")
|
jvmArgs("-Dapple.awt.enableTemplateImages=true", "-Dapple.awt.UIElement=true")
|
||||||
|
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
packageName = "WoW Backup"
|
packageName = "WoW Backup"
|
||||||
packageVersion = appVersion
|
packageVersion = appVersion
|
||||||
|
|
||||||
modules("java.naming", "java.sql", "java.instrument", "jdk.unsupported")
|
modules("java.naming", "java.sql", "java.instrument", "jdk.unsupported", "java.net.http")
|
||||||
|
|
||||||
macOS {
|
macOS {
|
||||||
bundleID = "com.rukira.wowbackup"
|
bundleID = "com.rukira.wowbackup"
|
||||||
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
||||||
|
infoPlist {
|
||||||
|
extraKeysRawXml = """
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<true/>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
windows {
|
windows {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ import java.awt.GraphicsEnvironment
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
if (Platform.current == OS.Mac) {
|
||||||
|
System.setProperty("apple.awt.UIElement", "true")
|
||||||
|
}
|
||||||
LoggingSetup.init()
|
LoggingSetup.init()
|
||||||
logger.info { "WoW Backup starting" }
|
logger.info { "WoW Backup starting" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,214 +63,234 @@ fun ConfigScreen(
|
||||||
var showForceBackupDialog by remember { mutableStateOf(false) }
|
var showForceBackupDialog by remember { mutableStateOf(false) }
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.weight(1f)
|
||||||
.verticalScroll(scrollState)
|
.fillMaxWidth(),
|
||||||
.padding(24.dp),
|
) {
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
Column(
|
||||||
) {
|
modifier = Modifier
|
||||||
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
// === WoW Installation ===
|
// === WoW Installation ===
|
||||||
SectionHeader("WoW Installation")
|
SectionHeader("WoW Installation")
|
||||||
|
|
||||||
PathField(
|
PathField(
|
||||||
label = "WoW install location",
|
label = "WoW install location",
|
||||||
value = config.wowInstallPath ?: "",
|
value = config.wowInstallPath ?: "",
|
||||||
error = errors["wowPath"],
|
error = errors["wowPath"],
|
||||||
onBrowse = {
|
onBrowse = {
|
||||||
val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) })
|
val dir = pickFolder("Select WoW Install Folder", config.wowInstallPath?.let { File(it) })
|
||||||
if (dir != null) viewModel.updateWoWPath(dir.absolutePath)
|
if (dir != null) viewModel.updateWoWPath(dir.absolutePath)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// WoW path warnings (e.g. missing WTF/Interface, permission issues)
|
// WoW path warnings (e.g. missing WTF/Interface, permission issues)
|
||||||
uiState.warnings["wowPath"]?.forEach { warning ->
|
uiState.warnings["wowPath"]?.forEach { warning ->
|
||||||
Text(
|
Text(
|
||||||
warning,
|
warning,
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
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"
|
|
||||||
}
|
}
|
||||||
val interfaceStatus = when {
|
|
||||||
validation.interfaceReadable -> "Interface folder: found"
|
// WoW validation summary when valid
|
||||||
validation.hasInterface -> "Interface folder: found (not readable)"
|
uiState.wowValidation?.let { validation ->
|
||||||
else -> "Interface folder: not found"
|
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(
|
Text(
|
||||||
"$wtfStatus | $interfaceStatus",
|
"Theme changes apply immediately.",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedButton(onClick = { viewModel.detectWoWLocation() }) {
|
VerticalScrollbar(
|
||||||
Text("Auto-detect")
|
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||||
}
|
adapter = rememberScrollbarAdapter(scrollState),
|
||||||
|
style = ScrollbarStyle(
|
||||||
// === Backup Settings ===
|
minimalHeight = 48.dp,
|
||||||
SectionHeader("Backup Settings")
|
thickness = 8.dp,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
PathField(
|
hoverDurationMillis = 300,
|
||||||
label = "Backup destination",
|
unhoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
|
||||||
value = config.backupPath ?: "",
|
hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||||
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(
|
// === Sticky Footer ===
|
||||||
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))
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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
|
// Force backup confirmation dialog
|
||||||
if (showForceBackupDialog) {
|
if (showForceBackupDialog) {
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ fun WoWBackupTheme(
|
||||||
|
|
||||||
DynamicMaterialTheme(
|
DynamicMaterialTheme(
|
||||||
seedColor = accentColor.seedColor,
|
seedColor = accentColor.seedColor,
|
||||||
useDarkTheme = useDarkTheme,
|
isDark = useDarkTheme,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,18 +84,31 @@ Strip all template code. Route to placeholder screens based on `currentScreen`:
|
||||||
### Step 5: Auto-show config if not configured
|
### 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`.
|
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:**
|
**Files to modify:**
|
||||||
- `composeApp/build.gradle.kts` — add to desktop application config:
|
- `composeApp/build.gradle.kts` — add to desktop application config:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
application {
|
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
|
## Verification
|
||||||
1. `./gradlew composeApp:run` — app launches with tray icon visible
|
1. `./gradlew composeApp:run` — app launches with tray icon visible
|
||||||
2. Tray icon shows context menu with Status, Settings, Quit
|
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`.
|
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
|
## Future Enhancements (Post-v1.0)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
1. **Multi-Flavour & Account Profile Support**
|
1. **Multi-Flavour & Account Profile Support**
|
||||||
- Support backing up multiple game versions simultaneously (`_retail_`, `_classic_`, `_classic_era_`, `_ptr_`).
|
- Support backing up multiple game versions simultaneously (`_retail_`, `_classic_`, `_classic_era_`, `_ptr_`).
|
||||||
|
|
|
||||||
BIN
docs/screenshots/hero.png
Normal file
BIN
docs/screenshots/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 259 KiB |
BIN
docs/screenshots/wow-backup-hero.xcf
Normal file
BIN
docs/screenshots/wow-backup-hero.xcf
Normal file
Binary file not shown.
|
|
@ -1,18 +1,25 @@
|
||||||
[versions]
|
[versions]
|
||||||
androidx-lifecycle = "2.10.0-alpha08"
|
androidx-lifecycle = "2.11.0"
|
||||||
composeMultiplatform = "1.11.0-alpha03"
|
composeMaterial3 = "1.9.0"
|
||||||
|
composeMultiplatform = "1.11.1"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.2.21"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.11.0"
|
||||||
kotlinx-datetime = "0.6.2"
|
kotlinx-datetime = "0.8.0"
|
||||||
kotlinx-serialization = "1.10.0"
|
kotlinx-serialization = "1.11.0"
|
||||||
cardiologist = "0.8.0"
|
cardiologist = "0.8.0"
|
||||||
kotlin-logging = "7.0.3"
|
kotlin-logging = "8.0.4"
|
||||||
logback = "1.5.18"
|
logback = "1.5.38"
|
||||||
materialKolor = "2.0.0"
|
materialKolor = "4.0.5"
|
||||||
|
|
||||||
[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" }
|
||||||
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||||
|
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
|
||||||
|
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
|
||||||
|
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMaterial3" }
|
||||||
|
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
|
||||||
|
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
|
||||||
|
compose-components-uiToolingPreview = { module = "org.jetbrains.compose.components:components-ui-tooling-preview", version.ref = "composeMultiplatform" }
|
||||||
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
|
|
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue