diff --git a/bun.lock b/bun.lock
index 99a11cd..a71eb9b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -8,6 +8,7 @@
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
@@ -15,7 +16,6 @@
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-form": "^1.25.0",
"@tauri-apps/api": "^2",
- "@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-opener": "^2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -24,6 +24,7 @@
"react-dom": "^19.1.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
+ "ts-pattern": "^5.9.0",
"zod": "^4.1.12",
},
"devDependencies": {
@@ -186,6 +187,8 @@
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+ "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.8", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA=="],
+
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
@@ -330,8 +333,6 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
- "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
-
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -470,6 +471,8 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "ts-pattern": ["ts-pattern@5.9.0", "", {}, "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg=="],
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
@@ -496,6 +499,10 @@
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
+
+ "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
diff --git a/config.html b/config.html
new file mode 100644
index 0000000..05769e3
--- /dev/null
+++ b/config.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Config
+
+
+
+
+
+
diff --git a/index.html b/index.html
index fba1582..f7593a8 100644
--- a/index.html
+++ b/index.html
@@ -4,9 +4,8 @@
- Tauri + React + Typescript
+ Patcher
-
diff --git a/package.json b/package.json
index 5dc60d8..d67d974 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
@@ -21,7 +22,6 @@
"@tailwindcss/vite": "^4.1.17",
"@tanstack/react-form": "^1.25.0",
"@tauri-apps/api": "^2",
- "@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-opener": "^2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -30,6 +30,7 @@
"react-dom": "^19.1.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
+ "ts-pattern": "^5.9.0",
"zod": "^4.1.12"
},
"devDependencies": {
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index bd35481..84cd40f 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -7,13 +7,19 @@ name = "Config"
version = "0.1.0"
dependencies = [
"display-info",
+ "futures-util",
+ "memmap2",
+ "rayon",
+ "reqwest",
"serde",
"serde_json",
"tauri",
"tauri-build",
- "tauri-plugin-notification",
"tauri-plugin-opener",
+ "tokio",
+ "walkdir",
"windows 0.62.2",
+ "xxhash-rust",
]
[[package]]
@@ -505,6 +511,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -528,9 +544,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.10.0",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -541,7 +557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
- "core-foundation",
+ "core-foundation 0.10.1",
"libc",
]
@@ -572,6 +588,25 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -833,6 +868,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
[[package]]
name = "embed-resource"
version = "3.0.6"
@@ -853,6 +894,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "endi"
version = "1.1.0"
@@ -975,6 +1025,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -982,7 +1041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
]
[[package]]
@@ -996,6 +1055,12 @@ dependencies = [
"syn 2.0.110",
]
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@@ -1028,6 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
@@ -1406,6 +1472,25 @@ dependencies = [
"syn 2.0.110",
]
+[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.12.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1504,6 +1589,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
+ "h2",
"http",
"http-body",
"httparse",
@@ -1515,6 +1601,38 @@ dependencies = [
"want",
]
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
[[package]]
name = "hyper-util"
version = "0.1.18"
@@ -1534,9 +1652,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
@@ -1943,18 +2063,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
-[[package]]
-name = "mac-notification-sys"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ee70bb2bba058d58e252d2944582d634fc884fc9c489a966d428dedcf653e97"
-dependencies = [
- "cc",
- "objc2 0.6.3",
- "objc2-foundation 0.3.2",
- "time",
-]
-
[[package]]
name = "markup5ever"
version = "0.14.1"
@@ -2058,6 +2166,23 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
[[package]]
name = "ndk"
version = "0.9.0"
@@ -2113,20 +2238,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
-[[package]]
-name = "notify-rust"
-version = "4.11.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400"
-dependencies = [
- "futures-lite",
- "log",
- "mac-notification-sys",
- "serde",
- "tauri-winrt-notification",
- "zbus",
-]
-
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -2462,6 +2573,50 @@ dependencies = [
"pathdiff",
]
+[[package]]
+name = "openssl"
+version = "0.10.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
+dependencies = [
+ "bitflags 2.10.0",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2912,16 +3067,6 @@ dependencies = [
"rand_core 0.6.4",
]
-[[package]]
-name = "rand"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
-dependencies = [
- "rand_chacha 0.9.0",
- "rand_core 0.9.3",
-]
-
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -2942,16 +3087,6 @@ dependencies = [
"rand_core 0.6.4",
]
-[[package]]
-name = "rand_chacha"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.9.3",
-]
-
[[package]]
name = "rand_core"
version = "0.5.1"
@@ -2970,15 +3105,6 @@ dependencies = [
"getrandom 0.2.16",
]
-[[package]]
-name = "rand_core"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
-dependencies = [
- "getrandom 0.3.4",
-]
-
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -3003,6 +3129,26 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -3080,22 +3226,31 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"base64 0.22.1",
"bytes",
+ "encoding_rs",
+ "futures-channel",
"futures-core",
"futures-util",
+ "h2",
"http",
"http-body",
"http-body-util",
"hyper",
+ "hyper-rustls",
+ "hyper-tls",
"hyper-util",
"js-sys",
"log",
+ "mime",
+ "native-tls",
"percent-encoding",
"pin-project-lite",
+ "rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
+ "tokio-native-tls",
"tokio-util",
"tower",
"tower-http",
@@ -3107,6 +3262,20 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -3129,6 +3298,39 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "rustls"
+version = "0.23.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -3150,6 +3352,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "schannel"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "schemars"
version = "0.8.22"
@@ -3207,6 +3418,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.10.0",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "selectors"
version = "0.24.0"
@@ -3505,7 +3739,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
- "foreign-types",
+ "foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
@@ -3587,6 +3821,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "swift-rs"
version = "1.0.7"
@@ -3640,6 +3880,27 @@ dependencies = [
"syn 2.0.110",
]
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.10.0",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -3661,7 +3922,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch",
@@ -3841,25 +4102,6 @@ dependencies = [
"walkdir",
]
-[[package]]
-name = "tauri-plugin-notification"
-version = "2.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
-dependencies = [
- "log",
- "notify-rust",
- "rand 0.9.2",
- "serde",
- "serde_json",
- "serde_repr",
- "tauri",
- "tauri-plugin",
- "thiserror 2.0.17",
- "time",
- "url",
-]
-
[[package]]
name = "tauri-plugin-opener"
version = "2.5.2"
@@ -3983,18 +4225,6 @@ dependencies = [
"toml 0.9.8",
]
-[[package]]
-name = "tauri-winrt-notification"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
-dependencies = [
- "quick-xml 0.37.5",
- "thiserror 2.0.17",
- "windows 0.61.3",
- "windows-version",
-]
-
[[package]]
name = "tempfile"
version = "3.23.0"
@@ -4109,11 +4339,45 @@ dependencies = [
"bytes",
"libc",
"mio",
+ "parking_lot",
"pin-project-lite",
+ "signal-hook-registry",
"socket2",
+ "tokio-macros",
"windows-sys 0.61.2",
]
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
[[package]]
name = "tokio-util"
version = "0.7.17"
@@ -4403,6 +4667,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.7"
@@ -4451,6 +4721,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
[[package]]
name = "version-compare"
version = "0.2.1"
@@ -4999,6 +5275,17 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -5044,6 +5331,15 @@ dependencies = [
"windows-targets 0.42.2",
]
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -5413,6 +5709,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
[[package]]
name = "yoke"
version = "0.8.1"
@@ -5538,6 +5840,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
[[package]]
name = "zerotrie"
version = "0.2.3"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 04b8830..4f477b1 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -23,7 +23,13 @@ tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
display-info = "0.5.7"
-tauri-plugin-notification = "2"
+xxhash-rust = { version = "0.8.15", features = ["xxh3"] }
+reqwest = { version = "0.12", features = ["json", "stream", "blocking"] }
+tokio = { version = "1", features = ["full"] }
+rayon = "1.10"
+memmap2 = "0.9"
+futures-util = "0.3"
+walkdir = "2.3"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_Graphics_Direct3D9"] }
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 2985263..6ff9e23 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -2,14 +2,11 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
- "windows": [
- "main"
- ],
+ "windows": ["main", "config"],
"permissions": [
"core:default",
"opener:default",
"core:window:default",
- "core:window:allow-start-dragging",
- "notification:default"
+ "core:window:allow-start-dragging"
]
-}
\ No newline at end of file
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 48ca4a6..1dcbc6c 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,8 +1,29 @@
use serde::{Deserialize, Serialize};
-use std::fs;
+use std::{fs, fs::File, sync::Mutex};
use std::path::PathBuf;
-use tauri::Manager;
-use tauri_plugin_notification::NotificationExt;
+use std::collections::HashMap;
+use tokio::io::AsyncWriteExt;
+use futures_util::StreamExt;
+use tauri::{AppHandle, Emitter, Manager};
+use xxhash_rust::xxh3::xxh3_64;
+use rayon::prelude::*;
+use memmap2::Mmap;
+use walkdir::WalkDir;
+
+pub const DIR: &str = if cfg!(debug_assertions) {
+ if cfg!(target_os = "windows") {
+ "C:\\Users\\Ego\\Desktop\\test\\"
+ } else {
+ "/home/ego/Documents/metin2/client/dist/"
+ }
+} else {
+ "./"
+};
+
+// TODO HANDLE ERRORS PROPERLY
+// TODO SUPPORT REGEX
+// TODO SUPPORT ALL FILE MOVES
+pub const SERVER: &str = "http://192.168.31.60:8080";
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
@@ -22,19 +43,8 @@ pub struct Config {
pub fog_mode_on: bool,
pub camera_mode: CameraMode,
pub private_shop: PrivateShop,
- // pub object_culling: bool,
- // pub visibility: i32,
- // pub software_cursor: bool,
- // pub is_save_id: i32,
- // pub save_id: i32,
- // pub preloading_delay_time: i32,
- // pub decompressed_texture: bool,
- // pub use_default_ime: bool,
- // pub software_tiling: i32,
}
-// ------------------ Enums ------------------
-
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Locale {
@@ -137,7 +147,7 @@ impl Default for Config {
}
}
-fn get_config_path(_app: &tauri::AppHandle) -> PathBuf {
+fn get_config_path(_app: &AppHandle) -> PathBuf {
std::env::current_exe()
.expect("Failed to get executable path")
.parent()
@@ -147,7 +157,7 @@ fn get_config_path(_app: &tauri::AppHandle) -> PathBuf {
}
#[tauri::command]
-fn get_config(app: tauri::AppHandle) -> Config {
+fn get_config(app: AppHandle) -> Config {
let config_path = get_config_path(&app);
match fs::read_to_string(&config_path) {
@@ -160,39 +170,39 @@ fn get_config(app: tauri::AppHandle) -> Config {
}
#[tauri::command]
-fn set_config(config: Config, app: tauri::AppHandle) -> bool {
+fn set_config(config: Config, app: AppHandle) -> bool {
let config_path = get_config_path(&app);
match serde_json::to_string_pretty(&config) {
Ok(json) => match fs::write(&config_path, json) {
Ok(_) => {
- app.notification()
- .builder()
- .title("Configuration")
- .body("Saved Metin2 client settings")
- .show()
- .unwrap();
+ // app.notification()
+ // .builder()
+ // .title("Configuration")
+ // .body("Saved Metin2 client settings")
+ // .show()
+ // .unwrap();
true
}
- Err(e) => {
- app.notification()
- .builder()
- .title("Configuration")
- .body(&format!("Could not save config: {}", e))
- .show()
- .unwrap();
+ Err(_e) => {
+ // app.notification()
+ // .builder()
+ // .title("Configuration")
+ // .body(&format!("Could not save config: {}", e))
+ // .show()
+ // .unwrap();
false
}
},
- Err(e) => {
- app.notification()
- .builder()
- .title("Configuration")
- .body(&format!("Could not serialize config: {}", e))
- .show()
- .unwrap();
+ Err(_e) => {
+ // app.notification()
+ // .builder()
+ // .title("Configuration")
+ // .body(&format!("Could not serialize config: {}", e))
+ // .show()
+ // .unwrap();
false
}
@@ -289,20 +299,297 @@ fn get_resolutions() -> Vec {
}
#[tauri::command]
-fn close(app: tauri::AppHandle) {
+fn close(app: AppHandle) {
app.exit(0);
}
+pub fn pack_hash(app: AppHandle) -> std::io::Result> {
+ let entries: Vec = WalkDir::new(DIR)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.path().is_file())
+ .map(|e| e.path().to_path_buf())
+ .collect();
+
+ let results = Mutex::new(Vec::with_capacity(entries.len()));
+
+ entries.par_iter().try_for_each(|path| -> std::io::Result<()> {
+ // Relative path to DIR
+ let rel_path = path.strip_prefix(DIR)
+ .unwrap()
+ .to_string_lossy()
+ .replace("\\", "/"); // normalize
+
+ app.emit("file", &rel_path)
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
+
+ let file = File::open(path)?;
+ let mmap = unsafe { Mmap::map(&file)? };
+ let hash = xxh3_64(&mmap);
+
+ results.lock().unwrap().push((rel_path, hash));
+
+ Ok(())
+ })?;
+
+ Ok(results.into_inner().unwrap())
+}
+
+#[derive(Debug, Deserialize)]
+struct ChecksumResponse {
+ checksums: HashMap,
+}
+
+async fn fetch_server_checksums() -> Result, String> {
+ let response = reqwest::get(format!("{}/checksum", SERVER))
+ .await
+ .map_err(|e| format!("Failed to fetch checksums: {}", e))?;
+
+ let parsed: ChecksumResponse = response
+ .json()
+ .await
+ .map_err(|e| format!("Failed to parse checksums: {}", e))?;
+
+ let mut out = HashMap::new();
+
+ for (name, hash_str) in parsed.checksums {
+ let hash = hash_str
+ .parse::()
+ .map_err(|_| format!("Invalid checksum number for file: {}", name))?;
+
+ // Normalize separators
+ let normalized_name = name.replace("\\", "/");
+ out.insert(normalized_name, hash);
+ }
+
+ Ok(out)
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(into = "u8", try_from = "u8")]
+pub enum Status {
+ Awaiting = 0,
+ Fetching = 1,
+ Checksumming = 2,
+ Verifying = 3,
+ Updating = 4,
+ Ready = 5,
+}
+
+impl From for u8 {
+ fn from(status: Status) -> Self {
+ status as u8
+ }
+}
+
+impl TryFrom for Status {
+ type Error = String;
+
+ fn try_from(value: u8) -> Result {
+ match value {
+ 0 => Ok(Status::Awaiting),
+ 1 => Ok(Status::Fetching),
+ 2 => Ok(Status::Checksumming),
+ 3 => Ok(Status::Verifying),
+ 4 => Ok(Status::Updating),
+ 5 => Ok(Status::Ready),
+ _ => Err(format!("Invalid status value: {}", value)),
+ }
+ }
+}
+
+async fn download_file(app_handle: &AppHandle, url: &str, dest_path: &str, file_name: &str) -> Result<(), String> {
+ if tokio::fs::metadata(dest_path).await.is_ok() {
+ let _ = tokio::fs::remove_file(dest_path).await;
+ }
+
+ let client = reqwest::Client::new();
+ let response = client.get(url)
+ .send()
+ .await
+ .map_err(|e| format!("Failed to download {}: {}", file_name, e))?;
+
+ let total_size = response
+ .content_length()
+ .ok_or_else(|| format!("Failed to get content length for {}", file_name))?;
+
+ let mut stream = response.bytes_stream();
+ let mut file = tokio::fs::File::create(dest_path)
+ .await
+ .map_err(|e| format!("Failed to create file {}: {}", dest_path, e))?;
+
+ let mut downloaded: u64 = 0;
+
+ while let Some(chunk) = stream.next().await {
+ let chunk = chunk
+ .map_err(|e| format!("Error while downloading {}: {}", file_name, e))?;
+
+ file.write_all(&chunk)
+ .await
+ .map_err(|e| format!("Failed to write {}: {}", dest_path, e))?;
+
+ downloaded += chunk.len() as u64;
+
+ // Emit progress safely
+ if let Err(e) = app_handle.emit("download", serde_json::json!({
+ "file": file_name,
+ "downloaded": downloaded,
+ "total": total_size,
+ "percent": (downloaded as f64 / total_size as f64 * 100.0) as u8
+ })) {
+ eprintln!("Failed to emit download progress: {}", e);
+ }
+ }
+
+ Ok(())
+}
+
+
+pub struct WindowState {
+ config_open: Mutex,
+}
+
+impl Default for WindowState {
+ fn default() -> Self {
+ Self {
+ config_open: Mutex::new(false),
+ }
+ }
+}
+
+#[tauri::command]
+fn open_config(app: AppHandle) -> Result<(), String> {
+ let state = app.state::();
+ let mut is_open = state.config_open.lock().unwrap();
+
+ if *is_open {
+ if let Some(window) = app.get_webview_window("config") {
+ let _ = window.close();
+ return Ok(());
+ } else {
+ *is_open = false;
+ }
+ }
+
+ *is_open = true;
+
+ let window = tauri::WebviewWindowBuilder::new(
+ &app,
+ "config",
+ tauri::WebviewUrl::App("config.html".into())
+ )
+ .title("Configuration")
+ .inner_size(800.0, 600.0)
+ .resizable(false)
+ .decorations(false)
+ .visible(true)
+ .build()
+ .map_err(|e| format!("Failed to create config window: {}", e))?;
+
+ let app_handle = app.clone();
+ window.on_window_event(move |event| {
+ if let tauri::WindowEvent::Destroyed = event {
+ let state = app_handle.state::();
+ *state.config_open.lock().unwrap() = false;
+ }
+ });
+
+ Ok(())
+}
+
pub fn run() {
tauri::Builder::default()
- .plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
+ .manage(WindowState::default())
.invoke_handler(tauri::generate_handler![
+ open_config,
get_resolutions,
get_config,
set_config,
close,
])
+ .setup(|app| {
+ let app_handle = app.handle().clone();
+
+ let _ = fs::create_dir_all(DIR);
+
+ tauri::async_runtime::spawn(async move {
+ let _ = app_handle.emit("status", Status::Fetching).unwrap();
+
+ let server_hashes = match fetch_server_checksums().await {
+ Ok(hashes) => hashes,
+ Err(e) => {
+ let _ = app_handle.emit("error", e.to_string()).unwrap();
+ return;
+ }
+ };
+
+ let _ = app_handle.emit("status", Status::Checksumming).unwrap();
+
+ let local_hashes = match tokio::task::spawn_blocking({
+ let app_handle = app_handle.clone();
+ move || pack_hash(app_handle)
+ })
+ .await
+ {
+ Ok(Ok(hashes)) => hashes,
+ Ok(Err(e)) => {
+ let _ = app_handle.emit("error", e.to_string()).unwrap();
+ return;
+ }
+ Err(e) => {
+ let _ = app_handle.emit("error", e.to_string()).unwrap();
+ return;
+ }
+ };
+ let local_map: HashMap = local_hashes.into_iter().collect();
+ let _ = app_handle.emit("status", Status::Verifying).unwrap();
+
+ let mut updatable: HashMap = HashMap::new();
+
+ for (filename, server_hash) in &server_hashes {
+ match local_map.get(filename) {
+ Some(local_hash) => {
+ if local_hash != server_hash {
+ updatable.insert(filename.clone(), *server_hash);
+ }
+ }
+ None => {
+ updatable.insert(filename.clone(), *server_hash);
+ }
+ }
+ }
+
+ if !updatable.is_empty() {
+ let _ = app_handle.emit("status", Status::Updating).unwrap();
+
+ for (file, _hash) in &updatable {
+ let url = format!("{}/{}", SERVER, file);
+
+ let mut dest_path = PathBuf::from(DIR);
+ dest_path.push(file);
+
+ if let Some(parent) = dest_path.parent() {
+ if let Err(e) = fs::create_dir_all(parent) {
+ let _ = app_handle.emit("error", format!("Failed to create dir: {}", e.to_string()));
+ continue;
+ }
+ }
+
+ match download_file(&app_handle, &url, dest_path.to_str().unwrap(), file).await {
+ Ok(_) => {},
+ Err(e) => {
+ let _ = app_handle.emit("error", e.to_string());
+ }
+ }
+ }
+ }
+
+ let _ = app_handle.emit("status", Status::Ready).unwrap();
+ });
+
+ Ok(())
+ })
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 48c355b..03c37b2 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -14,9 +14,9 @@
"app": {
"windows": [
{
- "title": "Configuration",
- "width": 800,
- "height": 600,
+ "title": "Patcher",
+ "width": 600,
+ "height": 300,
"resizable": false,
"decorations": false
}
diff --git a/src/assets/background.jpg b/src/assets/background.jpg
new file mode 100644
index 0000000..927d1fb
Binary files /dev/null and b/src/assets/background.jpg differ
diff --git a/src/components/drag-bar.tsx b/src/components/drag-bar.tsx
new file mode 100644
index 0000000..b0e983d
--- /dev/null
+++ b/src/components/drag-bar.tsx
@@ -0,0 +1,40 @@
+import { invoke } from "@tauri-apps/api/core";
+import { CogIcon, XIcon } from "lucide-react";
+import { useCallback } from "react";
+
+export default function DragBar({ isMain }: { isMain?: boolean }) {
+ const close = useCallback(async () => {
+ if (isMain) {
+ await invoke("close");
+ return;
+ }
+
+ await invoke("open_config");
+ }, [isMain]);
+
+ const config = useCallback(async () => {
+ await invoke("open_config");
+ }, []);
+
+ return (
+
+
+ {isMain && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
new file mode 100644
index 0000000..10af7e6
--- /dev/null
+++ b/src/components/ui/progress.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Progress }
diff --git a/src/config.tsx b/src/config.tsx
new file mode 100644
index 0000000..87f6c06
--- /dev/null
+++ b/src/config.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./pages/config";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+
+
+
+);
diff --git a/src/index.css b/src/index.css
index f4c1e9b..91ebb33 100644
--- a/src/index.css
+++ b/src/index.css
@@ -3,6 +3,102 @@
@custom-variant dark (&:is(.dark *));
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(1 0 0);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(87% 0 0);
+ --chart-2: oklch(70.8% 0 0);
+ --chart-3: oklch(55.6% 0 0);
+ --chart-4: oklch(43.9% 0 0);
+ --chart-5: oklch(37.1% 0 0);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+ --header: oklch(1 0 0);
+ --header-foreground: oklch(0.145 0 0);
+ --footer: oklch(1 0 0);
+ --footer-foreground: oklch(0.145 0 0);
+ --code: oklch(1 0 0);
+ --code-foreground: oklch(0.708 0 0);
+ --code-highlight: oklch(0.27 0 0);
+ --code-number: oklch(0.72 0 0);
+ --code-selection: oklch(0.922 0 0);
+ --code-border: oklch(0.922 0 0);
+ --radius: 0rem;
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.269 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.722 0.09 222.3);
+ --primary-foreground: oklch(0.1134 0 0);
+ --secondary: oklch(0.722 0.09 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.371 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --destructive-foreground: oklch(0.985 0 0);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(87% 0 0);
+ --chart-2: oklch(70.8% 0 0);
+ --chart-3: oklch(55.6% 0 0);
+ --chart-4: oklch(43.9% 0 0);
+ --chart-5: oklch(37.1% 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.439 0 0);
+ --header: oklch(0.145 0 0);
+ --header-foreground: oklch(0.985 0 0);
+ --footer: oklch(0.145 0 0);
+ --footer-foreground: oklch(0.985 0 0);
+ --code: oklch(0.2 0 0);
+ --code-foreground: oklch(0.708 0 0);
+ --code-highlight: oklch(0.27 0 0);
+ --code-number: oklch(0.72 0 0);
+ --code-selection: oklch(0.922 0 0);
+ --code-border: oklch(1 0 0 / 10%);
+ --radius: 0rem;
+}
+
+.container {
+ @apply mx-auto;
+}
+
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
@@ -41,75 +137,6 @@
--color-sidebar-ring: var(--sidebar-ring);
}
-:root {
- --radius: 0.625rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
-}
-
-.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
-}
-
@layer base {
* {
@apply border-border outline-ring/50;
diff --git a/src/main.tsx b/src/main.tsx
index 02055fd..9d47849 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
-import App from "./App";
+import App from "./pages/patcher";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
diff --git a/src/App.tsx b/src/pages/config.tsx
similarity index 95%
rename from src/App.tsx
rename to src/pages/config.tsx
index 86355c3..e1e731d 100644
--- a/src/App.tsx
+++ b/src/pages/config.tsx
@@ -2,7 +2,7 @@ import { z } from "zod";
import { PropsWithChildren, useCallback, useEffect, useState } from "react";
import { useForm } from "@tanstack/react-form";
import { invoke } from "@tauri-apps/api/core";
-import { CornerUpLeftIcon, Loader2Icon, SaveIcon, XIcon } from "lucide-react";
+import { CornerUpLeftIcon, Loader2Icon, SaveIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Field,
@@ -32,6 +32,7 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
+import DragBar from "@/components/drag-bar";
const config = z.object({
$schema: z.string(),
@@ -73,16 +74,6 @@ const config = z.object({
view_distance: z.number().min(0).max(100),
show_sales_text: z.boolean(),
}),
-
- // object_culling: z.boolean(), // Should be 0
- // visibility: z.number().int(), // Should be 3
- // software_cursor: z.boolean(), // Always false
- // is_save_id: z.number().int(),
- // save_id: z.number().int(),
- // preloading_delay_time: z.number().int(), // Default seems to be 20
- // decompressed_texture: z.boolean(), // Should always be true
- // use_default_ime: z.boolean(), // Should be false
- // software_tiling: z.number().int(), // Should always be 0 for auto tiling
});
type Config = z.infer;
@@ -98,7 +89,7 @@ const Category = ({
return (
@@ -112,6 +103,7 @@ const Category = ({
);
};
+
const Settings = ({
resolutions,
initialConfig,
@@ -156,7 +148,12 @@ const Settings = ({
}}
className="flex flex-col h-screen"
>
-
+
-
-
Sound
Volume settings
@@ -511,7 +506,6 @@ const Settings = ({
}}
/>
-
{
- await invoke("close");
- }, []);
-
useEffect(() => {
loadResolutions();
}, []);
return (
-
+
{config ? (
) : (
diff --git a/src/pages/patcher.tsx b/src/pages/patcher.tsx
new file mode 100644
index 0000000..b06744c
--- /dev/null
+++ b/src/pages/patcher.tsx
@@ -0,0 +1,176 @@
+import { P, match } from "ts-pattern";
+import {
+ createContext,
+ PropsWithChildren,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import DragBar from "@/components/drag-bar";
+import { Progress } from "@/components/ui/progress";
+import { listen } from "@tauri-apps/api/event";
+import Background from "@/assets/background.jpg";
+
+const prettyBytes = (num: number, precision = 3, addSpace = true) => {
+ const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+ if (Math.abs(num) < 1) return num + (addSpace ? " " : "") + UNITS[0];
+ const exponent = Math.min(
+ Math.floor(Math.log10(num < 0 ? -num : num) / 3),
+ UNITS.length - 1
+ );
+ const n = Number(
+ ((num < 0 ? -num : num) / 1000 ** exponent).toPrecision(precision)
+ );
+ return (num < 0 ? "-" : "") + n + (addSpace ? " " : "") + UNITS[exponent];
+};
+
+const PatcherProgress = () => {
+ const state = useStatus();
+
+ return (
+
+ {match(state)
+ .with({ error: P.not(P.nullish) }, ({ error }) => (
+
{JSON.stringify(error)}
+ ))
+ .with({ status: Status.Awaiting }, () => (
+
Starting patcher...
+ ))
+ .with({ status: Status.Fetching }, () => (
+
Fetching checksums...
+ ))
+ .with({ status: Status.Checksumming }, () => (
+
Checking local files...
+ ))
+ .with({ status: Status.Verifying }, () => (
+
Verifying files...
+ ))
+ .with({ status: Status.Updating }, ({ download }) => (
+
+
Updating files...
+ {download && (
+
+
+
+ {download.file}: {prettyBytes(download.downloaded)}/
+ {prettyBytes(download.total)}
+
+
+ )}
+
+ ))
+ .otherwise(() => (
+
Ready!
+ ))}
+
+ );
+};
+
+enum Status {
+ Awaiting,
+ Fetching,
+ Checksumming,
+ Verifying,
+ Updating,
+ Ready,
+}
+
+type Download = {
+ file: string;
+ downloaded: number;
+ total: number;
+ percent: number;
+};
+
+const stateContext = createContext<{
+ status: Status;
+ error: string | null;
+ download: Download | null;
+}>({
+ status: Status.Awaiting,
+ error: null,
+ download: null,
+});
+
+const StateProvider = ({ children }: PropsWithChildren) => {
+ const [status, setStatus] = useState(Status.Awaiting);
+ const [error, setError] = useState(null);
+ const [download, setDownload] = useState(null);
+
+ useEffect(() => {
+ const setupListeners = async () => {
+ const statusUnlisten = await listen("status", (event) => {
+ setStatus(event.payload);
+ });
+ const errorUnlisten = await listen("error", (event) => {
+ setError(event.payload);
+ });
+ const downloadUnlisten = await listen("download", (event) => {
+ setDownload(event.payload);
+ });
+
+ return () => {
+ statusUnlisten();
+ errorUnlisten();
+ downloadUnlisten();
+ };
+ };
+
+ let cleanup: () => void;
+ setupListeners().then((cleanupFn) => {
+ cleanup = cleanupFn;
+ });
+
+ return () => {
+ if (cleanup) cleanup();
+ };
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useStatus = () => useContext(stateContext);
+
+const Patcher = () => {
+ const { status, error } = useStatus();
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+function App() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default App;