-
diff --git a/package.json b/package.json
index 7226f0f..c3f715d 100644
--- a/package.json
+++ b/package.json
@@ -18,9 +18,9 @@
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1",
+ "@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-shell": ">=2.0.0",
"markdown-it": "^14.1.0",
- "moment": "^2.30.1",
"nuxt": "^3.13.0",
"scss": "^0.2.4",
"vue": "latest",
diff --git a/pages/account.vue b/pages/account.vue
new file mode 100644
index 0000000..6a072de
--- /dev/null
+++ b/pages/account.vue
@@ -0,0 +1,72 @@
+
+
+
+
+ Account
+
+
+
+
+
+
+
+
+
+
Sign out
+
+ Sign out of your Drop account on this device
+
+
+
+ Sign out
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/auth/failed.vue b/pages/auth/failed.vue
index db8a6e5..1eca3c0 100644
--- a/pages/auth/failed.vue
+++ b/pages/auth/failed.vue
@@ -26,7 +26,7 @@
import { XCircleIcon } from "@heroicons/vue/16/solid";
const route = useRoute();
-const message = route.query.error ?? "An unknown error occurred.";
+const message = route.query.error ?? "An unknown error occurred";
definePageMeta({
layout: "mini",
diff --git a/pages/index.vue b/pages/index.vue
index 72433ae..52bc1df 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -1,4 +1,7 @@
diff --git a/pages/library.vue b/pages/library.vue
index 8acd519..ab5079a 100644
--- a/pages/library.vue
+++ b/pages/library.vue
@@ -1,56 +1,75 @@
-
+
-
+
-
+
+ {{ nav.label }}
+
-
+
diff --git a/pages/library/index.vue b/pages/library/index.vue
index 61c7765..dd245b6 100644
--- a/pages/library/index.vue
+++ b/pages/library/index.vue
@@ -1,3 +1,9 @@
+
-
+
+ Library Failed to update
+
+
\ No newline at end of file
diff --git a/pages/queue.vue b/pages/queue.vue
index 6572203..b44f179 100644
--- a/pages/queue.vue
+++ b/pages/queue.vue
@@ -1,27 +1,46 @@
-
+
+
+
+ {{ formatKilobytes(stats.speed) }}/s
+ {{ formatTime(stats.time) }} left
+
+
+
-
+
- {{ games[element.id].game.mName }}
+ {{ games[element.meta.id].game.mName }}
- {{ games[element.id].game.mShortDescription }}
+ {{ games[element.meta.id].game.mShortDescription }}
@@ -39,8 +58,17 @@
:style="{ width: `${element.progress * 100}%` }"
/>
+
{{
+ formatKilobytes(element.current / 1000)
+ }}
+ /
+ {{ formatKilobytes(element.max / 1000) }}
- cancelGame(element.id)" class="group">
+ cancelGame(element.meta)" class="group">
diff --git a/pages/settings.vue b/pages/settings.vue
index 1e92a0f..3c39c65 100644
--- a/pages/settings.vue
+++ b/pages/settings.vue
@@ -9,25 +9,18 @@
-
+
-
+ ? 'text-zinc-100'
+ : 'text-zinc-400 group-hover:text-zinc-200',
+ 'transition h-6 w-6 shrink-0',
+ ]" aria-hidden="true" />
{{ item.label }}
@@ -43,13 +36,53 @@
diff --git a/pages/settings/debug.vue b/pages/settings/debug.vue
new file mode 100644
index 0000000..7a549ca
--- /dev/null
+++ b/pages/settings/debug.vue
@@ -0,0 +1,136 @@
+
+
+
+
+ Debug Information
+
+
+ Technical information about your Drop client installation, helpful for
+ debugging.
+
+
+
+
+
+
+
+ Client ID
+
+
+
+ {{ clientId || "Not signed in" }}
+
+
+
+
+
+
+
+ Platform
+
+
+
+ {{ platformInfo }}
+
+
+
+
+
+
+
+ Server URL
+
+
+
+ {{ baseUrl || "Not connected" }}
+
+
+
+
+
+
+
+ Data Directory
+
+
+
+ {{ dataDir || "Unknown" }}
+
+
+
+
+ openDataDir()"
+ type="button"
+ class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
+ >
+
+ Open Data Directory
+
+ openLogFile()"
+ type="button"
+ class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
+ >
+
+ Open Log File
+
+
+
+
+
+
+
+
diff --git a/pages/settings/downloads.vue b/pages/settings/downloads.vue
index d7e7d53..c03fa78 100644
--- a/pages/settings/downloads.vue
+++ b/pages/settings/downloads.vue
@@ -59,6 +59,54 @@
+
+
+ Download Settings
+
+
+ Configure how Drop downloads games and other content.
+
+
+
+
+ Maximum Download Threads
+
+
+
+
+
+ The maximum number of concurrent download threads. Higher values may
+ download faster but use more system resources. Default is 4.
+
+
+
+
+
+ {{ saveState.success ? 'Saved' : 'Save Changes' }}
+
+
+
@@ -172,6 +220,7 @@ import {
} from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core";
+import { type Settings } from "~/types";
const open = ref(false);
const currentDirectory = ref(undefined);
@@ -180,6 +229,14 @@ const createDirectoryLoading = ref(false);
const dirs = ref>([]);
+const settings = await invoke("fetch_settings");
+const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
+
+const saveState = reactive({
+ loading: false,
+ success: false
+});
+
async function updateDirs() {
const newDirs = await invoke>("fetch_download_dir_stats");
dirs.value = newDirs;
@@ -213,7 +270,7 @@ async function submitDirectory() {
try {
error.value = undefined;
if (!currentDirectory.value)
- throw new Error("Please select a directory first.");
+ throw new Error("Please select a directory first");
createDirectoryLoading.value = true;
// Add directory
@@ -235,4 +292,42 @@ async function deleteDirectory(index: number) {
await invoke("delete_download_dir", { index });
await updateDirs();
}
+
+async function saveDownloadThreads() {
+ try {
+ saveState.loading = true;
+ await invoke("update_settings", {
+ newSettings: { maxDownloadThreads: downloadThreads.value },
+ });
+
+ // Show success state
+ saveState.success = true;
+
+ // Reset back to normal state after 2 seconds
+ setTimeout(() => {
+ saveState.success = false;
+ }, 2000);
+
+ } catch (error) {
+ console.error('Failed to save settings:', error);
+ } finally {
+ saveState.loading = false;
+ }
+}
+
+function validateNumberInput(event: KeyboardEvent) {
+ // Allow only numbers and basic control keys
+ if (!/^\d$/.test(event.key) &&
+ !['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
+ event.preventDefault();
+ }
+}
+
+function validatePaste(event: ClipboardEvent) {
+ // Prevent paste if content contains non-numeric characters
+ const pastedData = event.clipboardData?.getData('text');
+ if (pastedData && !/^\d+$/.test(pastedData)) {
+ event.preventDefault();
+ }
+}
diff --git a/pages/settings/index.vue b/pages/settings/index.vue
index 27e0f69..241c840 100644
--- a/pages/settings/index.vue
+++ b/pages/settings/index.vue
@@ -1,3 +1,60 @@
-
-
\ No newline at end of file
+
+
+
General
+
+ Configure basic application settings
+
+
+
+
+
+
Start with system
+
+ Drop will automatically start when you log into your computer
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/settings/interface.vue b/pages/settings/interface.vue
index 27e0f69..3df236b 100644
--- a/pages/settings/interface.vue
+++ b/pages/settings/interface.vue
@@ -1,3 +1,7 @@
-
\ No newline at end of file
+
+
+
diff --git a/plugins/global-error-handler.ts b/plugins/global-error-handler.ts
index f9d7249..b7bcd45 100644
--- a/plugins/global-error-handler.ts
+++ b/plugins/global-error-handler.ts
@@ -1,7 +1,7 @@
export default defineNuxtPlugin((nuxtApp) => {
// Also possible
nuxtApp.hook("vue:error", (error, instance, info) => {
- console.log(error);
+ console.error(error, info);
const router = useRouter();
router.replace(`/error`);
});
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 74a6576..22b5365 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -255,12 +255,29 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "atomic-instant-full"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe"
+
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "auto-launch"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
+dependencies = [
+ "dirs 4.0.0",
+ "thiserror 1.0.69",
+ "winreg 0.10.1",
+]
+
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -355,6 +372,12 @@ dependencies = [
"piper",
]
+[[package]]
+name = "boxcar"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
+
[[package]]
name = "brotli"
version = "7.0.0"
@@ -864,7 +887,16 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys 0.3.7",
]
[[package]]
@@ -873,7 +905,18 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
]
[[package]]
@@ -963,8 +1006,10 @@ dependencies = [
[[package]]
name = "drop-app"
-version = "0.1.0"
+version = "0.2.0-beta-prerelease-1"
dependencies = [
+ "atomic-instant-full",
+ "boxcar",
"chrono",
"directories",
"hex",
@@ -973,6 +1018,7 @@ dependencies = [
"log4rs",
"md5",
"openssl",
+ "parking_lot 0.12.3",
"rayon",
"reqwest",
"rustbreak",
@@ -980,13 +1026,20 @@ dependencies = [
"serde",
"serde-binary",
"serde_json",
+ "serde_with",
+ "shared_child",
+ "slice-deque",
"tauri",
"tauri-build",
+ "tauri-plugin-autostart",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
+ "tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
+ "throttle_my_fn",
"tokio",
+ "umu-wrapper-lib",
"url",
"urlencoding",
"uuid",
@@ -1037,7 +1090,7 @@ dependencies = [
"rustc_version",
"toml 0.8.2",
"vswhom",
- "winreg",
+ "winreg 0.52.0",
]
[[package]]
@@ -1435,6 +1488,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "gethostname"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
+dependencies = [
+ "rustix",
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -2288,7 +2351,7 @@ dependencies = [
"log",
"log-mdc",
"once_cell",
- "parking_lot",
+ "parking_lot 0.12.3",
"rand 0.8.5",
"serde",
"serde-value",
@@ -2306,6 +2369,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+[[package]]
+name = "mach"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -2853,6 +2925,17 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "os_info"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430"
+dependencies = [
+ "log",
+ "serde",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "os_pipe"
version = "1.2.1"
@@ -2894,6 +2977,17 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -2901,7 +2995,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.9.10",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
]
[[package]]
@@ -2912,7 +3020,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall",
+ "redox_syscall 0.5.8",
"smallvec",
"windows-targets 0.52.6",
]
@@ -3347,6 +3455,15 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.8"
@@ -3793,9 +3910,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.11.0"
+version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
+checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -3811,9 +3928,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.11.0"
+version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
+checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
@@ -3934,6 +4051,17 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "slice-deque"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
+dependencies = [
+ "libc",
+ "mach",
+ "winapi",
+]
+
[[package]]
name = "smallvec"
version = "1.13.2"
@@ -3966,7 +4094,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
- "redox_syscall",
+ "redox_syscall 0.5.8",
"wasm-bindgen",
"web-sys",
"windows-sys 0.59.0",
@@ -4024,7 +4152,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
dependencies = [
"new_debug_unreachable",
"once_cell",
- "parking_lot",
+ "parking_lot 0.12.3",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
@@ -4107,6 +4235,15 @@ dependencies = [
"syn 2.0.91",
]
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "system-configuration"
version = "0.6.1"
@@ -4168,7 +4305,7 @@ dependencies = [
"ndk-sys",
"objc",
"once_cell",
- "parking_lot",
+ "parking_lot 0.12.3",
"raw-window-handle",
"scopeguard",
"tao-macros",
@@ -4205,7 +4342,7 @@ checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c"
dependencies = [
"anyhow",
"bytes",
- "dirs",
+ "dirs 5.0.1",
"dunce",
"embed_plist",
"futures-util",
@@ -4255,7 +4392,7 @@ checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
dependencies = [
"anyhow",
"cargo_toml",
- "dirs",
+ "dirs 5.0.1",
"glob",
"heck 0.5.0",
"json-patch",
@@ -4327,6 +4464,20 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "tauri-plugin-autostart"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c13f843e5e5df3eed270fc42b02923cc1a6b5c7e56b0f3ac1d858ab2c8b5fb"
+dependencies = [
+ "auto-launch",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.9",
+]
+
[[package]]
name = "tauri-plugin-deep-link"
version = "2.2.0"
@@ -4388,6 +4539,24 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "tauri-plugin-os"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
+dependencies = [
+ "gethostname",
+ "log",
+ "os_info",
+ "serde",
+ "serde_json",
+ "serialize-to-javascript",
+ "sys-locale",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.9",
+]
+
[[package]]
name = "tauri-plugin-shell"
version = "2.2.0"
@@ -4597,6 +4766,18 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "throttle_my_fn"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "482c185e5675626c9a130b3a8f362c322a239338c882f745a1d9a85838b987f0"
+dependencies = [
+ "parking_lot 0.11.2",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "time"
version = "0.3.37"
@@ -4813,7 +4994,7 @@ checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b"
dependencies = [
"core-graphics",
"crossbeam-channel",
- "dirs",
+ "dirs 5.0.1",
"libappindicator",
"muda",
"objc2",
@@ -4870,6 +5051,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "umu-wrapper-lib"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa36636bef667cee9eb4f497c24279182b8b9f098fd04b0b8c5d2ebc4e451f1"
+
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -5713,6 +5900,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "winreg"
version = "0.52.0"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 64f0f9b..f9f24c3 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "drop-app"
-version = "0.1.0"
-description = "A Tauri App"
-authors = ["you"]
+version = "0.2.0-beta-prerelease-1"
+description = "The client application for the open-source, self-hosted game distribution platform Drop"
+authors = ["Drop OSS"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,7 +26,6 @@ tauri-build = { version = "2.0.0", features = [] }
[dependencies]
tauri-plugin-shell = "2.0.0"
-serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
serde-binary = "0.5.0"
rayon = "1.10.0"
@@ -41,12 +40,20 @@ http = "1.1.0"
urlencoding = "2.1.3"
md5 = "0.7.0"
chrono = "0.4.38"
+tauri-plugin-os = "2"
+boxcar = "0.2.7"
+umu-wrapper-lib = "0.1.0"
+tauri-plugin-autostart = "2.0.0"
+shared_child = "1.0.1"
+serde_with = "3.12.0"
+slice-deque = "0.3.0"
+throttle_my_fn = "0.2.6"
+parking_lot = "0.12.3"
+atomic-instant-full = "0.1.0"
[dependencies.tauri]
version = "2.1.1"
-features = [
- "tray-icon"
-]
+features = ["tray-icon"]
[dependencies.tokio]
@@ -81,6 +88,10 @@ features = [] # You can also use "yaml_enc" or "bin_enc"
version = "0.12"
features = ["json", "blocking"]
+[dependencies.serde]
+version = "1"
+features = ["derive", "rc"]
+
[profile.release]
lto = true
codegen-units = 1
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 4bafeab..1b818b3 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -13,6 +13,7 @@
"core:window:allow-maximize",
"core:window:allow-close",
"deep-link:default",
- "dialog:default"
+ "dialog:default",
+ "os:default"
]
}
\ No newline at end of file
diff --git a/src-tauri/rust-toolchain.toml b/src-tauri/rust-toolchain.toml
new file mode 100644
index 0000000..271800c
--- /dev/null
+++ b/src-tauri/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs
new file mode 100644
index 0000000..dec9148
--- /dev/null
+++ b/src-tauri/src/autostart.rs
@@ -0,0 +1,76 @@
+use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
+use log::debug;
+use tauri::AppHandle;
+use tauri_plugin_autostart::ManagerExt;
+
+pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
+ let manager = app.autolaunch();
+ if enabled {
+ manager.enable().map_err(|e| e.to_string())?;
+ debug!("enabled autostart");
+ } else {
+ manager.disable().map_err(|e| e.to_string())?;
+ debug!("eisabled autostart");
+ }
+
+ // Store the state in DB
+ let mut db_handle = borrow_db_mut_checked();
+ db_handle.settings.autostart = enabled;
+ drop(db_handle);
+ save_db();
+
+ Ok(())
+}
+
+pub fn get_autostart_enabled_logic(app: AppHandle) -> Result
{
+ // First check DB state
+ let db_handle = borrow_db_checked();
+ let db_state = db_handle.settings.autostart;
+ drop(db_handle);
+
+ // Get actual system state
+ let manager = app.autolaunch();
+ let system_state = manager.is_enabled()?;
+
+ // If they don't match, sync to DB state
+ if db_state != system_state {
+ if db_state {
+ manager.enable()?;
+ } else {
+ manager.disable()?;
+ }
+ }
+
+ Ok(db_state)
+}
+
+// New function to sync state on startup
+pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
+ let db_handle = borrow_db_checked();
+ let should_be_enabled = db_handle.settings.autostart;
+ drop(db_handle);
+
+ let manager = app.autolaunch();
+ let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
+
+ if current_state != should_be_enabled {
+ if should_be_enabled {
+ manager.enable().map_err(|e| e.to_string())?;
+ debug!("synced autostart: enabled");
+ } else {
+ manager.disable().map_err(|e| e.to_string())?;
+ debug!("synced autostart: disabled");
+ }
+ }
+
+ Ok(())
+}
+#[tauri::command]
+pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
+ toggle_autostart_logic(app, enabled)
+}
+
+#[tauri::command]
+pub fn get_autostart_enabled(app: AppHandle) -> Result {
+ get_autostart_enabled_logic(app)
+}
diff --git a/src-tauri/src/cleanup.rs b/src-tauri/src/cleanup.rs
index 2fe2d2d..de0e4e6 100644
--- a/src-tauri/src/cleanup.rs
+++ b/src-tauri/src/cleanup.rs
@@ -1,18 +1,23 @@
-use std::sync::Mutex;
-
-use log::info;
+use log::{debug, error};
use tauri::AppHandle;
use crate::AppState;
#[tauri::command]
-pub fn quit(app: tauri::AppHandle) {
- cleanup_and_exit(&app);
+pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex>>) {
+ cleanup_and_exit(&app, &state);
}
-
-pub fn cleanup_and_exit(app: &AppHandle, ) {
- info!("exiting drop application...");
+pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex>>) {
+ debug!("cleaning up and exiting application");
+ let download_manager = state.lock().unwrap().download_manager.clone();
+ match download_manager.ensure_terminated() {
+ Ok(res) => match res {
+ Ok(_) => debug!("download manager terminated correctly"),
+ Err(_) => error!("download manager failed to terminate correctly"),
+ },
+ Err(e) => panic!("{:?}", e),
+ }
app.exit(0);
}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
new file mode 100644
index 0000000..b47a348
--- /dev/null
+++ b/src-tauri/src/commands.rs
@@ -0,0 +1,11 @@
+use crate::AppState;
+
+#[tauri::command]
+pub fn fetch_state(
+ state: tauri::State<'_, std::sync::Mutex>>,
+) -> Result {
+ let guard = state.lock().unwrap();
+ let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
+ drop(guard);
+ Ok(cloned_state)
+}
diff --git a/src-tauri/src/database/commands.rs b/src-tauri/src/database/commands.rs
new file mode 100644
index 0000000..18befb8
--- /dev/null
+++ b/src-tauri/src/database/commands.rs
@@ -0,0 +1,92 @@
+use std::{
+ fs::create_dir_all,
+ io::{Error, ErrorKind},
+ path::{Path, PathBuf},
+};
+
+use serde_json::Value;
+
+use crate::{
+ database::{db::borrow_db_mut_checked, settings::Settings},
+ download_manager::internal_error::InternalError,
+};
+
+use super::{
+ db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
+ debug::SystemData,
+};
+
+// Will, in future, return disk/remaining size
+// Just returns the directories that have been set up
+#[tauri::command]
+pub fn fetch_download_dir_stats() -> Vec {
+ let lock = borrow_db_checked();
+ lock.applications.install_dirs.clone()
+}
+
+#[tauri::command]
+pub fn delete_download_dir(index: usize) {
+ let mut lock = borrow_db_mut_checked();
+ lock.applications.install_dirs.remove(index);
+ drop(lock);
+ save_db();
+}
+
+#[tauri::command]
+pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
+ // Check the new directory is all good
+ let new_dir_path = Path::new(&new_dir);
+ if new_dir_path.exists() {
+ let dir_contents = new_dir_path.read_dir()?;
+ if dir_contents.count() != 0 {
+ return Err(Error::new(
+ ErrorKind::DirectoryNotEmpty,
+ "Selected directory cannot contain any existing files",
+ )
+ .into());
+ }
+ } else {
+ create_dir_all(new_dir_path)?;
+ }
+
+ // Add it to the dictionary
+ let mut lock = borrow_db_mut_checked();
+ if lock.applications.install_dirs.contains(&new_dir) {
+ return Err(Error::new(
+ ErrorKind::AlreadyExists,
+ "Selected directory already exists in database",
+ )
+ .into());
+ }
+ lock.applications.install_dirs.push(new_dir);
+ drop(lock);
+ save_db();
+
+ Ok(())
+}
+
+#[tauri::command]
+pub fn update_settings(new_settings: Value) {
+ let mut db_lock = borrow_db_mut_checked();
+ let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
+ for (key, value) in new_settings.as_object().unwrap() {
+ current_settings[key] = value.clone();
+ }
+ let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
+ db_lock.settings = new_settings;
+ println!("new Settings: {:?}", db_lock.settings);
+}
+#[tauri::command]
+pub fn fetch_settings() -> Settings {
+ borrow_db_checked().settings.clone()
+}
+#[tauri::command]
+pub fn fetch_system_data() -> SystemData {
+ let db_handle = borrow_db_checked();
+ SystemData::new(
+ db_handle.auth.as_ref().unwrap().client_id.clone(),
+ db_handle.base_url.clone(),
+ DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
+ std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
+ )
+}
diff --git a/src-tauri/src/database/db.rs b/src-tauri/src/database/db.rs
new file mode 100644
index 0000000..b92de3d
--- /dev/null
+++ b/src-tauri/src/database/db.rs
@@ -0,0 +1,256 @@
+use std::{
+ collections::HashMap,
+ fs::{self, create_dir_all},
+ hash::Hash,
+ path::{Path, PathBuf},
+ sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
+};
+
+use chrono::Utc;
+use directories::BaseDirs;
+use log::{debug, error, info};
+use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde_with::serde_as;
+use tauri::AppHandle;
+use url::Url;
+
+use crate::{
+ database::settings::Settings,
+ download_manager::downloadable_metadata::DownloadableMetadata,
+ games::{library::push_game_update, state::GameStatusManager},
+ process::process_manager::Platform,
+ DB,
+};
+
+#[derive(serde::Serialize, Clone, Deserialize)]
+pub struct DatabaseAuth {
+ pub private: String,
+ pub cert: String,
+ pub client_id: String,
+}
+
+// Strings are version names for a particular game
+#[derive(Serialize, Clone, Deserialize)]
+#[serde(tag = "type")]
+pub enum GameDownloadStatus {
+ Remote {},
+ SetupRequired {
+ version_name: String,
+ install_dir: String,
+ },
+ Installed {
+ version_name: String,
+ install_dir: String,
+ },
+}
+
+// Stuff that shouldn't be synced to disk
+#[derive(Clone, Serialize)]
+pub enum ApplicationTransientStatus {
+ Downloading { version_name: String },
+ Uninstalling {},
+ Updating { version_name: String },
+ Running {},
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct GameVersion {
+ pub game_id: String,
+ pub version_name: String,
+
+ pub platform: Platform,
+
+ pub launch_command: String,
+ pub launch_args: Vec,
+
+ pub setup_command: String,
+ pub setup_args: Vec,
+
+ pub only_setup: bool,
+
+ pub version_index: usize,
+ pub delta: bool,
+
+ pub umu_id_override: Option,
+}
+
+#[serde_as]
+#[derive(Serialize, Clone, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct DatabaseApplications {
+ pub install_dirs: Vec,
+ // Guaranteed to exist if the game also exists in the app state map
+ pub game_statuses: HashMap,
+ pub game_versions: HashMap>,
+ pub installed_game_version: HashMap,
+
+ #[serde(skip)]
+ pub transient_statuses: HashMap,
+}
+
+#[derive(Serialize, Deserialize, Clone, Default)]
+pub struct Database {
+ #[serde(default)]
+ pub settings: Settings,
+ pub auth: Option,
+ pub base_url: String,
+ pub applications: DatabaseApplications,
+ pub prev_database: Option,
+}
+impl Database {
+ fn new>(games_base_dir: T, prev_database: Option) -> Self {
+ Self {
+ applications: DatabaseApplications {
+ install_dirs: vec![games_base_dir.into()],
+ game_statuses: HashMap::new(),
+ game_versions: HashMap::new(),
+ installed_game_version: HashMap::new(),
+ transient_statuses: HashMap::new(),
+ },
+ prev_database,
+ base_url: "".to_owned(),
+ auth: None,
+ settings: Settings {
+ autostart: false,
+ max_download_threads: 4,
+ },
+ }
+ }
+}
+pub static DATA_ROOT_DIR: LazyLock> =
+ LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
+
+// Custom JSON serializer to support everything we need
+#[derive(Debug, Default, Clone)]
+pub struct DropDatabaseSerializer;
+
+impl DeSerializer for DropDatabaseSerializer {
+ fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> {
+ serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string()))
+ }
+
+ fn deserialize(&self, s: R) -> rustbreak::error::DeSerResult {
+ serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string()))
+ }
+}
+
+pub type DatabaseInterface =
+ rustbreak::Database;
+
+pub trait DatabaseImpls {
+ fn set_up_database() -> DatabaseInterface;
+ fn database_is_set_up(&self) -> bool;
+ fn fetch_base_url(&self) -> Url;
+}
+impl DatabaseImpls for DatabaseInterface {
+ fn set_up_database() -> DatabaseInterface {
+ let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
+ let db_path = data_root_dir.join("drop.db");
+ let games_base_dir = data_root_dir.join("games");
+ let logs_root_dir = data_root_dir.join("logs");
+
+ debug!("creating data directory at {:?}", data_root_dir);
+ create_dir_all(data_root_dir.clone()).unwrap();
+ create_dir_all(games_base_dir.clone()).unwrap();
+ create_dir_all(logs_root_dir.clone()).unwrap();
+
+ let exists = fs::exists(db_path.clone()).unwrap();
+
+ match exists {
+ true => match PathDatabase::load_from_path(db_path.clone()) {
+ Ok(db) => db,
+ Err(e) => handle_invalid_database(e, db_path, games_base_dir),
+ },
+ false => {
+ let default = Database::new(games_base_dir, None);
+ debug!(
+ "Creating database at path {}",
+ db_path.as_os_str().to_str().unwrap()
+ );
+ PathDatabase::create_at_path(db_path, default)
+ .expect("Database could not be created")
+ }
+ }
+ }
+
+ fn database_is_set_up(&self) -> bool {
+ !self.borrow_data().unwrap().base_url.is_empty()
+ }
+
+ fn fetch_base_url(&self) -> Url {
+ let handle = self.borrow_data().unwrap();
+ Url::parse(&handle.base_url).unwrap()
+ }
+}
+
+pub fn set_game_status, &DownloadableMetadata)>(
+ app_handle: &AppHandle,
+ meta: DownloadableMetadata,
+ setter: F,
+) {
+ let mut db_handle = borrow_db_mut_checked();
+ setter(&mut db_handle, &meta);
+ drop(db_handle);
+ save_db();
+
+ let status = GameStatusManager::fetch_state(&meta.id);
+
+ push_game_update(app_handle, &meta.id, status);
+}
+// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
+fn handle_invalid_database(
+ _e: RustbreakError,
+ db_path: PathBuf,
+ games_base_dir: PathBuf,
+) -> rustbreak::Database {
+ let new_path = {
+ let time = Utc::now().timestamp();
+ let mut base = db_path.clone();
+ base.set_file_name(format!("drop.db.backup-{}", time));
+ base
+ };
+ info!(
+ "old database stored at: {}",
+ new_path.to_string_lossy().to_string()
+ );
+ fs::rename(&db_path, &new_path).unwrap();
+
+ let db = Database::new(
+ games_base_dir.into_os_string().into_string().unwrap(),
+ Some(new_path),
+ );
+
+ PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
+}
+
+pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
+ match DB.borrow_data() {
+ Ok(data) => data,
+ Err(e) => {
+ error!("database borrow failed with error {}", e);
+ panic!("database borrow failed with error {}", e);
+ }
+ }
+}
+
+pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
+ match DB.borrow_data_mut() {
+ Ok(data) => data,
+ Err(e) => {
+ error!("database borrow mut failed with error {}", e);
+ panic!("database borrow mut failed with error {}", e);
+ }
+ }
+}
+
+pub fn save_db() {
+ match DB.save() {
+ Ok(_) => {}
+ Err(e) => {
+ error!("database failed to save with error {}", e);
+ panic!("database failed to save with error {}", e)
+ }
+ }
+}
diff --git a/src-tauri/src/database/debug.rs b/src-tauri/src/database/debug.rs
new file mode 100644
index 0000000..45d2034
--- /dev/null
+++ b/src-tauri/src/database/debug.rs
@@ -0,0 +1,21 @@
+use serde::Serialize;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SystemData {
+ client_id: String,
+ base_url: String,
+ data_dir: String,
+ log_level: String,
+}
+
+impl SystemData {
+ pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self {
+ Self {
+ client_id,
+ base_url,
+ data_dir,
+ log_level,
+ }
+ }
+}
diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs
new file mode 100644
index 0000000..2a4255b
--- /dev/null
+++ b/src-tauri/src/database/mod.rs
@@ -0,0 +1,4 @@
+pub mod commands;
+pub mod db;
+pub mod debug;
+pub mod settings;
diff --git a/src-tauri/src/database/settings.rs b/src-tauri/src/database/settings.rs
new file mode 100644
index 0000000..b8c1742
--- /dev/null
+++ b/src-tauri/src/database/settings.rs
@@ -0,0 +1,24 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct Settings {
+ pub autostart: bool,
+ pub max_download_threads: usize,
+ // ... other settings ...
+}
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ autostart: false,
+ max_download_threads: 4,
+ }
+ }
+}
+// Ideally use pointers instead of a macro to assign the settings
+// fn deserialize_into(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
+// where T: for<'a> Deserialize<'a>
+// {
+// *t = serde_json::from_value(v)?;
+// Ok(())
+// }
diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs
deleted file mode 100644
index 00e93ab..0000000
--- a/src-tauri/src/db.rs
+++ /dev/null
@@ -1,204 +0,0 @@
-use std::{
- collections::HashMap,
- fs::{self, create_dir_all},
- path::{Path, PathBuf},
- sync::{LazyLock, Mutex},
-};
-
-use directories::BaseDirs;
-use log::debug;
-use rustbreak::{DeSerError, DeSerializer, PathDatabase};
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
-use url::Url;
-
-use crate::{process::process_manager::Platform, DB};
-
-#[derive(serde::Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct DatabaseAuth {
- pub private: String,
- pub cert: String,
- pub client_id: String,
-}
-
-// Strings are version names for a particular game
-#[derive(Serialize, Clone, Deserialize)]
-#[serde(tag = "type")]
-pub enum GameStatus {
- Remote {},
- SetupRequired {
- version_name: String,
- install_dir: String,
- },
- Installed {
- version_name: String,
- install_dir: String,
- },
-}
-
-// Stuff that shouldn't be synced to disk
-#[derive(Clone, Serialize)]
-pub enum GameTransientStatus {
- Downloading { version_name: String },
- Uninstalling {},
- Updating { version_name: String },
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct GameVersion {
- pub version_index: usize,
- pub version_name: String,
- pub launch_command: String,
- pub setup_command: String,
- pub platform: Platform,
-}
-
-#[derive(Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct DatabaseGames {
- pub install_dirs: Vec,
- // Guaranteed to exist if the game also exists in the app state map
- pub statuses: HashMap,
- pub versions: HashMap>,
-
- #[serde(skip)]
- pub transient_statuses: HashMap,
-}
-
-#[derive(Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Database {
- pub auth: Option,
- pub base_url: String,
- pub games: DatabaseGames,
-}
-pub static DATA_ROOT_DIR: LazyLock> =
- LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
-
-// Custom JSON serializer to support everything we need
-#[derive(Debug, Default, Clone)]
-pub struct DropDatabaseSerializer;
-
-impl DeSerializer for DropDatabaseSerializer {
- fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> {
- serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string()))
- }
-
- fn deserialize(&self, s: R) -> rustbreak::error::DeSerResult {
- serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string()))
- }
-}
-
-pub type DatabaseInterface =
- rustbreak::Database;
-
-pub trait DatabaseImpls {
- fn set_up_database() -> DatabaseInterface;
- fn database_is_set_up(&self) -> bool;
- fn fetch_base_url(&self) -> Url;
-}
-impl DatabaseImpls for DatabaseInterface {
- fn set_up_database() -> DatabaseInterface {
- let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
- let db_path = data_root_dir.join("drop.db");
- let games_base_dir = data_root_dir.join("games");
- let logs_root_dir = data_root_dir.join("logs");
-
- debug!("Creating data directory at {:?}", data_root_dir);
- create_dir_all(data_root_dir.clone()).unwrap();
- debug!("Creating games directory");
- create_dir_all(games_base_dir.clone()).unwrap();
- debug!("Creating logs directory");
- create_dir_all(logs_root_dir.clone()).unwrap();
-
- #[allow(clippy::let_and_return)]
- let exists = fs::exists(db_path.clone()).unwrap();
-
- match exists {
- true => PathDatabase::load_from_path(db_path).expect("Database loading failed"),
- false => {
- let default = Database {
- auth: None,
- base_url: "".to_string(),
- games: DatabaseGames {
- install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
- statuses: HashMap::new(),
- transient_statuses: HashMap::new(),
- versions: HashMap::new(),
- },
- };
- debug!(
- "Creating database at path {}",
- db_path.as_os_str().to_str().unwrap()
- );
- PathDatabase::create_at_path(db_path, default)
- .expect("Database could not be created")
- }
- }
- }
-
- fn database_is_set_up(&self) -> bool {
- !self.borrow_data().unwrap().base_url.is_empty()
- }
-
- fn fetch_base_url(&self) -> Url {
- let handle = self.borrow_data().unwrap();
- Url::parse(&handle.base_url).unwrap()
- }
-}
-
-#[tauri::command]
-pub fn add_download_dir(new_dir: String) -> Result<(), String> {
- // Check the new directory is all good
- let new_dir_path = Path::new(&new_dir);
- if new_dir_path.exists() {
- let metadata = new_dir_path
- .metadata()
- .map_err(|e| format!("Unable to access file or directory: {}", e))?;
- if !metadata.is_dir() {
- return Err("Invalid path: not a directory".to_string());
- }
- let dir_contents = new_dir_path
- .read_dir()
- .map_err(|e| format!("Unable to check directory contents: {}", e))?;
- if dir_contents.count() != 0 {
- return Err("Directory is not empty".to_string());
- }
- } else {
- create_dir_all(new_dir_path)
- .map_err(|e| format!("Unable to create directories to path: {}", e))?;
- }
-
- // Add it to the dictionary
- let mut lock = DB.borrow_data_mut().unwrap();
- if lock.games.install_dirs.contains(&new_dir) {
- return Err("Download directory already used".to_string());
- }
- lock.games.install_dirs.push(new_dir);
- drop(lock);
- DB.save().unwrap();
-
- Ok(())
-}
-
-#[tauri::command]
-pub fn delete_download_dir(index: usize) -> Result<(), String> {
- let mut lock = DB.borrow_data_mut().unwrap();
- lock.games.install_dirs.remove(index);
- drop(lock);
- DB.save().unwrap();
-
- Ok(())
-}
-
-// Will, in future, return disk/remaining size
-// Just returns the directories that have been set up
-#[tauri::command]
-pub fn fetch_download_dir_stats() -> Result, String> {
- let lock = DB.borrow_data().unwrap();
- let directories = lock.games.install_dirs.clone();
- drop(lock);
-
- Ok(directories)
-}
diff --git a/src-tauri/src/download_manager/commands.rs b/src-tauri/src/download_manager/commands.rs
new file mode 100644
index 0000000..0a65c0d
--- /dev/null
+++ b/src-tauri/src/download_manager/commands.rs
@@ -0,0 +1,31 @@
+use std::sync::Mutex;
+
+use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
+
+#[tauri::command]
+pub fn pause_downloads(state: tauri::State<'_, Mutex>) {
+ state.lock().unwrap().download_manager.pause_downloads()
+}
+
+#[tauri::command]
+pub fn resume_downloads(state: tauri::State<'_, Mutex>) {
+ state.lock().unwrap().download_manager.resume_downloads()
+}
+
+#[tauri::command]
+pub fn move_download_in_queue(
+ state: tauri::State<'_, Mutex>,
+ old_index: usize,
+ new_index: usize,
+) {
+ state
+ .lock()
+ .unwrap()
+ .download_manager
+ .rearrange(old_index, new_index)
+}
+
+#[tauri::command]
+pub fn cancel_game(state: tauri::State<'_, Mutex>, meta: DownloadableMetadata) {
+ state.lock().unwrap().download_manager.cancel(meta)
+}
diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/download_manager/download_manager.rs
similarity index 54%
rename from src-tauri/src/downloads/download_manager.rs
rename to src-tauri/src/download_manager/download_manager.rs
index 8676087..a6a58ea 100644
--- a/src-tauri/src/downloads/download_manager.rs
+++ b/src-tauri/src/download_manager/download_manager.rs
@@ -4,18 +4,19 @@ use std::{
fmt::Debug,
sync::{
mpsc::{SendError, Sender},
- Arc, Mutex, MutexGuard,
+ Mutex, MutexGuard,
},
thread::JoinHandle,
};
-use log::info;
+use log::{debug, info};
use serde::Serialize;
+use crate::error::application_download_error::ApplicationDownloadError;
+
use super::{
- download_agent::{GameDownloadAgent, GameDownloadError},
- download_manager_builder::CurrentProgressObject,
- progress_object::ProgressObject,
+ download_manager_builder::{CurrentProgressObject, DownloadAgent},
+ downloadable_metadata::DownloadableMetadata,
queue::Queue,
};
@@ -24,35 +25,49 @@ pub enum DownloadManagerSignal {
Go,
/// Pauses the DownloadManager
Stop,
- /// Called when a GameDownloadAgent has fully completed a download.
- Completed(String),
- /// Generates and appends a GameDownloadAgent
+ /// Called when a DownloadAgent has fully completed a download.
+ Completed(DownloadableMetadata),
+ /// Generates and appends a DownloadAgent
/// to the registry and queue
- Queue(String, String, usize),
+ Queue(DownloadAgent),
/// Tells the Manager to stop the current
/// download, sync everything to disk, and
/// then exit
Finish,
- /// Stops (but doesn't remove) current download
- Cancel,
- /// Removes a given game
- Remove(String),
+ /// Stops, removes, and tells a download to cleanup
+ Cancel(DownloadableMetadata),
+ /// Removes a given application
+ Remove(DownloadableMetadata),
/// Any error which occurs in the agent
- Error(GameDownloadError),
+ Error(ApplicationDownloadError),
/// Pushes UI update
- Update,
+ UpdateUIQueue,
+ UpdateUIStats(usize, usize), //kb/s and seconds
+ /// Uninstall download
+ /// Takes download ID
+ Uninstall(DownloadableMetadata),
}
+#[derive(Debug, Clone)]
pub enum DownloadManagerStatus {
Downloading,
Paused,
Empty,
- Error(GameDownloadError),
+ Error(ApplicationDownloadError),
Finished,
}
-#[derive(Serialize, Clone)]
-pub enum GameDownloadStatus {
+impl Serialize for DownloadManagerStatus {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&format!["{:?}", self])
+ }
+}
+
+#[derive(Serialize, Clone, Debug)]
+pub enum DownloadStatus {
Queued,
Downloading,
Error,
@@ -69,32 +84,11 @@ pub enum GameDownloadStatus {
/// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager {
- terminator: JoinHandle>,
+ terminator: Mutex>>>,
download_queue: Queue,
progress: CurrentProgressObject,
command_sender: Sender,
}
-pub struct GameDownloadAgentQueueStandin {
- pub id: String,
- pub status: Mutex,
- pub progress: Arc,
-}
-impl From> for GameDownloadAgentQueueStandin {
- fn from(value: Arc) -> Self {
- Self {
- id: value.id.clone(),
- status: Mutex::from(GameDownloadStatus::Queued),
- progress: value.progress.clone(),
- }
- }
-}
-impl Debug for GameDownloadAgentQueueStandin {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("GameDownloadAgentQueueStandin")
- .field("id", &self.id)
- .finish()
- }
-}
#[allow(dead_code)]
impl DownloadManager {
@@ -105,49 +99,44 @@ impl DownloadManager {
command_sender: Sender,
) -> Self {
Self {
- terminator,
+ terminator: Mutex::new(Some(terminator)),
download_queue,
progress,
command_sender,
}
}
- pub fn queue_game(
+ pub fn queue_download(
&self,
- id: String,
- version: String,
- target_download_dir: usize,
+ download: DownloadAgent,
) -> Result<(), SendError> {
- info!("Adding game id {}", id);
- self.command_sender.send(DownloadManagerSignal::Queue(
- id,
- version,
- target_download_dir,
- ))?;
+ info!("creating download with meta {:?}", download.metadata());
+ self.command_sender
+ .send(DownloadManagerSignal::Queue(download))?;
self.command_sender.send(DownloadManagerSignal::Go)
}
- pub fn edit(&self) -> MutexGuard<'_, VecDeque>> {
+ pub fn edit(&self) -> MutexGuard<'_, VecDeque> {
self.download_queue.edit()
}
- pub fn read_queue(&self) -> VecDeque> {
+ pub fn read_queue(&self) -> VecDeque {
self.download_queue.read()
}
- pub fn get_current_game_download_progress(&self) -> Option {
+ pub fn get_current_download_progress(&self) -> Option {
let progress_object = (*self.progress.lock().unwrap()).clone()?;
Some(progress_object.get_progress())
}
- pub fn rearrange_string(&self, id: String, new_index: usize) {
+ pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
let mut queue = self.edit();
- let current_index = get_index_from_id(&mut queue, id).unwrap();
+ let current_index = get_index_from_id(&mut queue, meta).unwrap();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
self.command_sender
- .send(DownloadManagerSignal::Update)
+ .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
- pub fn cancel(&self, game_id: String) {
+ pub fn cancel(&self, meta: DownloadableMetadata) {
self.command_sender
- .send(DownloadManagerSignal::Remove(game_id))
+ .send(DownloadManagerSignal::Cancel(meta))
.unwrap();
}
pub fn rearrange(&self, current_index: usize, new_index: usize) {
@@ -158,23 +147,25 @@ impl DownloadManager {
let needs_pause = current_index == 0 || new_index == 0;
if needs_pause {
self.command_sender
- .send(DownloadManagerSignal::Cancel)
+ .send(DownloadManagerSignal::Stop)
.unwrap();
}
- info!("moving {} to {}", current_index, new_index);
+ debug!(
+ "moving download at index {} to index {}",
+ current_index, new_index
+ );
let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
- info!("new queue: {:?}", queue);
drop(queue);
if needs_pause {
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
self.command_sender
- .send(DownloadManagerSignal::Update)
+ .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
pub fn pause_downloads(&self) {
@@ -185,21 +176,30 @@ impl DownloadManager {
pub fn resume_downloads(&self) {
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
- pub fn ensure_terminated(self) -> Result, Box> {
+ pub fn ensure_terminated(&self) -> Result, Box> {
self.command_sender
.send(DownloadManagerSignal::Finish)
.unwrap();
- self.terminator.join()
+ let terminator = self.terminator.lock().unwrap().take();
+ terminator.unwrap().join()
+ }
+ pub fn uninstall_application(&self, meta: DownloadableMetadata) {
+ self.command_sender
+ .send(DownloadManagerSignal::Uninstall(meta))
+ .unwrap();
+ }
+ pub fn get_sender(&self) -> Sender {
+ self.command_sender.clone()
}
}
/// Takes in the locked value from .edit() and attempts to
-/// get the index of whatever game_id is passed in
+/// get the index of whatever id is passed in
fn get_index_from_id(
- queue: &mut MutexGuard<'_, VecDeque>>,
- id: String,
+ queue: &mut MutexGuard<'_, VecDeque>,
+ meta: &DownloadableMetadata,
) -> Option {
queue
.iter()
- .position(|download_agent| download_agent.id == id)
+ .position(|download_agent| download_agent == meta)
}
diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs
new file mode 100644
index 0000000..52c014d
--- /dev/null
+++ b/src-tauri/src/download_manager/download_manager_builder.rs
@@ -0,0 +1,362 @@
+use std::{
+ collections::HashMap,
+ sync::{
+ mpsc::{channel, Receiver, Sender},
+ Arc, Mutex,
+ },
+ thread::{spawn, JoinHandle},
+};
+
+use log::{debug, error, info, warn};
+use tauri::{AppHandle, Emitter};
+
+use crate::{
+ error::application_download_error::ApplicationDownloadError,
+ games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
+};
+
+use super::{
+ download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
+ download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
+ downloadable::Downloadable,
+ downloadable_metadata::DownloadableMetadata,
+ progress_object::ProgressObject,
+ queue::Queue,
+};
+
+pub type DownloadAgent = Arc>;
+pub type CurrentProgressObject = Arc>>>;
+
+/*
+
+Welcome to the download manager, the most overengineered, glorious piece of bullshit.
+
+The download manager takes a queue of ids and their associated
+DownloadAgents, and then, one-by-one, executes them. It provides an interface
+to interact with the currently downloading agent, and manage the queue.
+
+When the DownloadManager is initialised, it is designed to provide a reference
+which can be used to provide some instructions (the DownloadManagerInterface),
+but other than that, it runs without any sort of interruptions.
+
+It does this by opening up two data structures. Primarily is the command_receiver,
+and mpsc (multi-channel-single-producer) which allows commands to be sent from
+the Interface, and queued up for the Manager to process.
+
+These have been mapped in the DownloadManagerSignal docs.
+
+The other way to interact with the DownloadManager is via the donwload_queue,
+which is just a collection of ids which may be rearranged to suit
+whichever download queue order is required.
+
++----------------------------------------------------------------------------+
+| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
+| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
+| WHICH HAS NOT BEEN ACCOUNTED FOR |
++----------------------------------------------------------------------------+
+
+This download queue does not actually own any of the DownloadAgents. It is
+simply an id-based reference system. The actual Agents are stored in the
+download_agent_registry HashMap, as ordering is no issue here. This is why
+appending or removing from the download_queue must be done via signals.
+
+Behold, my madness - quexeky
+
+*/
+
+pub struct DownloadManagerBuilder {
+ download_agent_registry: HashMap,
+ download_queue: Queue,
+ command_receiver: Receiver,
+ sender: Sender,
+ progress: CurrentProgressObject,
+ status: Arc>,
+ app_handle: AppHandle,
+
+ current_download_agent: Option, // Should be the only download agent in the map with the "Go" flag
+ current_download_thread: Mutex>>,
+ active_control_flag: Option,
+}
+impl DownloadManagerBuilder {
+ pub fn build(app_handle: AppHandle) -> DownloadManager {
+ let queue = Queue::new();
+ let (command_sender, command_receiver) = channel();
+ let active_progress = Arc::new(Mutex::new(None));
+ let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
+
+ let manager = Self {
+ download_agent_registry: HashMap::new(),
+ download_queue: queue.clone(),
+ command_receiver,
+ status: status.clone(),
+ sender: command_sender.clone(),
+ progress: active_progress.clone(),
+ app_handle,
+
+ current_download_agent: None,
+ current_download_thread: Mutex::new(None),
+ active_control_flag: None,
+ };
+
+ let terminator = spawn(|| manager.manage_queue());
+
+ DownloadManager::new(terminator, queue, active_progress, command_sender)
+ }
+
+ fn set_status(&self, status: DownloadManagerStatus) {
+ *self.status.lock().unwrap() = status;
+ }
+
+ fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
+ self.download_queue.pop_front();
+ let download_agent = self.download_agent_registry.remove(meta).unwrap();
+ self.cleanup_current_download();
+ download_agent
+ }
+
+ // CAREFUL WITH THIS FUNCTION
+ // Make sure the download thread is terminated
+ fn cleanup_current_download(&mut self) {
+ self.active_control_flag = None;
+ *self.progress.lock().unwrap() = None;
+ self.current_download_agent = None;
+
+ let mut download_thread_lock = self.current_download_thread.lock().unwrap();
+ *download_thread_lock = None;
+ drop(download_thread_lock);
+ }
+
+ fn stop_and_wait_current_download(&self) {
+ self.set_status(DownloadManagerStatus::Paused);
+ if let Some(current_flag) = &self.active_control_flag {
+ current_flag.set(DownloadThreadControlFlag::Stop);
+ }
+
+ let mut download_thread_lock = self.current_download_thread.lock().unwrap();
+ if let Some(current_download_thread) = download_thread_lock.take() {
+ current_download_thread.join().unwrap();
+ }
+ }
+
+ fn manage_queue(mut self) -> Result<(), ()> {
+ loop {
+ let signal = match self.command_receiver.recv() {
+ Ok(signal) => signal,
+ Err(_) => return Err(()),
+ };
+
+ match signal {
+ DownloadManagerSignal::Go => {
+ self.manage_go_signal();
+ }
+ DownloadManagerSignal::Stop => {
+ self.manage_stop_signal();
+ }
+ DownloadManagerSignal::Completed(meta) => {
+ self.manage_completed_signal(meta);
+ }
+ DownloadManagerSignal::Queue(download_agent) => {
+ self.manage_queue_signal(download_agent);
+ }
+ DownloadManagerSignal::Error(e) => {
+ self.manage_error_signal(e);
+ }
+ DownloadManagerSignal::UpdateUIQueue => {
+ self.push_ui_queue_update();
+ }
+ DownloadManagerSignal::UpdateUIStats(kbs, time) => {
+ self.push_ui_stats_update(kbs, time);
+ }
+ DownloadManagerSignal::Finish => {
+ self.stop_and_wait_current_download();
+ return Ok(());
+ }
+ DownloadManagerSignal::Cancel(meta) => {
+ self.manage_cancel_signal(&meta);
+ }
+ _ => {}
+ };
+ }
+ }
+ fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
+ debug!("got signal Queue");
+ let meta = download_agent.metadata();
+
+ debug!("queue metadata: {:?}", meta);
+
+ if self.download_queue.exists(meta.clone()) {
+ warn!("download with same ID already exists");
+ return;
+ }
+
+ download_agent.on_initialised(&self.app_handle);
+ self.download_queue.append(meta.clone());
+ self.download_agent_registry.insert(meta, download_agent);
+
+ self.sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+ }
+
+ fn manage_go_signal(&mut self) {
+ debug!("got signal Go");
+ if self.download_agent_registry.is_empty() {
+ debug!(
+ "Download agent registry: {:?}",
+ self.download_agent_registry.len()
+ );
+ return;
+ }
+
+ if self.current_download_agent.is_some() {
+ debug!(
+ "Current download agent: {:?}",
+ self.current_download_agent.as_ref().unwrap().metadata()
+ );
+ return;
+ }
+
+ debug!("current download queue: {:?}", self.download_queue.read());
+
+ // Should always be Some if the above two statements keep going
+ let agent_data = self.download_queue.read().front().unwrap().clone();
+
+ info!("starting download for {:?}", agent_data);
+
+ let download_agent = self
+ .download_agent_registry
+ .get(&agent_data)
+ .unwrap()
+ .clone();
+
+ self.active_control_flag = Some(download_agent.control_flag());
+ self.current_download_agent = Some(download_agent.clone());
+
+ let sender = self.sender.clone();
+
+ let mut download_thread_lock = self.current_download_thread.lock().unwrap();
+ let app_handle = self.app_handle.clone();
+
+ *download_thread_lock = Some(spawn(move || {
+ match download_agent.download(&app_handle) {
+ // Ok(true) is for completed and exited properly
+ Ok(true) => {
+ debug!("download {:?} has completed", download_agent.metadata());
+ download_agent.on_complete(&app_handle);
+ sender
+ .send(DownloadManagerSignal::Completed(download_agent.metadata()))
+ .unwrap();
+ }
+ // Ok(false) is for incomplete but exited properly
+ Ok(false) => {
+ download_agent.on_incomplete(&app_handle);
+ }
+ Err(e) => {
+ error!("download {:?} has error {}", download_agent.metadata(), &e);
+ download_agent.on_error(&app_handle, e.clone());
+ sender.send(DownloadManagerSignal::Error(e)).unwrap();
+ }
+ }
+ sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
+ }));
+
+ self.set_status(DownloadManagerStatus::Downloading);
+ let active_control_flag = self.active_control_flag.clone().unwrap();
+ active_control_flag.set(DownloadThreadControlFlag::Go);
+ }
+ fn manage_stop_signal(&mut self) {
+ debug!("got signal Stop");
+
+ if let Some(active_control_flag) = self.active_control_flag.clone() {
+ self.set_status(DownloadManagerStatus::Paused);
+ active_control_flag.set(DownloadThreadControlFlag::Stop);
+ }
+ }
+ fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
+ debug!("got signal Completed");
+ if let Some(interface) = &self.current_download_agent {
+ if interface.metadata() == meta {
+ self.remove_and_cleanup_front_download(&meta);
+ }
+ }
+ self.push_ui_queue_update();
+ self.sender.send(DownloadManagerSignal::Go).unwrap();
+ }
+ fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
+ debug!("got signal Error");
+ if let Some(current_agent) = self.current_download_agent.clone() {
+ current_agent.on_error(&self.app_handle, error.clone());
+
+ self.stop_and_wait_current_download();
+ self.remove_and_cleanup_front_download(¤t_agent.metadata());
+ }
+ self.set_status(DownloadManagerStatus::Error(error));
+ }
+ fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
+ debug!("got signal Cancel");
+
+ if let Some(current_download) = &self.current_download_agent {
+ if ¤t_download.metadata() == meta {
+ self.set_status(DownloadManagerStatus::Paused);
+ current_download.on_cancelled(&self.app_handle);
+ self.stop_and_wait_current_download();
+
+ self.download_queue.pop_front();
+
+ self.cleanup_current_download();
+ debug!("current download queue: {:?}", self.download_queue.read());
+ }
+ // TODO: Collapse these two into a single if statement somehow
+ else if let Some(download_agent) = self.download_agent_registry.get(meta) {
+ let index = self.download_queue.get_by_meta(meta);
+ if let Some(index) = index {
+ download_agent.on_cancelled(&self.app_handle);
+ let _ = self.download_queue.edit().remove(index).unwrap();
+ let removed = self.download_agent_registry.remove(meta);
+ debug!(
+ "removed {:?} from queue {:?}",
+ removed.map(|x| x.metadata()),
+ self.download_queue.read()
+ );
+ }
+ }
+ } else if let Some(download_agent) = self.download_agent_registry.get(meta) {
+ let index = self.download_queue.get_by_meta(meta);
+ if let Some(index) = index {
+ download_agent.on_cancelled(&self.app_handle);
+ let _ = self.download_queue.edit().remove(index).unwrap();
+ let removed = self.download_agent_registry.remove(meta);
+ debug!(
+ "removed {:?} from queue {:?}",
+ removed.map(|x| x.metadata()),
+ self.download_queue.read()
+ );
+ }
+ }
+ self.push_ui_queue_update();
+ }
+ fn push_ui_stats_update(&self, kbs: usize, time: usize) {
+ let event_data = StatsUpdateEvent { speed: kbs, time };
+
+ self.app_handle.emit("update_stats", event_data).unwrap();
+ }
+ fn push_ui_queue_update(&self) {
+ let queue = &self.download_queue.read();
+ let queue_objs = queue
+ .iter()
+ .map(|key| {
+ let val = self.download_agent_registry.get(key).unwrap();
+ QueueUpdateEventQueueData {
+ meta: DownloadableMetadata::clone(key),
+ status: val.status(),
+ progress: val.progress().get_progress(),
+ current: val.progress().sum(),
+ max: val.progress().get_max(),
+ }
+ })
+ .collect();
+
+ let event_data = QueueUpdateEvent { queue: queue_objs };
+ self.app_handle.emit("update_queue", event_data).unwrap();
+ }
+}
diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/download_manager/download_thread_control_flag.rs
similarity index 100%
rename from src-tauri/src/downloads/download_thread_control_flag.rs
rename to src-tauri/src/download_manager/download_thread_control_flag.rs
diff --git a/src-tauri/src/download_manager/downloadable.rs b/src-tauri/src/download_manager/downloadable.rs
new file mode 100644
index 0000000..181b329
--- /dev/null
+++ b/src-tauri/src/download_manager/downloadable.rs
@@ -0,0 +1,23 @@
+use std::sync::Arc;
+
+use tauri::AppHandle;
+
+use crate::error::application_download_error::ApplicationDownloadError;
+
+use super::{
+ download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
+ downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
+};
+
+pub trait Downloadable: Send + Sync {
+ fn download(&self, app_handle: &AppHandle) -> Result;
+ fn progress(&self) -> Arc;
+ fn control_flag(&self) -> DownloadThreadControl;
+ fn status(&self) -> DownloadStatus;
+ fn metadata(&self) -> DownloadableMetadata;
+ fn on_initialised(&self, app_handle: &AppHandle);
+ fn on_error(&self, app_handle: &AppHandle, error: ApplicationDownloadError);
+ fn on_complete(&self, app_handle: &AppHandle);
+ fn on_incomplete(&self, app_handle: &AppHandle);
+ fn on_cancelled(&self, app_handle: &AppHandle);
+}
diff --git a/src-tauri/src/download_manager/downloadable_metadata.rs b/src-tauri/src/download_manager/downloadable_metadata.rs
new file mode 100644
index 0000000..790b669
--- /dev/null
+++ b/src-tauri/src/download_manager/downloadable_metadata.rs
@@ -0,0 +1,26 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
+pub enum DownloadType {
+ Game,
+ Tool,
+ DLC,
+ Mod,
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct DownloadableMetadata {
+ pub id: String,
+ pub version: Option,
+ pub download_type: DownloadType,
+}
+impl DownloadableMetadata {
+ pub fn new(id: String, version: Option, download_type: DownloadType) -> Self {
+ Self {
+ id,
+ version,
+ download_type,
+ }
+ }
+}
diff --git a/src-tauri/src/download_manager/internal_error.rs b/src-tauri/src/download_manager/internal_error.rs
new file mode 100644
index 0000000..4864599
--- /dev/null
+++ b/src-tauri/src/download_manager/internal_error.rs
@@ -0,0 +1,27 @@
+use std::{fmt::Display, io, sync::mpsc::SendError};
+
+use serde_with::SerializeDisplay;
+
+#[derive(SerializeDisplay)]
+pub enum InternalError {
+ IOError(io::Error),
+ SignalError(SendError),
+}
+impl Display for InternalError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ InternalError::IOError(error) => write!(f, "{}", error),
+ InternalError::SignalError(send_error) => write!(f, "{}", send_error),
+ }
+ }
+}
+impl From> for InternalError {
+ fn from(value: SendError) -> Self {
+ InternalError::SignalError(value)
+ }
+}
+impl From for InternalError {
+ fn from(value: io::Error) -> Self {
+ InternalError::IOError(value)
+ }
+}
diff --git a/src-tauri/src/download_manager/mod.rs b/src-tauri/src/download_manager/mod.rs
new file mode 100644
index 0000000..0bac198
--- /dev/null
+++ b/src-tauri/src/download_manager/mod.rs
@@ -0,0 +1,10 @@
+pub mod commands;
+pub mod download_manager;
+pub mod download_manager_builder;
+pub mod download_thread_control_flag;
+pub mod downloadable;
+pub mod downloadable_metadata;
+pub mod internal_error;
+pub mod progress_object;
+pub mod queue;
+pub mod rolling_progress_updates;
diff --git a/src-tauri/src/download_manager/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs
new file mode 100644
index 0000000..fc0907b
--- /dev/null
+++ b/src-tauri/src/download_manager/progress_object.rs
@@ -0,0 +1,155 @@
+use std::{
+ sync::{
+ atomic::{AtomicUsize, Ordering},
+ mpsc::Sender,
+ Arc, Mutex,
+ },
+ time::{Duration, Instant},
+};
+
+use atomic_instant_full::AtomicInstant;
+use throttle_my_fn::throttle;
+
+use super::{
+ download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
+};
+
+#[derive(Clone)]
+pub struct ProgressObject {
+ max: Arc>,
+ progress_instances: Arc>>>,
+ start: Arc>,
+ sender: Sender,
+ //last_update: Arc>,
+ last_update_time: Arc,
+ bytes_last_update: Arc,
+ rolling: RollingProgressWindow<250>,
+}
+
+pub struct ProgressHandle {
+ progress: Arc,
+ progress_object: Arc,
+}
+
+impl ProgressHandle {
+ pub fn new(progress: Arc, progress_object: Arc) -> Self {
+ Self {
+ progress,
+ progress_object,
+ }
+ }
+ pub fn set(&self, amount: usize) {
+ self.progress.store(amount, Ordering::Relaxed);
+ }
+ pub fn add(&self, amount: usize) {
+ self.progress
+ .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
+ calculate_update(&self.progress_object);
+ }
+ pub fn skip(&self, amount: usize) {
+ self.progress
+ .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
+ // Offset the bytes at last offset by this amount
+ self.progress_object
+ .bytes_last_update
+ .fetch_add(amount, Ordering::Relaxed);
+ // Dont' fire update
+ }
+}
+
+impl ProgressObject {
+ pub fn new(max: usize, length: usize, sender: Sender) -> Self {
+ let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
+ // TODO: consolidate this calculation with the set_max function below
+ Self {
+ max: Arc::new(Mutex::new(max)),
+ progress_instances: Arc::new(arr),
+ start: Arc::new(Mutex::new(Instant::now())),
+ sender,
+
+ last_update_time: Arc::new(AtomicInstant::now()),
+ bytes_last_update: Arc::new(AtomicUsize::new(0)),
+ rolling: RollingProgressWindow::new(),
+ }
+ }
+
+ pub fn set_time_now(&self) {
+ *self.start.lock().unwrap() = Instant::now();
+ }
+ pub fn sum(&self) -> usize {
+ self.progress_instances
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|instance| instance.load(Ordering::Relaxed))
+ .sum()
+ }
+ pub fn get_max(&self) -> usize {
+ *self.max.lock().unwrap()
+ }
+ pub fn set_max(&self, new_max: usize) {
+ *self.max.lock().unwrap() = new_max;
+ }
+ pub fn set_size(&self, length: usize) {
+ *self.progress_instances.lock().unwrap() =
+ (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
+ }
+ pub fn get_progress(&self) -> f64 {
+ self.sum() as f64 / self.get_max() as f64
+ }
+ pub fn get(&self, index: usize) -> Arc {
+ self.progress_instances.lock().unwrap()[index].clone()
+ }
+ fn update_window(&self, kilobytes_per_second: usize) {
+ self.rolling.update(kilobytes_per_second);
+ }
+}
+
+#[throttle(1, Duration::from_millis(20))]
+pub fn calculate_update(progress: &ProgressObject) {
+ let last_update_time = progress
+ .last_update_time
+ .swap(Instant::now(), Ordering::SeqCst);
+ let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
+
+ let current_bytes_downloaded = progress.sum();
+ let max = progress.get_max();
+ let bytes_at_last_update = progress
+ .bytes_last_update
+ .swap(current_bytes_downloaded, Ordering::Relaxed);
+
+ let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
+
+ let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
+
+ let bytes_remaining = max - current_bytes_downloaded; // bytes
+
+ progress.update_window(kilobytes_per_second);
+ push_update(progress, bytes_remaining);
+}
+
+#[throttle(1, Duration::from_millis(500))]
+pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
+ let average_speed = progress.rolling.get_average();
+ let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
+
+ update_ui(progress, average_speed, time_remaining);
+ update_queue(progress);
+}
+
+fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
+ progress_object
+ .sender
+ .send(DownloadManagerSignal::UpdateUIStats(
+ kilobytes_per_second,
+ time_remaining,
+ ))
+ .unwrap();
+}
+
+fn update_queue(progress: &ProgressObject) {
+ progress
+ .sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+}
diff --git a/src-tauri/src/download_manager/queue.rs b/src-tauri/src/download_manager/queue.rs
new file mode 100644
index 0000000..f3e9493
--- /dev/null
+++ b/src-tauri/src/download_manager/queue.rs
@@ -0,0 +1,80 @@
+use std::{
+ collections::VecDeque,
+ sync::{Arc, Mutex, MutexGuard},
+};
+
+use super::downloadable_metadata::DownloadableMetadata;
+
+#[derive(Clone)]
+pub struct Queue {
+ inner: Arc>>,
+}
+
+#[allow(dead_code)]
+impl Default for Queue {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Queue {
+ pub fn new() -> Self {
+ Self {
+ inner: Arc::new(Mutex::new(VecDeque::new())),
+ }
+ }
+ pub fn read(&self) -> VecDeque {
+ self.inner.lock().unwrap().clone()
+ }
+ pub fn edit(&self) -> MutexGuard<'_, VecDeque> {
+ self.inner.lock().unwrap()
+ }
+ pub fn pop_front(&self) -> Option {
+ self.edit().pop_front()
+ }
+ pub fn is_empty(&self) -> bool {
+ self.inner.lock().unwrap().len() == 0
+ }
+ pub fn exists(&self, meta: DownloadableMetadata) -> bool {
+ self.read().contains(&meta)
+ }
+ /// Either inserts `interface` at the specified index, or appends to
+ /// the back of the deque if index is greater than the length of the deque
+ pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
+ if self.read().len() > index {
+ self.append(interface);
+ } else {
+ self.edit().insert(index, interface);
+ }
+ }
+ pub fn append(&self, interface: DownloadableMetadata) {
+ self.edit().push_back(interface);
+ }
+ pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option {
+ let mut queue = self.edit();
+ let front = queue.front()?;
+ if front == meta {
+ return queue.pop_front();
+ }
+ None
+ }
+ pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option {
+ self.read().iter().position(|data| data == meta)
+ }
+ pub fn move_to_index_by_meta(
+ &self,
+ meta: &DownloadableMetadata,
+ new_index: usize,
+ ) -> Result<(), ()> {
+ let index = match self.get_by_meta(meta) {
+ Some(index) => index,
+ None => return Err(()),
+ };
+ let existing = match self.edit().remove(index) {
+ Some(existing) => existing,
+ None => return Err(()),
+ };
+ self.edit().insert(new_index, existing);
+ Ok(())
+ }
+}
diff --git a/src-tauri/src/download_manager/rolling_progress_updates.rs b/src-tauri/src/download_manager/rolling_progress_updates.rs
new file mode 100644
index 0000000..2239b9a
--- /dev/null
+++ b/src-tauri/src/download_manager/rolling_progress_updates.rs
@@ -0,0 +1,33 @@
+use std::sync::{
+ atomic::{AtomicUsize, Ordering},
+ Arc,
+};
+
+#[derive(Clone)]
+pub struct RollingProgressWindow {
+ window: Arc<[AtomicUsize; S]>,
+ current: Arc,
+}
+impl RollingProgressWindow {
+ pub fn new() -> Self {
+ Self {
+ window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
+ current: Arc::new(AtomicUsize::new(0)),
+ }
+ }
+ pub fn update(&self, kilobytes_per_second: usize) {
+ let index = self.current.fetch_add(1, Ordering::SeqCst);
+ let current = &self.window[index % S];
+ current.store(kilobytes_per_second, Ordering::SeqCst);
+ }
+ pub fn get_average(&self) -> usize {
+ let current = self.current.load(Ordering::SeqCst);
+ self.window
+ .iter()
+ .enumerate()
+ .filter(|(i, _)| i < ¤t)
+ .map(|(_, x)| x.load(Ordering::Relaxed))
+ .sum::()
+ / S
+ }
+}
diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs
deleted file mode 100644
index 99d8217..0000000
--- a/src-tauri/src/downloads/download_agent.rs
+++ /dev/null
@@ -1,335 +0,0 @@
-use crate::auth::generate_authorization_header;
-use crate::db::DatabaseImpls;
-use crate::downloads::manifest::{DropDownloadContext, DropManifest};
-use crate::downloads::progress_object::ProgressHandle;
-use crate::remote::RemoteAccessError;
-use crate::DB;
-use core::time;
-use log::{debug, error, info};
-use rayon::ThreadPoolBuilder;
-use serde::ser::{Error, SerializeMap};
-use serde::{Deserialize, Serialize};
-use std::fmt::{Display, Formatter};
-use std::fs::{create_dir_all, File};
-use std::io;
-use std::path::Path;
-use std::sync::mpsc::Sender;
-use std::sync::{Arc, Mutex};
-use std::time::Instant;
-use urlencoding::encode;
-
-#[cfg(target_os = "linux")]
-use rustix::fs::{fallocate, FallocateFlags};
-
-use super::download_logic::download_game_chunk;
-use super::download_manager::DownloadManagerSignal;
-use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
-use super::progress_object::ProgressObject;
-use super::stored_manifest::StoredManifest;
-
-pub struct GameDownloadAgent {
- pub id: String,
- pub version: String,
- pub control_flag: DownloadThreadControl,
- contexts: Vec,
- completed_contexts: Mutex>,
- pub manifest: Mutex>,
- pub progress: Arc,
- sender: Sender,
- pub stored_manifest: StoredManifest,
-}
-
-#[derive(Debug)]
-pub enum GameDownloadError {
- Communication(RemoteAccessError),
- Checksum,
- Setup(SetupError),
- Lock,
- IoError(io::Error),
- DownloadError,
-}
-
-#[derive(Debug)]
-pub enum SetupError {
- Context,
-}
-
-impl Display for GameDownloadError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- GameDownloadError::Communication(error) => write!(f, "{}", error),
- GameDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
- GameDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
- GameDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
- GameDownloadError::IoError(error) => write!(f, "{}", error),
- GameDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
- }
- }
-}
-
-impl Display for SetupError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- SetupError::Context => write!(f, "Failed to generate contexts for download"),
- }
- }
-}
-
-impl GameDownloadAgent {
- pub fn new(
- id: String,
- version: String,
- target_download_dir: usize,
- sender: Sender,
- ) -> Self {
- // Don't run by default
- let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
-
- let db_lock = DB.borrow_data().unwrap();
- let base_dir = db_lock.games.install_dirs[target_download_dir].clone();
- drop(db_lock);
-
- let base_dir_path = Path::new(&base_dir);
- let data_base_dir_path = base_dir_path.join(id.clone());
-
- let stored_manifest =
- StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
-
- Self {
- id,
- version,
- control_flag,
- manifest: Mutex::new(None),
- contexts: Vec::new(),
- completed_contexts: Mutex::new(Vec::new()),
- progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
- sender,
- stored_manifest,
- }
- }
-
- // Blocking
- pub fn setup_download(&mut self) -> Result<(), GameDownloadError> {
- self.ensure_manifest_exists()?;
- info!("Ensured manifest exists");
-
- self.ensure_contexts()?;
- info!("Ensured contexts exists");
-
- self.control_flag.set(DownloadThreadControlFlag::Go);
-
- Ok(())
- }
-
- // Blocking
- pub fn download(&mut self) -> Result<(), GameDownloadError> {
- self.setup_download()?;
- self.set_progress_object_params();
- let timer = Instant::now();
- self.run().map_err(|_| GameDownloadError::DownloadError)?;
-
- info!(
- "{} took {}ms to download",
- self.id,
- timer.elapsed().as_millis()
- );
- Ok(())
- }
-
- pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> {
- if self.manifest.lock().unwrap().is_some() {
- return Ok(());
- }
-
- self.download_manifest()
- }
-
- fn download_manifest(&self) -> Result<(), GameDownloadError> {
- let base_url = DB.fetch_base_url();
- let manifest_url = base_url
- .join(
- format!(
- "/api/v1/client/metadata/manifest?id={}&version={}",
- self.id,
- encode(&self.version)
- )
- .as_str(),
- )
- .unwrap();
-
- let header = generate_authorization_header();
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(manifest_url.to_string())
- .header("Authorization", header)
- .send()
- .unwrap();
-
- if response.status() != 200 {
- return Err(GameDownloadError::Communication(
- RemoteAccessError::ManifestDownloadFailed(
- response.status(),
- response.text().unwrap(),
- ),
- ));
- }
-
- let manifest_download = response.json::().unwrap();
-
- if let Ok(mut manifest) = self.manifest.lock() {
- *manifest = Some(manifest_download);
- return Ok(());
- }
-
- Err(GameDownloadError::Lock)
- }
-
- fn set_progress_object_params(&self) {
- // Avoid re-setting it
- if self.progress.get_max() != 0 {
- return;
- }
-
- let length = self.contexts.len();
-
- let chunk_count = self.contexts.iter().map(|chunk| chunk.length).sum();
-
- debug!("Setting ProgressObject max to {}", chunk_count);
- self.progress.set_max(chunk_count);
- debug!("Setting ProgressObject size to {}", length);
- self.progress.set_size(length);
- debug!("Setting ProgressObject time to now");
- self.progress.set_time_now();
- }
-
- pub fn ensure_contexts(&mut self) -> Result<(), GameDownloadError> {
- if !self.contexts.is_empty() {
- return Ok(());
- }
-
- self.generate_contexts()?;
- Ok(())
- }
-
- pub fn generate_contexts(&mut self) -> Result<(), GameDownloadError> {
- let manifest = self.manifest.lock().unwrap().clone().unwrap();
- let game_id = self.id.clone();
-
- let mut contexts = Vec::new();
- let base_path = Path::new(&self.stored_manifest.base_path);
- create_dir_all(base_path).unwrap();
-
- *self.completed_contexts.lock().unwrap() = self.stored_manifest.get_completed_contexts();
-
- info!(
- "Completed contexts: {:?}",
- *self.completed_contexts.lock().unwrap()
- );
-
- for (raw_path, chunk) in manifest {
- let path = base_path.join(Path::new(&raw_path));
-
- let container = path.parent().unwrap();
- create_dir_all(container).unwrap();
-
- let file = File::create(path.clone()).unwrap();
- let mut running_offset = 0;
-
- for (index, length) in chunk.lengths.iter().enumerate() {
- contexts.push(DropDownloadContext {
- file_name: raw_path.to_string(),
- version: chunk.version_name.to_string(),
- offset: running_offset,
- index,
- game_id: game_id.to_string(),
- path: path.clone(),
- checksum: chunk.checksums[index].clone(),
- length: *length,
- permissions: chunk.permissions,
- });
- running_offset += *length as u64;
- }
-
- #[cfg(target_os = "linux")]
- if running_offset > 0 {
- let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
- }
- }
- self.contexts = contexts;
-
- Ok(())
- }
-
- pub fn run(&self) -> Result<(), ()> {
- info!("downloading game: {}", self.id);
- const DOWNLOAD_MAX_THREADS: usize = 1;
-
- let pool = ThreadPoolBuilder::new()
- .num_threads(DOWNLOAD_MAX_THREADS)
- .build()
- .unwrap();
-
- let completed_indexes = Arc::new(Mutex::new(Vec::new()));
- let completed_indexes_loop_arc = completed_indexes.clone();
-
- pool.scope(move |scope| {
- let completed_lock = self.completed_contexts.lock().unwrap();
-
- for (index, context) in self.contexts.iter().enumerate() {
- let progress = self.progress.get(index); // Clone arcs
- let progress_handle = ProgressHandle::new(progress, self.progress.clone());
- // If we've done this one already, skip it
- if completed_lock.contains(&index) {
- progress_handle.add(context.length);
- continue;
- }
-
- let context = context.clone();
- let control_flag = self.control_flag.clone(); // Clone arcs
- let completed_indexes_ref = completed_indexes_loop_arc.clone();
-
- scope.spawn(move |_| {
- match download_game_chunk(context.clone(), control_flag, progress_handle) {
- Ok(res) => {
- if res {
- let mut lock = completed_indexes_ref.lock().unwrap();
- lock.push(index);
- }
- }
- Err(e) => {
- error!("GameDownloadError: {}", e);
- self.sender.send(DownloadManagerSignal::Error(e)).unwrap();
- }
- }
- });
- }
- });
-
- let completed_lock_len = {
- let mut completed_lock = self.completed_contexts.lock().unwrap();
- let newly_completed_lock = completed_indexes.lock().unwrap();
-
- completed_lock.extend(newly_completed_lock.iter());
-
- completed_lock.len()
- };
-
- // If we're not out of contexts, we're not done, so we don't fire completed
- if completed_lock_len != self.contexts.len() {
- info!("da for {} exited without completing", self.id.clone());
- self.stored_manifest
- .set_completed_contexts(&self.completed_contexts);
- info!("Setting completed contexts");
- self.stored_manifest.write();
- info!("Wrote completed contexts");
- return Ok(());
- }
-
- // We've completed
- self.sender
- .send(DownloadManagerSignal::Completed(self.id.clone()))
- .unwrap();
-
- Ok(())
- }
-}
diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs
deleted file mode 100644
index d3ca2e7..0000000
--- a/src-tauri/src/downloads/download_commands.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use std::sync::Mutex;
-
-use crate::AppState;
-
-#[tauri::command]
-pub fn download_game(
- game_id: String,
- game_version: String,
- install_dir: usize,
- state: tauri::State<'_, Mutex>,
-) -> Result<(), String> {
- state
- .lock()
- .unwrap()
- .download_manager
- .queue_game(game_id, game_version, install_dir)
- .map_err(|_| "An error occurred while communicating with the download manager.".to_string())
-}
-
-#[tauri::command]
-pub fn pause_game_downloads(state: tauri::State<'_, Mutex>) {
- state.lock().unwrap().download_manager.pause_downloads()
-}
-
-#[tauri::command]
-pub fn resume_game_downloads(state: tauri::State<'_, Mutex>) {
- state.lock().unwrap().download_manager.resume_downloads()
-}
-
-#[tauri::command]
-pub fn move_game_in_queue(
- state: tauri::State<'_, Mutex>,
- old_index: usize,
- new_index: usize,
-) {
- state
- .lock()
- .unwrap()
- .download_manager
- .rearrange(old_index, new_index)
-}
-
-#[tauri::command]
-pub fn cancel_game(state: tauri::State<'_, Mutex>, game_id: String) {
- state.lock().unwrap().download_manager.cancel(game_id)
-}
-
-/*
-#[tauri::command]
-pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {}
-*/
-
-/*
-fn use_download_agent(
- state: tauri::State<'_, Mutex>,
- game_id: String,
-) -> Result, String> {
- let lock = state.lock().unwrap();
- let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?;
- Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure
-}
-*/
diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs
deleted file mode 100644
index 9d2ab9d..0000000
--- a/src-tauri/src/downloads/download_manager_builder.rs
+++ /dev/null
@@ -1,417 +0,0 @@
-use std::{
- collections::HashMap,
- sync::{
- mpsc::{channel, Receiver, Sender},
- Arc, Mutex, RwLockWriteGuard,
- },
- thread::{spawn, JoinHandle},
-};
-
-use log::{error, info};
-use tauri::{AppHandle, Emitter};
-
-use crate::{
- db::{Database, GameStatus, GameTransientStatus},
- library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData},
- state::GameStatusManager,
- DB,
-};
-
-use super::{
- download_agent::{GameDownloadAgent, GameDownloadError},
- download_manager::{
- DownloadManager, DownloadManagerSignal, DownloadManagerStatus,
- GameDownloadAgentQueueStandin, GameDownloadStatus,
- },
- download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
- progress_object::ProgressObject,
- queue::Queue,
-};
-
-/*
-
-Welcome to the download manager, the most overengineered, glorious piece of bullshit.
-
-The download manager takes a queue of game_ids and their associated
-GameDownloadAgents, and then, one-by-one, executes them. It provides an interface
-to interact with the currently downloading agent, and manage the queue.
-
-When the DownloadManager is initialised, it is designed to provide a reference
-which can be used to provide some instructions (the DownloadManagerInterface),
-but other than that, it runs without any sort of interruptions.
-
-It does this by opening up two data structures. Primarily is the command_receiver,
-and mpsc (multi-channel-single-producer) which allows commands to be sent from
-the Interface, and queued up for the Manager to process.
-
-These have been mapped in the DownloadManagerSignal docs.
-
-The other way to interact with the DownloadManager is via the donwload_queue,
-which is just a collection of ids which may be rearranged to suit
-whichever download queue order is required.
-
-+----------------------------------------------------------------------------+
-| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
-| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
-| WHICH HAS NOT BEEN ACCOUNTED FOR |
-+----------------------------------------------------------------------------+
-
-This download queue does not actually own any of the GameDownloadAgents. It is
-simply a id-based reference system. The actual Agents are stored in the
-download_agent_registry HashMap, as ordering is no issue here. This is why
-appending or removing from the download_queue must be done via signals.
-
-Behold, my madness - quexeky
-
-*/
-
-// Refactored to consolidate this type. It's a monster.
-pub type CurrentProgressObject = Arc>>>;
-
-pub struct DownloadManagerBuilder {
- download_agent_registry: HashMap>>,
- download_queue: Queue,
- command_receiver: Receiver,
- sender: Sender,
- progress: CurrentProgressObject,
- status: Arc>,
- app_handle: AppHandle,
-
- current_download_agent: Option>, // Should be the only game download agent in the map with the "Go" flag
- current_download_thread: Mutex>>,
- active_control_flag: Option,
-}
-
-impl DownloadManagerBuilder {
- pub fn build(app_handle: AppHandle) -> DownloadManager {
- let queue = Queue::new();
- let (command_sender, command_receiver) = channel();
- let active_progress = Arc::new(Mutex::new(None));
- let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
-
- let manager = Self {
- download_agent_registry: HashMap::new(),
- download_queue: queue.clone(),
- command_receiver,
- status: status.clone(),
- sender: command_sender.clone(),
- progress: active_progress.clone(),
- app_handle,
-
- current_download_agent: None,
- current_download_thread: Mutex::new(None),
- active_control_flag: None,
- };
-
- let terminator = spawn(|| manager.manage_queue());
-
- DownloadManager::new(terminator, queue, active_progress, command_sender)
- }
-
- fn set_game_status, &String) -> ()>(
- &self,
- id: String,
- setter: F,
- ) {
- let mut db_handle = DB.borrow_data_mut().unwrap();
- setter(&mut db_handle, &id);
- drop(db_handle);
- DB.save().unwrap();
-
- let status = GameStatusManager::fetch_state(&id);
-
- self.app_handle
- .emit(
- &format!("update_game/{}", id),
- GameUpdateEvent {
- game_id: id,
- status,
- },
- )
- .unwrap();
- }
-
- fn push_manager_update(&self) {
- let queue = self.download_queue.read();
- let queue_objs: Vec = queue
- .iter()
- .map(|interface| QueueUpdateEventQueueData {
- id: interface.id.clone(),
- status: interface.status.lock().unwrap().clone(),
- progress: interface.progress.get_progress(),
- })
- .collect();
-
- let event_data = QueueUpdateEvent { queue: queue_objs };
- self.app_handle.emit("update_queue", event_data).unwrap();
- }
-
- fn stop_and_wait_current_download(&self) {
- self.set_status(DownloadManagerStatus::Paused);
- if let Some(current_flag) = &self.active_control_flag {
- current_flag.set(DownloadThreadControlFlag::Stop);
- }
-
- let mut download_thread_lock = self.current_download_thread.lock().unwrap();
- if let Some(current_download_thread) = download_thread_lock.take() {
- current_download_thread.join().unwrap();
- }
- drop(download_thread_lock);
- }
-
- fn sync_download_agent(&self) {}
-
- fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc> {
- self.download_queue.pop_front();
- let download_agent = self.download_agent_registry.remove(game_id).unwrap();
- self.cleanup_current_download();
- download_agent
- }
-
- // CAREFUL WITH THIS FUNCTION
- // Make sure the download thread is terminated
- fn cleanup_current_download(&mut self) {
- self.active_control_flag = None;
- *self.progress.lock().unwrap() = None;
- self.current_download_agent = None;
-
- let mut download_thread_lock = self.current_download_thread.lock().unwrap();
- *download_thread_lock = None;
- drop(download_thread_lock);
- }
-
- fn manage_queue(mut self) -> Result<(), ()> {
- loop {
- let signal = match self.command_receiver.recv() {
- Ok(signal) => signal,
- Err(_) => return Err(()),
- };
-
- match signal {
- DownloadManagerSignal::Go => {
- self.manage_go_signal();
- }
- DownloadManagerSignal::Stop => {
- self.manage_stop_signal();
- }
- DownloadManagerSignal::Completed(game_id) => {
- self.manage_completed_signal(game_id);
- }
- DownloadManagerSignal::Queue(game_id, version, target_download_dir) => {
- self.manage_queue_signal(game_id, version, target_download_dir);
- }
- DownloadManagerSignal::Error(e) => {
- self.manage_error_signal(e);
- }
- DownloadManagerSignal::Cancel => {
- self.manage_cancel_signal();
- }
- DownloadManagerSignal::Update => {
- self.push_manager_update();
- }
- DownloadManagerSignal::Finish => {
- self.stop_and_wait_current_download();
- return Ok(());
- }
- DownloadManagerSignal::Remove(game_id) => {
- self.manage_remove_game(game_id);
- }
- };
- }
- }
-
- fn manage_remove_game(&mut self, game_id: String) {
- if let Some(current_download) = &self.current_download_agent {
- if current_download.id == game_id {
- self.manage_cancel_signal();
- }
- }
-
- let index = self.download_queue.get_by_id(game_id.clone()).unwrap();
- let mut queue_handle = self.download_queue.edit();
- queue_handle.remove(index);
- self.set_game_status(game_id, |db_handle, id| {
- db_handle.games.transient_statuses.remove(id);
- });
- drop(queue_handle);
-
- if self.current_download_agent.is_none() {
- self.manage_go_signal();
- }
-
- self.push_manager_update();
- }
-
- fn manage_stop_signal(&mut self) {
- info!("Got signal 'Stop'");
- self.set_status(DownloadManagerStatus::Paused);
- if let Some(active_control_flag) = self.active_control_flag.clone() {
- active_control_flag.set(DownloadThreadControlFlag::Stop);
- }
- }
-
- fn manage_completed_signal(&mut self, game_id: String) {
- info!("Got signal 'Completed'");
- if let Some(interface) = &self.current_download_agent {
- // When if let chains are stabilised, combine these two statements
- if interface.id == game_id {
- info!("Popping consumed data");
- let download_agent = self.remove_and_cleanup_game(&game_id);
- let download_agent_lock = download_agent.lock().unwrap();
-
- let version = download_agent_lock.version.clone();
- let install_dir = download_agent_lock.stored_manifest.base_path.clone().to_string_lossy().to_string();
-
- drop(download_agent_lock);
-
- if let Err(error) =
- on_game_complete(game_id, version, install_dir, &self.app_handle)
- {
- self.sender
- .send(DownloadManagerSignal::Error(
- GameDownloadError::Communication(error),
- ))
- .unwrap();
- }
- }
- }
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- self.sender.send(DownloadManagerSignal::Go).unwrap();
- }
-
- fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) {
- info!("Got signal Queue");
- let download_agent = Arc::new(Mutex::new(GameDownloadAgent::new(
- id.clone(),
- version,
- target_download_dir,
- self.sender.clone(),
- )));
- let download_agent_lock = download_agent.lock().unwrap();
-
- let agent_status = GameDownloadStatus::Queued;
- let interface_data = GameDownloadAgentQueueStandin {
- id: id.clone(),
- status: Mutex::new(agent_status),
- progress: download_agent_lock.progress.clone(),
- };
- let version_name = download_agent_lock.version.clone();
-
- drop(download_agent_lock);
-
- self.download_agent_registry
- .insert(interface_data.id.clone(), download_agent);
- self.download_queue.append(interface_data);
-
- self.set_game_status(id, |db, id| {
- db.games.transient_statuses.insert(
- id.to_string(),
- GameTransientStatus::Downloading { version_name },
- );
- });
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- }
-
- fn manage_go_signal(&mut self) {
- if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) {
- return;
- }
-
- if self.current_download_agent.is_some() {
- info!("skipping go signal due to existing download job");
- return;
- }
-
- info!("current download queue: {:?}", self.download_queue.read());
- let agent_data = self.download_queue.read().front().unwrap().clone();
- info!("starting download for {}", agent_data.id.clone());
- let download_agent = self
- .download_agent_registry
- .get(&agent_data.id)
- .unwrap()
- .clone();
- let download_agent_lock = download_agent.lock().unwrap();
- self.current_download_agent = Some(agent_data);
- // Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData
- let agent_data = self.current_download_agent.clone().unwrap();
-
- let version_name = download_agent_lock.version.clone();
-
- let progress_object = download_agent_lock.progress.clone();
- *self.progress.lock().unwrap() = Some(progress_object);
-
- let active_control_flag = download_agent_lock.control_flag.clone();
- self.active_control_flag = Some(active_control_flag.clone());
-
- let sender = self.sender.clone();
-
- drop(download_agent_lock);
-
- info!("Spawning download");
- let mut download_thread_lock = self.current_download_thread.lock().unwrap();
- *download_thread_lock = Some(spawn(move || {
- let mut download_agent_lock = download_agent.lock().unwrap();
- match download_agent_lock.download() {
- // Returns once we've exited the download
- // (not necessarily completed)
- // The download agent will fire the completed event for us
- Ok(_) => {}
- // If an error occurred while *starting* the download
- Err(err) => {
- error!("error while managing download: {}", err);
- sender.send(DownloadManagerSignal::Error(err)).unwrap();
- }
- };
- drop(download_agent_lock);
- }));
-
- // Set status for games
- for queue_game in self.download_queue.read() {
- let mut status_handle = queue_game.status.lock().unwrap();
- if queue_game.id == agent_data.id {
- *status_handle = GameDownloadStatus::Downloading;
- } else {
- *status_handle = GameDownloadStatus::Queued;
- }
- drop(status_handle);
- }
-
- // Set flags for download manager
- active_control_flag.set(DownloadThreadControlFlag::Go);
- self.set_status(DownloadManagerStatus::Downloading);
- self.set_game_status(agent_data.id.clone(), |db, id| {
- db.games.transient_statuses.insert(
- id.to_string(),
- GameTransientStatus::Downloading { version_name },
- );
- });
-
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- }
- fn manage_error_signal(&mut self, error: GameDownloadError) {
- let current_status = self.current_download_agent.clone().unwrap();
-
- self.remove_and_cleanup_game(¤t_status.id); // Remove all the locks and shit
-
- let mut lock = current_status.status.lock().unwrap();
- *lock = GameDownloadStatus::Error;
- self.set_status(DownloadManagerStatus::Error(error));
-
- let game_id = current_status.id.clone();
- self.set_game_status(game_id, |db_handle, id| {
- db_handle.games.transient_statuses.remove(id);
- });
-
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- }
- fn manage_cancel_signal(&mut self) {
- self.stop_and_wait_current_download();
-
- info!("cancel waited for download to finish");
-
- self.cleanup_current_download();
- }
- fn set_status(&self, status: DownloadManagerStatus) {
- *self.status.lock().unwrap() = status;
- }
-}
diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs
deleted file mode 100644
index 023b2c7..0000000
--- a/src-tauri/src/downloads/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-pub mod download_agent;
-pub mod download_commands;
-mod download_logic;
-pub mod download_manager;
-pub mod download_manager_builder;
-mod download_thread_control_flag;
-mod manifest;
-mod progress_object;
-pub mod queue;
-mod stored_manifest;
\ No newline at end of file
diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs
deleted file mode 100644
index 67f4b29..0000000
--- a/src-tauri/src/downloads/progress_object.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-use std::{
- sync::{
- atomic::{AtomicUsize, Ordering},
- mpsc::Sender,
- Arc, Mutex,
- },
- time::Instant,
-};
-
-use log::info;
-
-use super::download_manager::DownloadManagerSignal;
-
-#[derive(Clone)]
-pub struct ProgressObject {
- max: Arc>,
- progress_instances: Arc>>>,
- start: Arc>,
- sender: Sender,
-
- points_towards_update: Arc,
- points_to_push_update: Arc>,
-}
-
-pub struct ProgressHandle {
- progress: Arc,
- progress_object: Arc,
-}
-
-impl ProgressHandle {
- pub fn new(progress: Arc, progress_object: Arc) -> Self {
- Self {
- progress,
- progress_object,
- }
- }
- pub fn set(&self, amount: usize) {
- self.progress.store(amount, Ordering::Relaxed);
- }
- pub fn add(&self, amount: usize) {
- self.progress
- .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
- self.progress_object.check_push_update(amount);
- }
-}
-
-pub const PROGRESS_UPDATES: usize = 100;
-
-impl ProgressObject {
- pub fn new(max: usize, length: usize, sender: Sender) -> Self {
- let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
- // TODO: consolidate this calculation with the set_max function below
- let points_to_push_update = max / PROGRESS_UPDATES;
- Self {
- max: Arc::new(Mutex::new(max)),
- progress_instances: Arc::new(arr),
- start: Arc::new(Mutex::new(Instant::now())),
- sender,
-
- points_towards_update: Arc::new(AtomicUsize::new(0)),
- points_to_push_update: Arc::new(Mutex::new(points_to_push_update)),
- }
- }
-
- pub fn check_push_update(&self, amount_added: usize) {
- let current_amount = self
- .points_towards_update
- .fetch_add(amount_added, Ordering::Relaxed);
-
- let to_update_handle = self.points_to_push_update.lock().unwrap();
- let to_update = *to_update_handle;
- drop(to_update_handle);
-
- if current_amount < to_update {
- return;
- }
- self.points_towards_update
- .fetch_sub(to_update, Ordering::Relaxed);
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- }
-
- pub fn set_time_now(&self) {
- *self.start.lock().unwrap() = Instant::now();
- }
- pub fn sum(&self) -> usize {
- self.progress_instances
- .lock()
- .unwrap()
- .iter()
- .map(|instance| instance.load(Ordering::Relaxed))
- .sum()
- }
- pub fn get_max(&self) -> usize {
- *self.max.lock().unwrap()
- }
- pub fn set_max(&self, new_max: usize) {
- *self.max.lock().unwrap() = new_max;
- *self.points_to_push_update.lock().unwrap() = new_max / PROGRESS_UPDATES;
- info!("points to push update: {}", new_max / PROGRESS_UPDATES);
- }
- pub fn set_size(&self, length: usize) {
- *self.progress_instances.lock().unwrap() =
- (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
- }
- pub fn get_progress(&self) -> f64 {
- self.sum() as f64 / self.get_max() as f64
- }
- pub fn get(&self, index: usize) -> Arc {
- self.progress_instances.lock().unwrap()[index].clone()
- }
-}
diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs
deleted file mode 100644
index 0ea65ca..0000000
--- a/src-tauri/src/downloads/queue.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use std::{
- collections::VecDeque,
- sync::{Arc, Mutex, MutexGuard},
-};
-
-use super::download_manager::GameDownloadAgentQueueStandin;
-
-#[derive(Clone)]
-pub struct Queue {
- inner: Arc>>>,
-}
-
-#[allow(dead_code)]
-impl Queue {
- pub fn new() -> Self {
- Self {
- inner: Arc::new(Mutex::new(VecDeque::new())),
- }
- }
- pub fn read(&self) -> VecDeque> {
- self.inner.lock().unwrap().clone()
- }
- pub fn edit(&self) -> MutexGuard<'_, VecDeque>> {
- self.inner.lock().unwrap()
- }
- pub fn pop_front(&self) -> Option> {
- self.edit().pop_front()
- }
- pub fn empty(&self) -> bool {
- self.inner.lock().unwrap().len() == 0
- }
- /// Either inserts `interface` at the specified index, or appends to
- /// the back of the deque if index is greater than the length of the deque
- pub fn insert(&self, interface: GameDownloadAgentQueueStandin, index: usize) {
- if self.read().len() > index {
- self.append(interface);
- } else {
- self.edit().insert(index, Arc::new(interface));
- }
- }
- pub fn append(&self, interface: GameDownloadAgentQueueStandin) {
- self.edit().push_back(Arc::new(interface));
- }
- pub fn pop_front_if_equal(
- &self,
- game_id: String,
- ) -> Option> {
- let mut queue = self.edit();
- let front = match queue.front() {
- Some(front) => front,
- None => return None,
- };
- if front.id == game_id {
- return queue.pop_front();
- }
- None
- }
- pub fn get_by_id(&self, game_id: String) -> Option {
- self.read().iter().position(|data| data.id == game_id)
- }
- pub fn move_to_index_by_id(&self, game_id: String, new_index: usize) -> Result<(), ()> {
- let index = match self.get_by_id(game_id) {
- Some(index) => index,
- None => return Err(()),
- };
- let existing = match self.edit().remove(index) {
- Some(existing) => existing,
- None => return Err(()),
- };
- self.edit().insert(new_index, existing);
- Ok(())
- }
-}
diff --git a/src-tauri/src/error/application_download_error.rs b/src-tauri/src/error/application_download_error.rs
new file mode 100644
index 0000000..d68bd71
--- /dev/null
+++ b/src-tauri/src/error/application_download_error.rs
@@ -0,0 +1,32 @@
+use std::{
+ fmt::{Display, Formatter},
+ io,
+};
+
+use serde_with::SerializeDisplay;
+
+use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
+
+// TODO: Rename / separate from downloads
+#[derive(Debug, Clone, SerializeDisplay)]
+pub enum ApplicationDownloadError {
+ Communication(RemoteAccessError),
+ Checksum,
+ Setup(SetupError),
+ Lock,
+ IoError(io::ErrorKind),
+ DownloadError,
+}
+
+impl Display for ApplicationDownloadError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
+ ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
+ ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
+ ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
+ ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
+ ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
+ }
+ }
+}
diff --git a/src-tauri/src/error/drop_server_error.rs b/src-tauri/src/error/drop_server_error.rs
new file mode 100644
index 0000000..ab42263
--- /dev/null
+++ b/src-tauri/src/error/drop_server_error.rs
@@ -0,0 +1,10 @@
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct DropServerError {
+ pub status_code: usize,
+ pub status_message: String,
+ pub message: String,
+ pub url: String,
+}
diff --git a/src-tauri/src/error/library_error.rs b/src-tauri/src/error/library_error.rs
new file mode 100644
index 0000000..c13dd23
--- /dev/null
+++ b/src-tauri/src/error/library_error.rs
@@ -0,0 +1,19 @@
+use std::fmt::Display;
+
+use serde_with::SerializeDisplay;
+
+#[derive(SerializeDisplay)]
+pub enum LibraryError {
+ MetaNotFound(String),
+}
+impl Display for LibraryError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ LibraryError::MetaNotFound(id) => write!(
+ f,
+ "Could not locate any installed version of game ID {} in the database",
+ id
+ ),
+ }
+ }
+}
diff --git a/src-tauri/src/error/mod.rs b/src-tauri/src/error/mod.rs
new file mode 100644
index 0000000..89b74ae
--- /dev/null
+++ b/src-tauri/src/error/mod.rs
@@ -0,0 +1,6 @@
+pub mod application_download_error;
+pub mod drop_server_error;
+pub mod library_error;
+pub mod process_error;
+pub mod remote_access_error;
+pub mod setup_error;
diff --git a/src-tauri/src/error/process_error.rs b/src-tauri/src/error/process_error.rs
new file mode 100644
index 0000000..8afc9dc
--- /dev/null
+++ b/src-tauri/src/error/process_error.rs
@@ -0,0 +1,31 @@
+use std::{fmt::Display, io::Error};
+
+use serde_with::SerializeDisplay;
+
+#[derive(SerializeDisplay)]
+pub enum ProcessError {
+ SetupRequired,
+ NotInstalled,
+ AlreadyRunning,
+ NotDownloaded,
+ InvalidID,
+ InvalidVersion,
+ IOError(Error),
+ InvalidPlatform,
+}
+
+impl Display for ProcessError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = match self {
+ ProcessError::SetupRequired => "Game not set up",
+ ProcessError::NotInstalled => "Game not installed",
+ ProcessError::AlreadyRunning => "Game already running",
+ ProcessError::NotDownloaded => "Game not downloaded",
+ ProcessError::InvalidID => "Invalid Game ID",
+ ProcessError::InvalidVersion => "Invalid Game version",
+ ProcessError::IOError(error) => &error.to_string(),
+ ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
+ };
+ write!(f, "{}", s)
+ }
+}
diff --git a/src-tauri/src/error/remote_access_error.rs b/src-tauri/src/error/remote_access_error.rs
new file mode 100644
index 0000000..32572b2
--- /dev/null
+++ b/src-tauri/src/error/remote_access_error.rs
@@ -0,0 +1,69 @@
+use std::{
+ error::Error,
+ fmt::{Display, Formatter},
+ sync::Arc,
+};
+
+use http::StatusCode;
+use serde_with::SerializeDisplay;
+use url::ParseError;
+
+use super::drop_server_error::DropServerError;
+
+#[derive(Debug, Clone, SerializeDisplay)]
+pub enum RemoteAccessError {
+ FetchError(Arc),
+ ParsingError(ParseError),
+ InvalidEndpoint,
+ HandshakeFailed(String),
+ GameNotFound,
+ InvalidResponse(DropServerError),
+ InvalidRedirect,
+ ManifestDownloadFailed(StatusCode, String),
+ OutOfSync,
+ Generic(String),
+}
+
+impl Display for RemoteAccessError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ RemoteAccessError::FetchError(error) => write!(
+ f,
+ "{}: {}",
+ error,
+ error
+ .source()
+ .map(|e| e.to_string())
+ .or_else(|| Some("Unknown error".to_string()))
+ .unwrap()
+ ),
+ RemoteAccessError::ParsingError(parse_error) => {
+ write!(f, "{}", parse_error)
+ }
+ RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
+ RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
+ RemoteAccessError::GameNotFound => write!(f, "could not find game on server"),
+ RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
+ RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
+ RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
+ f,
+ "failed to download game manifest: {} {}",
+ status, response
+ ),
+ RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
+ RemoteAccessError::Generic(message) => write!(f, "{}", message),
+ }
+ }
+}
+
+impl From for RemoteAccessError {
+ fn from(err: reqwest::Error) -> Self {
+ RemoteAccessError::FetchError(Arc::new(err))
+ }
+}
+impl From for RemoteAccessError {
+ fn from(err: ParseError) -> Self {
+ RemoteAccessError::ParsingError(err)
+ }
+}
+impl std::error::Error for RemoteAccessError {}
diff --git a/src-tauri/src/error/setup_error.rs b/src-tauri/src/error/setup_error.rs
new file mode 100644
index 0000000..bd76ce5
--- /dev/null
+++ b/src-tauri/src/error/setup_error.rs
@@ -0,0 +1,14 @@
+use std::fmt::{Display, Formatter};
+
+#[derive(Debug, Clone)]
+pub enum SetupError {
+ Context,
+}
+
+impl Display for SetupError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ SetupError::Context => write!(f, "failed to generate contexts for download"),
+ }
+ }
+}
diff --git a/src-tauri/src/games/commands.rs b/src-tauri/src/games/commands.rs
new file mode 100644
index 0000000..8f1d652
--- /dev/null
+++ b/src-tauri/src/games/commands.rs
@@ -0,0 +1,53 @@
+use std::sync::Mutex;
+
+use tauri::AppHandle;
+
+use crate::{
+ database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState
+};
+
+use super::{
+ library::{
+ fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
+ Game,
+ },
+ state::{GameStatusManager, GameStatusWithTransient},
+};
+
+#[tauri::command]
+pub fn fetch_library(app: AppHandle) -> Result, RemoteAccessError> {
+ fetch_library_logic(app)
+}
+
+#[tauri::command]
+pub fn fetch_game(
+ game_id: String,
+ app: tauri::AppHandle,
+) -> Result {
+ fetch_game_logic(game_id, app)
+}
+
+#[tauri::command]
+pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
+ GameStatusManager::fetch_state(&id)
+}
+
+#[tauri::command]
+pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
+ let meta = match get_current_meta(&game_id) {
+ Some(data) => data,
+ None => return Err(LibraryError::MetaNotFound(game_id)),
+ };
+ println!("{:?}", meta);
+ uninstall_game_logic(meta, &app_handle);
+
+ Ok(())
+}
+
+#[tauri::command]
+pub fn fetch_game_verion_options(
+ game_id: String,
+ state: tauri::State<'_, Mutex>,
+) -> Result, RemoteAccessError> {
+ fetch_game_verion_options_logic(game_id, state)
+}
diff --git a/src-tauri/src/games/downloads/commands.rs b/src-tauri/src/games/downloads/commands.rs
new file mode 100644
index 0000000..67b1359
--- /dev/null
+++ b/src-tauri/src/games/downloads/commands.rs
@@ -0,0 +1,32 @@
+use std::sync::{Arc, Mutex};
+
+use crate::{
+ download_manager::{
+ download_manager::DownloadManagerSignal, downloadable::Downloadable,
+ internal_error::InternalError,
+ },
+ AppState,
+};
+
+use super::download_agent::GameDownloadAgent;
+
+#[tauri::command]
+pub fn download_game(
+ game_id: String,
+ game_version: String,
+ install_dir: usize,
+ state: tauri::State<'_, Mutex>,
+) -> Result<(), InternalError> {
+ let sender = state.lock().unwrap().download_manager.get_sender();
+ let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
+ game_id,
+ game_version,
+ install_dir,
+ sender,
+ )) as Box);
+ Ok(state
+ .lock()
+ .unwrap()
+ .download_manager
+ .queue_download(game_download_agent)?)
+}
diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs
new file mode 100644
index 0000000..d4fb1cd
--- /dev/null
+++ b/src-tauri/src/games/downloads/download_agent.rs
@@ -0,0 +1,407 @@
+use crate::auth::generate_authorization_header;
+use crate::database::db::{
+ borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
+ GameDownloadStatus,
+};
+use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
+use crate::download_manager::download_thread_control_flag::{
+ DownloadThreadControl, DownloadThreadControlFlag,
+};
+use crate::download_manager::downloadable::Downloadable;
+use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
+use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
+use crate::error::application_download_error::ApplicationDownloadError;
+use crate::error::remote_access_error::RemoteAccessError;
+use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
+use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
+use crate::remote::requests::make_request;
+use crate::DB;
+use log::{debug, error, info};
+use rayon::ThreadPoolBuilder;
+use slice_deque::SliceDeque;
+use std::fs::{create_dir_all, File};
+use std::path::Path;
+use std::sync::mpsc::Sender;
+use std::sync::{Arc, Mutex};
+use std::time::Instant;
+use tauri::{AppHandle, Emitter};
+use urlencoding::encode;
+
+#[cfg(target_os = "linux")]
+use rustix::fs::{fallocate, FallocateFlags};
+
+use super::download_logic::download_game_chunk;
+use super::stored_manifest::StoredManifest;
+
+pub struct GameDownloadAgent {
+ pub id: String,
+ pub version: String,
+ pub control_flag: DownloadThreadControl,
+ contexts: Mutex>,
+ completed_contexts: Mutex>,
+ pub manifest: Mutex>,
+ pub progress: Arc,
+ sender: Sender,
+ pub stored_manifest: StoredManifest,
+ status: Mutex,
+}
+
+impl GameDownloadAgent {
+ pub fn new(
+ id: String,
+ version: String,
+ target_download_dir: usize,
+ sender: Sender,
+ ) -> Self {
+ // Don't run by default
+ let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
+
+ let db_lock = borrow_db_checked();
+ let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
+ drop(db_lock);
+
+ let base_dir_path = Path::new(&base_dir);
+ let data_base_dir_path = base_dir_path.join(id.clone());
+
+ let stored_manifest =
+ StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
+
+ Self {
+ id,
+ version,
+ control_flag,
+ manifest: Mutex::new(None),
+ contexts: Mutex::new(Vec::new()),
+ completed_contexts: Mutex::new(SliceDeque::new()),
+ progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
+ sender,
+ stored_manifest,
+ status: Mutex::new(DownloadStatus::Queued),
+ }
+ }
+
+ // Blocking
+ pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
+ self.ensure_manifest_exists()?;
+
+ self.ensure_contexts()?;
+
+ self.control_flag.set(DownloadThreadControlFlag::Go);
+
+ Ok(())
+ }
+
+ // Blocking
+ pub fn download(&self, app_handle: &AppHandle) -> Result {
+ self.setup_download()?;
+ self.set_progress_object_params();
+ let timer = Instant::now();
+ push_game_update(
+ app_handle,
+ &self.metadata().id,
+ (
+ None,
+ Some(ApplicationTransientStatus::Downloading {
+ version_name: self.version.clone(),
+ }),
+ ),
+ );
+ let res = self
+ .run()
+ .map_err(|_| ApplicationDownloadError::DownloadError);
+
+ debug!(
+ "{} took {}ms to download",
+ self.id,
+ timer.elapsed().as_millis()
+ );
+ res
+ }
+
+ pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
+ if self.manifest.lock().unwrap().is_some() {
+ return Ok(());
+ }
+
+ self.download_manifest()
+ }
+
+ fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
+ let header = generate_authorization_header();
+ let client = reqwest::blocking::Client::new();
+ let response = make_request(
+ &client,
+ &["/api/v1/client/game/manifest"],
+ &[("id", &self.id), ("version", &self.version)],
+ |f| f.header("Authorization", header),
+ )
+ .map_err(|e| ApplicationDownloadError::Communication(e))?
+ .send()
+ .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
+
+ if response.status() != 200 {
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::ManifestDownloadFailed(
+ response.status(),
+ response.text().unwrap(),
+ ),
+ ));
+ }
+
+ let manifest_download: DropManifest = response.json().unwrap();
+
+ if let Ok(mut manifest) = self.manifest.lock() {
+ *manifest = Some(manifest_download);
+ return Ok(());
+ }
+
+ Err(ApplicationDownloadError::Lock)
+ }
+
+ fn set_progress_object_params(&self) {
+ // Avoid re-setting it
+ if self.progress.get_max() != 0 {
+ return;
+ }
+
+ let contexts = self.contexts.lock().unwrap();
+
+ let length = contexts.len();
+
+ let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
+
+ self.progress.set_max(chunk_count);
+ self.progress.set_size(length);
+ self.progress.set_time_now();
+ }
+
+ pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
+ if !self.contexts.lock().unwrap().is_empty() {
+ return Ok(());
+ }
+
+ self.generate_contexts()?;
+ Ok(())
+ }
+
+ pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> {
+ let manifest = self.manifest.lock().unwrap().clone().unwrap();
+ let game_id = self.id.clone();
+
+ let mut contexts = Vec::new();
+ let base_path = Path::new(&self.stored_manifest.base_path);
+ create_dir_all(base_path).unwrap();
+
+ {
+ let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
+ completed_contexts_lock.clear();
+ completed_contexts_lock
+ .extend_from_slice(&self.stored_manifest.get_completed_contexts());
+ }
+
+ for (raw_path, chunk) in manifest {
+ let path = base_path.join(Path::new(&raw_path));
+
+ let container = path.parent().unwrap();
+ create_dir_all(container).unwrap();
+
+ let file = File::create(path.clone()).unwrap();
+ let mut running_offset = 0;
+
+ for (index, length) in chunk.lengths.iter().enumerate() {
+ contexts.push(DropDownloadContext {
+ file_name: raw_path.to_string(),
+ version: chunk.version_name.to_string(),
+ offset: running_offset,
+ index,
+ game_id: game_id.to_string(),
+ path: path.clone(),
+ checksum: chunk.checksums[index].clone(),
+ length: *length,
+ permissions: chunk.permissions,
+ });
+ running_offset += *length as u64;
+ }
+
+ #[cfg(target_os = "linux")]
+ if running_offset > 0 {
+ let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
+ }
+ }
+ *self.contexts.lock().unwrap() = contexts;
+
+ Ok(())
+ }
+
+ // TODO: Change return value on Err
+ pub fn run(&self) -> Result {
+ let max_download_threads = borrow_db_checked().settings.max_download_threads;
+
+ debug!(
+ "downloading game: {} with {} threads",
+ self.id, max_download_threads
+ );
+ let pool = ThreadPoolBuilder::new()
+ .num_threads(max_download_threads)
+ .build()
+ .unwrap();
+
+ let completed_indexes = Arc::new(boxcar::Vec::new());
+ let completed_indexes_loop_arc = completed_indexes.clone();
+
+ let contexts = self.contexts.lock().unwrap();
+ pool.scope(|scope| {
+ let client = &reqwest::blocking::Client::new();
+ for (index, context) in contexts.iter().enumerate() {
+ let client = client.clone();
+ let completed_indexes = completed_indexes_loop_arc.clone();
+
+ let progress = self.progress.get(index);
+ let progress_handle = ProgressHandle::new(progress, self.progress.clone());
+
+ // If we've done this one already, skip it
+ if self.completed_contexts.lock().unwrap().contains(&index) {
+ progress_handle.skip(context.length);
+ continue;
+ }
+
+ let sender = self.sender.clone();
+
+ let request = match make_request(
+ &client,
+ &["/api/v1/client/chunk"],
+ &[
+ ("id", &context.game_id),
+ ("version", &context.version),
+ ("name", &context.file_name),
+ ("chunk", &context.index.to_string()),
+ ],
+ |r| r.header("Authorization", generate_authorization_header()),
+ ) {
+ Ok(request) => request,
+ Err(e) => {
+ sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap();
+ continue;
+ },
+ };
+
+ scope.spawn(move |_| {
+ match download_game_chunk(context, &self.control_flag, progress_handle, request)
+ {
+ Ok(res) => {
+ if res {
+ completed_indexes.push(index);
+ }
+ }
+ Err(e) => {
+ error!("{}", e);
+ sender.send(DownloadManagerSignal::Error(e)).unwrap();
+ }
+ }
+ });
+ }
+ });
+
+ let newly_completed = completed_indexes.to_owned();
+
+ let completed_lock_len = {
+ let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
+ for (_, item) in newly_completed.iter() {
+ completed_contexts_lock.push_front(*item);
+ }
+
+ completed_contexts_lock.len()
+ };
+
+ // If we're not out of contexts, we're not done, so we don't fire completed
+ if completed_lock_len != contexts.len() {
+ info!(
+ "download agent for {} exited without completing ({}/{})",
+ self.id.clone(),
+ completed_lock_len,
+ contexts.len(),
+ );
+ self.stored_manifest
+ .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
+ self.stored_manifest.write();
+ return Ok(false);
+ }
+
+ // We've completed
+ self.sender
+ .send(DownloadManagerSignal::Completed(self.metadata()))
+ .unwrap();
+
+ Ok(true)
+ }
+}
+
+impl Downloadable for GameDownloadAgent {
+ fn download(&self, app_handle: &AppHandle) -> Result {
+ *self.status.lock().unwrap() = DownloadStatus::Downloading;
+ self.download(app_handle)
+ }
+
+ fn progress(&self) -> Arc {
+ self.progress.clone()
+ }
+
+ fn control_flag(&self) -> DownloadThreadControl {
+ self.control_flag.clone()
+ }
+
+ fn metadata(&self) -> DownloadableMetadata {
+ DownloadableMetadata {
+ id: self.id.clone(),
+ version: Some(self.version.clone()),
+ download_type: DownloadType::Game,
+ }
+ }
+
+ fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
+ *self.status.lock().unwrap() = DownloadStatus::Queued;
+ }
+
+ fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
+ *self.status.lock().unwrap() = DownloadStatus::Error;
+ app_handle
+ .emit("download_error", error.to_string())
+ .unwrap();
+
+ error!("error while managing download: {}", error);
+
+ set_game_status(app_handle, self.metadata(), |db_handle, meta| {
+ db_handle.applications.transient_statuses.remove(meta);
+ });
+ }
+
+ fn on_complete(&self, app_handle: &tauri::AppHandle) {
+ on_game_complete(
+ &self.metadata(),
+ self.stored_manifest.base_path.to_string_lossy().to_string(),
+ app_handle,
+ )
+ .unwrap();
+ }
+
+ // TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
+ fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
+ let meta = self.metadata();
+ *self.status.lock().unwrap() = DownloadStatus::Queued;
+ app_handle
+ .emit(
+ &format!("update_game/{}", meta.id),
+ GameUpdateEvent {
+ game_id: meta.id.clone(),
+ status: (Some(GameDownloadStatus::Remote {}), None),
+ },
+ )
+ .unwrap();
+ }
+
+ fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
+
+ fn status(&self) -> DownloadStatus {
+ self.status.lock().unwrap().clone()
+ }
+}
diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/games/downloads/download_logic.rs
similarity index 64%
rename from src-tauri/src/downloads/download_logic.rs
rename to src-tauri/src/games/downloads/download_logic.rs
index 52cd6ba..70e5b7e 100644
--- a/src-tauri/src/downloads/download_logic.rs
+++ b/src-tauri/src/games/downloads/download_logic.rs
@@ -1,29 +1,23 @@
-use crate::auth::generate_authorization_header;
-use crate::db::DatabaseImpls;
-use crate::downloads::manifest::DropDownloadContext;
-use crate::remote::RemoteAccessError;
-use crate::DB;
+use crate::download_manager::download_thread_control_flag::{
+ DownloadThreadControl, DownloadThreadControlFlag,
+};
+use crate::download_manager::progress_object::ProgressHandle;
+use crate::error::application_download_error::ApplicationDownloadError;
+use crate::error::remote_access_error::RemoteAccessError;
+use crate::games::downloads::manifest::DropDownloadContext;
use log::warn;
use md5::{Context, Digest};
-use reqwest::blocking::Response;
-use tauri::utils::acl::Permission;
+use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions};
-use std::io::Read;
+use std::io::{ErrorKind, Read};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
-use std::thread::sleep;
-use std::time::Duration;
use std::{
fs::{File, OpenOptions},
io::{self, BufWriter, Seek, SeekFrom, Write},
path::PathBuf,
};
-use urlencoding::encode;
-
-use super::download_agent::GameDownloadError;
-use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
-use super::progress_object::ProgressHandle;
pub struct DropWriter {
hasher: Context,
@@ -45,19 +39,17 @@ impl DropWriter {
// Write automatically pushes to file and hasher
impl Write for DropWriter {
fn write(&mut self, buf: &[u8]) -> io::Result {
- /*
self.hasher.write_all(buf).map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!("Unable to write to hasher: {}", e),
)
})?;
- */
self.destination.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
- // self.hasher.flush()?;
+ self.hasher.flush()?;
self.destination.flush()
}
}
@@ -68,18 +60,18 @@ impl Seek for DropWriter {
}
}
-pub struct DropDownloadPipeline {
+pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
pub source: R,
pub destination: DropWriter,
- pub control_flag: DownloadThreadControl,
+ pub control_flag: &'a DownloadThreadControl,
pub progress: ProgressHandle,
pub size: usize,
}
-impl DropDownloadPipeline {
+impl<'a> DropDownloadPipeline<'a, Response, File> {
fn new(
source: Response,
destination: DropWriter,
- control_flag: DownloadThreadControl,
+ control_flag: &'a DownloadThreadControl,
progress: ProgressHandle,
size: usize,
) -> Self {
@@ -124,42 +116,25 @@ impl DropDownloadPipeline {
}
pub fn download_game_chunk(
- ctx: DropDownloadContext,
- control_flag: DownloadThreadControl,
+ ctx: &DropDownloadContext,
+ control_flag: &DownloadThreadControl,
progress: ProgressHandle,
-) -> Result {
+ request: RequestBuilder,
+) -> Result {
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0);
return Ok(false);
}
- let base_url = DB.fetch_base_url();
-
- let client = reqwest::blocking::Client::new();
- let chunk_url = base_url
- .join(&format!(
- "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
- // Encode the parts we don't trust
- ctx.game_id,
- encode(&ctx.version),
- encode(&ctx.file_name),
- ctx.index
- ))
- .unwrap();
-
- let header = generate_authorization_header();
-
- let response = client
- .get(chunk_url)
- .header("Authorization", header)
+ let response = request
.send()
- .map_err(|e| GameDownloadError::Communication(e.into()))?;
+ .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
- warn!("{}", response.text().unwrap());
- return Err(GameDownloadError::Communication(
- RemoteAccessError::InvalidCodeError(400),
+ let err = response.json().unwrap();
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::InvalidResponse(err),
));
}
@@ -173,8 +148,9 @@ pub fn download_game_chunk(
let content_length = response.content_length();
if content_length.is_none() {
- return Err(GameDownloadError::Communication(
- RemoteAccessError::InvalidResponse,
+ warn!("recieved 0 length content from server");
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::InvalidResponse(response.json().unwrap()),
));
}
@@ -186,7 +162,9 @@ pub fn download_game_chunk(
content_length.unwrap().try_into().unwrap(),
);
- let completed = pipeline.copy().map_err(GameDownloadError::IoError)?;
+ let completed = pipeline
+ .copy()
+ .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
if !completed {
return Ok(false);
};
@@ -195,19 +173,17 @@ pub fn download_game_chunk(
#[cfg(unix)]
{
let permissions = Permissions::from_mode(ctx.permissions);
- set_permissions(ctx.path, permissions).unwrap();
+ set_permissions(ctx.path.clone(), permissions).unwrap();
}
- /*
let checksum = pipeline
.finish()
- .map_err(|e| GameDownloadError::IoError(e))?;
+ .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
let res = hex::encode(checksum.0);
if res != ctx.checksum {
- return Err(GameDownloadError::Checksum);
+ return Err(ApplicationDownloadError::Checksum);
}
- */
Ok(true)
}
diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/games/downloads/manifest.rs
similarity index 100%
rename from src-tauri/src/downloads/manifest.rs
rename to src-tauri/src/games/downloads/manifest.rs
diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs
new file mode 100644
index 0000000..c9b3cd4
--- /dev/null
+++ b/src-tauri/src/games/downloads/mod.rs
@@ -0,0 +1,5 @@
+pub mod commands;
+pub mod download_agent;
+mod download_logic;
+mod manifest;
+mod stored_manifest;
diff --git a/src-tauri/src/downloads/stored_manifest.rs b/src-tauri/src/games/downloads/stored_manifest.rs
similarity index 85%
rename from src-tauri/src/downloads/stored_manifest.rs
rename to src-tauri/src/games/downloads/stored_manifest.rs
index cd4006d..fdc232b 100644
--- a/src-tauri/src/downloads/stored_manifest.rs
+++ b/src-tauri/src/games/downloads/stored_manifest.rs
@@ -1,12 +1,11 @@
use std::{
- default,
fs::File,
io::{Read, Write},
- path::{Path, PathBuf},
+ path::PathBuf,
sync::Mutex,
};
-use log::{error, info};
+use log::{error, warn};
use serde::{Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
@@ -44,15 +43,13 @@ impl StoredManifest {
}
};
- let manifest = match serde_binary::from_vec::(s, Endian::Little) {
+ match serde_binary::from_vec::(s, Endian::Little) {
Ok(manifest) => manifest,
Err(e) => {
- error!("{}", e);
+ warn!("{}", e);
StoredManifest::new(game_id, game_version, base_path)
}
- };
-
- return manifest;
+ }
}
pub fn write(&self) {
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
@@ -73,8 +70,8 @@ impl StoredManifest {
Err(e) => error!("{}", e),
};
}
- pub fn set_completed_contexts(&self, completed_contexts: &Mutex>) {
- *self.completed_contexts.lock().unwrap() = completed_contexts.lock().unwrap().clone();
+ pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
+ *self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec {
self.completed_contexts.lock().unwrap().clone()
diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs
new file mode 100644
index 0000000..b2f2fe2
--- /dev/null
+++ b/src-tauri/src/games/library.rs
@@ -0,0 +1,353 @@
+use std::fs::remove_dir_all;
+use std::sync::Mutex;
+use std::thread::spawn;
+
+use log::{debug, error, warn};
+use serde::{Deserialize, Serialize};
+use tauri::Emitter;
+use tauri::{AppHandle, Manager};
+
+use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
+use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
+use crate::download_manager::download_manager::DownloadStatus;
+use crate::download_manager::downloadable_metadata::DownloadableMetadata;
+use crate::error::remote_access_error::RemoteAccessError;
+use crate::games::state::{GameStatusManager, GameStatusWithTransient};
+use crate::remote::auth::generate_authorization_header;
+use crate::remote::requests::make_request;
+use crate::AppState;
+
+#[derive(serde::Serialize)]
+pub struct FetchGameStruct {
+ game: Game,
+ status: GameStatusWithTransient,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct Game {
+ id: String,
+ m_name: String,
+ m_short_description: String,
+ m_description: String,
+ // mDevelopers
+ // mPublishers
+ m_icon_id: String,
+ m_banner_id: String,
+ m_cover_id: String,
+ m_image_library: Vec,
+}
+#[derive(serde::Serialize, Clone)]
+pub struct GameUpdateEvent {
+ pub game_id: String,
+ pub status: (
+ Option,
+ Option,
+ ),
+}
+
+#[derive(Serialize, Clone)]
+pub struct QueueUpdateEventQueueData {
+ pub meta: DownloadableMetadata,
+ pub status: DownloadStatus,
+ pub progress: f64,
+ pub current: usize,
+ pub max: usize,
+}
+
+#[derive(serde::Serialize, Clone)]
+pub struct QueueUpdateEvent {
+ pub queue: Vec,
+}
+
+#[derive(serde::Serialize, Clone)]
+pub struct StatsUpdateEvent {
+ pub speed: usize,
+ pub time: usize,
+}
+
+pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> {
+ let header = generate_authorization_header();
+
+ let client = reqwest::blocking::Client::new();
+ let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
+ f.header("Authorization", header)
+ })?
+ .send()?;
+
+ if response.status() != 200 {
+ let err = response.json().unwrap();
+ warn!("{:?}", err);
+ return Err(RemoteAccessError::InvalidResponse(err));
+ }
+
+ let games: Vec = response.json()?;
+
+ let state = app.state::>();
+ let mut handle = state.lock().unwrap();
+
+ let mut db_handle = borrow_db_mut_checked();
+
+ for game in games.iter() {
+ handle.games.insert(game.id.clone(), game.clone());
+ if !db_handle.applications.game_statuses.contains_key(&game.id) {
+ db_handle
+ .applications
+ .game_statuses
+ .insert(game.id.clone(), GameDownloadStatus::Remote {});
+ }
+ }
+
+ drop(handle);
+
+ Ok(games)
+}
+
+pub fn fetch_game_logic(
+ id: String,
+ app: tauri::AppHandle,
+) -> Result {
+ let state = app.state::>();
+ let mut state_handle = state.lock().unwrap();
+
+ let game = state_handle.games.get(&id);
+ if let Some(game) = game {
+ let status = GameStatusManager::fetch_state(&id);
+
+ let data = FetchGameStruct {
+ game: game.clone(),
+ status,
+ };
+
+ return Ok(data);
+ }
+ let client = reqwest::blocking::Client::new();
+ let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| {
+ r.header("Authorization", generate_authorization_header())
+ })?
+ .send()?;
+
+ if response.status() == 404 {
+ return Err(RemoteAccessError::GameNotFound);
+ }
+ if response.status() != 200 {
+ let err = response.json().unwrap();
+ warn!("{:?}", err);
+ return Err(RemoteAccessError::InvalidResponse(err));
+ }
+
+ let game: Game = response.json()?;
+ state_handle.games.insert(id.clone(), game.clone());
+
+ let mut db_handle = borrow_db_mut_checked();
+
+ db_handle
+ .applications
+ .game_statuses
+ .entry(id.clone())
+ .or_insert(GameDownloadStatus::Remote {});
+ drop(db_handle);
+
+ let status = GameStatusManager::fetch_state(&id);
+
+ let data = FetchGameStruct {
+ game: game.clone(),
+ status,
+ };
+
+ Ok(data)
+}
+
+pub fn fetch_game_verion_options_logic(
+ game_id: String,
+ state: tauri::State<'_, Mutex>,
+) -> Result, RemoteAccessError> {
+ let client = reqwest::blocking::Client::new();
+
+ let response = make_request(
+ &client,
+ &["/api/v1/client/game/versions"],
+ &[("id", &game_id)],
+ |r| r.header("Authorization", generate_authorization_header()),
+ )?
+ .send()?;
+
+ if response.status() != 200 {
+ let err = response.json().unwrap();
+ warn!("{:?}", err);
+ return Err(RemoteAccessError::InvalidResponse(err));
+ }
+
+ let data: Vec = response.json()?;
+
+ let state_lock = state.lock().unwrap();
+ let process_manager_lock = state_lock.process_manager.lock().unwrap();
+ let data: Vec = data
+ .into_iter()
+ .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
+ .collect();
+ drop(process_manager_lock);
+ drop(state_lock);
+
+ Ok(data)
+}
+
+pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
+ println!("triggered uninstall for agent");
+ let mut db_handle = borrow_db_mut_checked();
+ db_handle
+ .applications
+ .transient_statuses
+ .entry(meta.clone())
+ .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
+
+ push_game_update(
+ app_handle,
+ &meta.id,
+ (None, Some(ApplicationTransientStatus::Uninstalling {})),
+ );
+
+ let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
+ if previous_state.is_none() {
+ warn!("uninstall job doesn't have previous state, failing silently");
+ return;
+ }
+ let previous_state = previous_state.unwrap();
+ if let Some((_, install_dir)) = match previous_state {
+ GameDownloadStatus::Installed {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ GameDownloadStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ _ => None,
+ } {
+ db_handle
+ .applications
+ .transient_statuses
+ .entry(meta.clone())
+ .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
+ drop(db_handle);
+
+ let app_handle = app_handle.clone();
+ spawn(move || match remove_dir_all(install_dir) {
+ Err(e) => {
+ error!("{}", e);
+ }
+ Ok(_) => {
+ let mut db_handle = borrow_db_mut_checked();
+ db_handle.applications.transient_statuses.remove(&meta);
+ db_handle
+ .applications
+ .game_statuses
+ .entry(meta.id.clone())
+ .and_modify(|e| *e = GameDownloadStatus::Remote {});
+ drop(db_handle);
+ save_db();
+
+ debug!("uninstalled game id {}", &meta.id);
+
+ push_game_update(
+ &app_handle,
+ &meta.id,
+ (Some(GameDownloadStatus::Remote {}), None),
+ );
+ }
+ });
+ }
+}
+
+pub fn get_current_meta(game_id: &String) -> Option {
+ borrow_db_checked()
+ .applications
+ .installed_game_version
+ .get(game_id)
+ .cloned()
+}
+
+pub fn on_game_complete(
+ meta: &DownloadableMetadata,
+ install_dir: String,
+ app_handle: &AppHandle,
+) -> Result<(), RemoteAccessError> {
+ // Fetch game version information from remote
+ if meta.version.is_none() {
+ return Err(RemoteAccessError::GameNotFound);
+ }
+
+ let header = generate_authorization_header();
+
+ let client = reqwest::blocking::Client::new();
+ let response = make_request(
+ &client,
+ &["/api/v1/client/metadata/version"],
+ &[
+ ("id", &meta.id),
+ ("version", meta.version.as_ref().unwrap()),
+ ],
+ |f| f.header("Authorization", header),
+ )?
+ .send()?;
+
+ let data: GameVersion = response.json()?;
+
+ let mut handle = borrow_db_mut_checked();
+ handle
+ .applications
+ .game_versions
+ .entry(meta.id.clone())
+ .or_default()
+ .insert(meta.version.clone().unwrap(), data.clone());
+ handle
+ .applications
+ .installed_game_version
+ .insert(meta.id.clone(), meta.clone());
+
+ drop(handle);
+ save_db();
+
+ let status = if data.setup_command.is_empty() {
+ GameDownloadStatus::Installed {
+ version_name: meta.version.clone().unwrap(),
+ install_dir,
+ }
+ } else {
+ GameDownloadStatus::SetupRequired {
+ version_name: meta.version.clone().unwrap(),
+ install_dir,
+ }
+ };
+
+ let mut db_handle = borrow_db_mut_checked();
+ db_handle
+ .applications
+ .game_statuses
+ .insert(meta.id.clone(), status.clone());
+ drop(db_handle);
+ save_db();
+ app_handle
+ .emit(
+ &format!("update_game/{}", meta.id),
+ GameUpdateEvent {
+ game_id: meta.id.clone(),
+ status: (Some(status), None),
+ },
+ )
+ .unwrap();
+
+ Ok(())
+}
+
+pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) {
+ app_handle
+ .emit(
+ &format!("update_game/{}", game_id),
+ GameUpdateEvent {
+ game_id: game_id.clone(),
+ status,
+ },
+ )
+ .unwrap();
+}
diff --git a/src-tauri/src/games/mod.rs b/src-tauri/src/games/mod.rs
new file mode 100644
index 0000000..65c5c6b
--- /dev/null
+++ b/src-tauri/src/games/mod.rs
@@ -0,0 +1,4 @@
+pub mod commands;
+pub mod downloads;
+pub mod library;
+pub mod state;
diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs
new file mode 100644
index 0000000..19b1769
--- /dev/null
+++ b/src-tauri/src/games/state.rs
@@ -0,0 +1,29 @@
+use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
+
+pub type GameStatusWithTransient = (
+ Option,
+ Option,
+);
+pub struct GameStatusManager {}
+
+impl GameStatusManager {
+ pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
+ let db_lock = borrow_db_checked();
+ let online_state = match db_lock.applications.installed_game_version.get(game_id) {
+ Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
+ None => None,
+ };
+ let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
+ drop(db_lock);
+
+ if online_state.is_some() {
+ return (None, online_state);
+ }
+
+ if offline_state.is_some() {
+ return (offline_state, None);
+ }
+
+ (None, None)
+ }
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 6e6dcf5..4301ee1 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,47 +1,67 @@
-mod auth;
-mod db;
-mod downloads;
-mod library;
+#![feature(try_trait_v2)]
+mod database;
+mod games;
+
+mod autostart;
+mod cleanup;
+mod commands;
+mod download_manager;
+mod error;
mod process;
mod remote;
-mod state;
-#[cfg(test)]
-mod tests;
-mod cleanup;
-use crate::db::DatabaseImpls;
-use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect};
+use crate::database::db::DatabaseImpls;
+use autostart::{get_autostart_enabled, toggle_autostart};
use cleanup::{cleanup_and_exit, quit};
-use db::{
- add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface,
- DATA_ROOT_DIR,
+use commands::fetch_state;
+use database::commands::{
+ add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
+ fetch_system_data, update_settings,
};
-use downloads::download_commands::*;
-use downloads::download_manager::DownloadManager;
-use downloads::download_manager_builder::DownloadManagerBuilder;
+use database::db::{
+ borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
+};
+use download_manager::commands::{
+ cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
+};
+use download_manager::download_manager::DownloadManager;
+use download_manager::download_manager_builder::DownloadManagerBuilder;
+use games::commands::{
+ fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
+};
+use games::downloads::commands::download_game;
+use games::library::Game;
+use http::Response;
use http::{header::*, response::Builder as ResponseBuilder};
-use library::{fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, Game};
-use log::{debug, info, LevelFilter};
+use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
-use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
-use process::process_commands::launch_game;
+use process::commands::{kill_game, launch_game};
use process::process_manager::ProcessManager;
-use remote::{gen_drop_url, use_remote};
+use remote::auth::{self, generate_authorization_header, recieve_handshake};
+use remote::commands::{
+ auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
+};
+use remote::requests::make_request;
use serde::{Deserialize, Serialize};
+use std::env;
+use std::path::Path;
+use std::str::FromStr;
use std::sync::Arc;
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
-use tauri::menu::{Menu, MenuItem, MenuItemBuilder, PredefinedMenuItem};
+use tauri::ipc::IpcResponse;
+use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
+use tauri_plugin_dialog::DialogExt;
#[derive(Clone, Copy, Serialize)]
pub enum AppStatus {
@@ -65,7 +85,7 @@ pub struct User {
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct AppState {
+pub struct AppState<'a> {
status: AppStatus,
user: Option,
games: HashMap,
@@ -73,28 +93,26 @@ pub struct AppState {
#[serde(skip_serializing)]
download_manager: Arc,
#[serde(skip_serializing)]
- process_manager: Arc>,
+ process_manager: Arc>>,
}
-#[tauri::command]
-fn fetch_state(state: tauri::State<'_, Mutex>) -> Result {
- let guard = state.lock().unwrap();
- let cloned_state = guard.clone();
- drop(guard);
- Ok(cloned_state)
-}
-
-fn setup(handle: AppHandle) -> AppState {
+fn setup(handle: AppHandle) -> AppState<'static> {
let logfile = FileAppender::builder()
- .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
+ .encoder(Box::new(PatternEncoder::new(
+ "{d} | {l} | {f}:{L} - {m}{n}",
+ )))
.append(false)
.build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log"))
.unwrap();
let console = ConsoleAppender::builder()
- .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
+ .encoder(Box::new(PatternEncoder::new(
+ "{d} | {l} | {f}:{L} - {m}{n}",
+ )))
.build();
+ let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info"));
+
let config = Config::builder()
.appenders(vec![
Appender::builder().build("logfile", Box::new(logfile)),
@@ -103,17 +121,17 @@ fn setup(handle: AppHandle) -> AppState {
.build(
Root::builder()
.appenders(vec!["logfile", "console"])
- .build(LevelFilter::Info),
+ .build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
)
.unwrap();
log4rs::init_config(config).unwrap();
let games = HashMap::new();
- let download_manager = Arc::new(DownloadManagerBuilder::build(handle));
- let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
+ let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
+ let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
- debug!("Checking if database is set up");
+ debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
if !is_set_up {
return AppState {
@@ -125,9 +143,59 @@ fn setup(handle: AppHandle) -> AppState {
};
}
- debug!("Database is set up");
+ debug!("database is set up");
+
+ // TODO: Account for possible failure
+ let (app_status, user) = auth::setup();
+
+ let db_handle = borrow_db_checked();
+ let mut missing_games = Vec::new();
+ let statuses = db_handle.applications.game_statuses.clone();
+ drop(db_handle);
+ for (game_id, status) in statuses.into_iter() {
+ match status {
+ database::db::GameDownloadStatus::Remote {} => {}
+ database::db::GameDownloadStatus::SetupRequired {
+ version_name: _,
+ install_dir,
+ } => {
+ let install_dir_path = Path::new(&install_dir);
+ if !install_dir_path.exists() {
+ missing_games.push(game_id);
+ }
+ }
+ database::db::GameDownloadStatus::Installed {
+ version_name: _,
+ install_dir,
+ } => {
+ let install_dir_path = Path::new(&install_dir);
+ if !install_dir_path.exists() {
+ missing_games.push(game_id);
+ }
+ }
+ }
+ }
+
+ info!("detected games missing: {:?}", missing_games);
+
+ let mut db_handle = borrow_db_mut_checked();
+ for game_id in missing_games {
+ db_handle
+ .applications
+ .game_statuses
+ .entry(game_id)
+ .and_modify(|v| *v = GameDownloadStatus::Remote {});
+ }
+
+ drop(db_handle);
+
+ debug!("finished setup!");
+
+ // Sync autostart state
+ if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
+ warn!("failed to sync autostart state: {}", e);
+ }
- let (app_status, user) = auth::setup().unwrap();
AppState {
status: app_status,
user,
@@ -137,12 +205,13 @@ fn setup(handle: AppHandle) -> AppState {
}
}
-
pub static DB: LazyLock = LazyLock::new(DatabaseInterface::set_up_database);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
- let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init());
+ let mut builder = tauri::Builder::default()
+ .plugin(tauri_plugin_os::init())
+ .plugin(tauri_plugin_dialog::init());
#[cfg(desktop)]
#[allow(unused_variables)]
@@ -152,15 +221,21 @@ pub fn run() {
}));
}
- let mut app = builder
+ let app = builder
.plugin(tauri_plugin_deep_link::init())
.invoke_handler(tauri::generate_handler![
// Core utils
fetch_state,
quit,
+ fetch_system_data,
+ // User utils
+ update_settings,
+ fetch_settings,
// Auth
auth_initiate,
retry_connect,
+ manual_recieve_handshake,
+ sign_out,
// Remote
use_remote,
gen_drop_url,
@@ -174,26 +249,34 @@ pub fn run() {
fetch_game_verion_options,
// Downloads
download_game,
- move_game_in_queue,
- pause_game_downloads,
- resume_game_downloads,
+ move_download_in_queue,
+ pause_downloads,
+ resume_downloads,
cancel_game,
+ uninstall_game,
// Processes
launch_game,
+ kill_game,
+ toggle_autostart,
+ get_autostart_enabled,
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_autostart::init(
+ tauri_plugin_autostart::MacosLauncher::LaunchAgent,
+ Some(vec!["--minimize"]),
+ ))
.setup(|app| {
let handle = app.handle().clone();
let state = setup(handle);
- info!("initialized drop client");
+ debug!("initialized drop client");
app.manage(Mutex::new(state));
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
- info!("registered all pre-defined deep links");
+ debug!("registered all pre-defined deep links");
}
let handle = app.handle().clone();
@@ -213,7 +296,7 @@ pub fn run() {
.unwrap();
app.deep_link().on_open_url(move |event| {
- info!("handling drop:// url");
+ debug!("handling drop:// url");
let binding = event.urls();
let url = binding.first().unwrap();
if url.host_str().unwrap() == "handshake" {
@@ -243,37 +326,59 @@ pub fn run() {
app.webview_windows().get("main").unwrap().show().unwrap();
}
"quit" => {
- cleanup_and_exit(app);
+ cleanup_and_exit(app, &app.state());
}
_ => {
- println!("Menu event not handled: {:?}", event.id);
+ println!("menu event not handled: {:?}", event.id);
}
})
.build(app)
.expect("error while setting up tray menu");
+ {
+ let mut db_handle = borrow_db_mut_checked();
+ if let Some(original) = db_handle.prev_database.take() {
+ warn!(
+ "Database corrupted. Original file at {}",
+ original
+ .canonicalize()
+ .unwrap()
+ .to_string_lossy()
+ .to_string()
+ );
+ app.dialog()
+ .message(
+ "Database corrupted. A copy has been saved at: ".to_string()
+ + original.to_str().unwrap(),
+ )
+ .title("Database corrupted")
+ .show(|_| {});
+ }
+ }
+
Ok(())
})
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
- let base_url = DB.fetch_base_url();
-
// Drop leading /
let object_id = &request.uri().path()[1..];
- let object_url = base_url
- .join("/api/v1/client/object/")
- .unwrap()
- .join(object_id)
- .unwrap();
-
let header = generate_authorization_header();
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
- let response = client
- .get(object_url.to_string())
- .header("Authorization", header)
- .send()
- .unwrap();
+ let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
+ f.header("Authorization", header)
+ })
+ .unwrap()
+ .send();
+ if response.is_err() {
+ warn!(
+ "failed to fetch object with error: {}",
+ response.err().unwrap()
+ );
+ responder.respond(Response::builder().status(500).body(Vec::new()).unwrap());
+ return;
+ }
+ let response = response.unwrap();
let resp_builder = ResponseBuilder::new().header(
CONTENT_TYPE,
@@ -284,22 +389,20 @@ pub fn run() {
responder.respond(resp);
})
- .on_window_event(|window, event| match event {
- WindowEvent::CloseRequested { api, .. } => {
+ .on_window_event(|window, event| {
+ if let WindowEvent::CloseRequested { api, .. } = event {
window.hide().unwrap();
api.prevent_close();
}
- _ => (),
})
.build(tauri::generate_context!())
.expect("error while running tauri application");
- app.run(|app_handle, event| match event {
- RunEvent::ExitRequested { code, api, .. } => {
+ app.run(|_app_handle, event| {
+ if let RunEvent::ExitRequested { code, api, .. } = event {
if code.is_none() {
api.prevent_exit();
}
}
- _ => {}
});
}
diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs
deleted file mode 100644
index 6e8536e..0000000
--- a/src-tauri/src/library.rs
+++ /dev/null
@@ -1,302 +0,0 @@
-use std::sync::Mutex;
-
-use serde::{Deserialize, Serialize};
-use tauri::Emitter;
-use tauri::{AppHandle, Manager};
-use urlencoding::encode;
-
-use crate::db::DatabaseImpls;
-use crate::db::GameVersion;
-use crate::db::{GameStatus, GameTransientStatus};
-use crate::downloads::download_manager::GameDownloadStatus;
-use crate::process::process_manager::Platform;
-use crate::remote::RemoteAccessError;
-use crate::state::{GameStatusManager, GameStatusWithTransient};
-use crate::{auth::generate_authorization_header, AppState, DB};
-
-#[derive(serde::Serialize)]
-pub struct FetchGameStruct {
- game: Game,
- status: GameStatusWithTransient,
-}
-
-#[derive(Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct Game {
- id: String,
- m_name: String,
- m_short_description: String,
- m_description: String,
- // mDevelopers
- // mPublishers
- m_icon_id: String,
- m_banner_id: String,
- m_cover_id: String,
- m_image_library: Vec,
-}
-#[derive(serde::Serialize, Clone)]
-pub struct GameUpdateEvent {
- pub game_id: String,
- pub status: (Option, Option),
-}
-
-#[derive(Serialize, Clone)]
-pub struct QueueUpdateEventQueueData {
- pub id: String,
- pub status: GameDownloadStatus,
- pub progress: f64,
-}
-
-#[derive(serde::Serialize, Clone)]
-pub struct QueueUpdateEvent {
- pub queue: Vec,
-}
-
-// Game version with some fields missing and size information
-#[derive(serde::Deserialize, serde::Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GameVersionOption {
- version_index: usize,
- version_name: String,
- platform: Platform,
- setup_command: String,
- launch_command: String,
- delta: bool,
- umu_id_override: Option,
- // total_size: usize,
-}
-
-fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> {
- let base_url = DB.fetch_base_url();
- let library_url = base_url.join("/api/v1/client/user/library")?;
-
- let header = generate_authorization_header();
-
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(library_url.to_string())
- .header("Authorization", header)
- .send()?;
-
- if response.status() != 200 {
- return Err(response.status().as_u16().into());
- }
-
- let games: Vec = response.json::>()?;
-
- let state = app.state::>();
- let mut handle = state.lock().unwrap();
-
- let mut db_handle = DB.borrow_data_mut().unwrap();
-
- for game in games.iter() {
- handle.games.insert(game.id.clone(), game.clone());
- if !db_handle.games.statuses.contains_key(&game.id) {
- db_handle
- .games
- .statuses
- .insert(game.id.clone(), GameStatus::Remote {});
- }
- }
-
- drop(handle);
-
- Ok(games)
-}
-
-#[tauri::command]
-pub fn fetch_library(app: AppHandle) -> Result, String> {
- fetch_library_logic(app).map_err(|e| e.to_string())
-}
-
-fn fetch_game_logic(
- id: String,
- app: tauri::AppHandle,
-) -> Result {
- let state = app.state::>();
- let mut state_handle = state.lock().unwrap();
-
- let game = state_handle.games.get(&id);
- if let Some(game) = game {
- let status = GameStatusManager::fetch_state(&id);
-
- let data = FetchGameStruct {
- game: game.clone(),
- status,
- };
-
- return Ok(data);
- }
-
- let base_url = DB.fetch_base_url();
-
- let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?;
- let header = generate_authorization_header();
-
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(endpoint.to_string())
- .header("Authorization", header)
- .send()?;
-
- if response.status() == 404 {
- return Err(RemoteAccessError::GameNotFound);
- }
- if response.status() != 200 {
- return Err(RemoteAccessError::InvalidCodeError(
- response.status().into(),
- ));
- }
-
- let game = response.json::()?;
- state_handle.games.insert(id.clone(), game.clone());
-
- let mut db_handle = DB.borrow_data_mut().unwrap();
-
- db_handle
- .games
- .statuses
- .entry(id.clone())
- .or_insert(GameStatus::Remote {});
- drop(db_handle);
-
- let status = GameStatusManager::fetch_state(&id);
-
- let data = FetchGameStruct {
- game: game.clone(),
- status,
- };
-
- Ok(data)
-}
-
-#[tauri::command]
-pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result {
- let result = fetch_game_logic(id, app);
-
- if result.is_err() {
- return Err(result.err().unwrap().to_string());
- }
-
- Ok(result.unwrap())
-}
-
-#[tauri::command]
-pub fn fetch_game_status(id: String) -> Result {
- let status = GameStatusManager::fetch_state(&id);
-
- Ok(status)
-}
-
-fn fetch_game_verion_options_logic<'a>(
- game_id: String,
- state: tauri::State<'_, Mutex>,
-) -> Result, RemoteAccessError> {
- let base_url = DB.fetch_base_url();
-
- let endpoint =
- base_url.join(format!("/api/v1/client/metadata/versions?id={}", game_id).as_str())?;
- let header = generate_authorization_header();
-
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(endpoint.to_string())
- .header("Authorization", header)
- .send()?;
-
- if response.status() != 200 {
- return Err(RemoteAccessError::InvalidCodeError(
- response.status().into(),
- ));
- }
-
- let data = response.json::>()?;
-
- let state_lock = state.lock().unwrap();
- let process_manager_lock = state_lock.process_manager.lock().unwrap();
- let data = data
- .into_iter()
- .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
- .collect::>();
- drop(process_manager_lock);
- drop(state_lock);
-
- Ok(data)
-}
-
-#[tauri::command]
-pub fn fetch_game_verion_options<'a>(
- game_id: String,
- state: tauri::State<'_, Mutex