add: Initial support for patching.

master
WildEgo 2025-11-24 00:39:27 +00:00
parent b1994c0c40
commit d0241a48b5
17 changed files with 1129 additions and 248 deletions

View File

@ -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=="],

13
config.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Config</title>
</head>
<body class="dark select-none">
<div id="root"></div>
<script type="module" src="/src/config.tsx"></script>
</body>
</html>

View File

@ -4,9 +4,8 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>
<title>Patcher</title>
</head>
<body class="dark select-none">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>

View File

@ -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": {

494
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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"
]
}
}

View File

@ -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<Resolution> {
}
#[tauri::command]
fn close(app: tauri::AppHandle) {
fn close(app: AppHandle) {
app.exit(0);
}
pub fn pack_hash(app: AppHandle) -> std::io::Result<Vec<(String, u64)>> {
let entries: Vec<PathBuf> = 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<String, String>,
}
async fn fetch_server_checksums() -> Result<HashMap<String, u64>, 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::<u64>()
.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<Status> for u8 {
fn from(status: Status) -> Self {
status as u8
}
}
impl TryFrom<u8> for Status {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<bool>,
}
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::<WindowState>();
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::<WindowState>();
*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<String, u64> = local_hashes.into_iter().collect();
let _ = app_handle.emit("status", Status::Verifying).unwrap();
let mut updatable: HashMap<String, u64> = 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");
}

View File

@ -14,9 +14,9 @@
"app": {
"windows": [
{
"title": "Configuration",
"width": 800,
"height": 600,
"title": "Patcher",
"width": 600,
"height": 300,
"resizable": false,
"decorations": false
}

BIN
src/assets/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -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 (
<div className="w-full grid grid-cols-[1fr_auto_auto] fixed top-0 inset-x-0 z-50 gap-px">
<div data-tauri-drag-region />
{isMain && (
<button
title="Config"
onClick={config}
className="bg-muted/20 hover:bg-gray-400/40 transition-colors py-1.5 px-4 shrink-0"
>
<CogIcon className="size-5" />
</button>
)}
<button
title="Close"
onClick={close}
className="bg-muted/20 hover:bg-red-800/40 transition-colors py-1.5 px-4 shrink-0"
>
<XIcon className="size-5" />
</button>
</div>
);
}

View File

@ -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<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

10
src/config.tsx Normal file
View File

@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -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;

View File

@ -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(

View File

@ -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<typeof config>;
@ -98,7 +89,7 @@ const Category = ({
return (
<AccordionItem
value={id}
className="bg-linear-to-t from-muted/10 to-muted/0 py-1"
className="bg-linear-to-tr from-primary/5 to-primary/0 py-1"
>
<AccordionTrigger className="hover:no-underline! px-6 py-4 rounded-none! items-center">
<div>
@ -112,6 +103,7 @@ const Category = ({
</AccordionItem>
);
};
const Settings = ({
resolutions,
initialConfig,
@ -156,7 +148,12 @@ const Settings = ({
}}
className="flex flex-col h-screen"
>
<Accordion type="single" collapsible defaultValue="general">
<Accordion
type="single"
collapsible
defaultValue="general"
className="divide-primary/10 divide-y"
>
<Category id="general" title="General" subtitle="Basic settings">
<form.Field
name="locale"
@ -250,7 +247,6 @@ const Settings = ({
}}
/>
</Category>
<Separator />
<Category id="display" title="Display" subtitle="Screen settings">
<div className="flex items-center gap-3">
<Checkbox
@ -450,7 +446,6 @@ const Settings = ({
}}
/>
</Category>
<Separator />
<Category id="sound" title="Sound" subtitle="Volume settings">
<FieldLegend>Sound</FieldLegend>
<FieldDescription>Volume settings</FieldDescription>
@ -511,7 +506,6 @@ const Settings = ({
}}
/>
</Category>
<Separator />
<Category
id="graphics"
title="Graphics"
@ -750,26 +744,13 @@ function App() {
setConfig(baseConfig);
}, []);
const close = useCallback(async () => {
await invoke("close");
}, []);
useEffect(() => {
loadResolutions();
}, []);
return (
<main>
<div className="w-full grid grid-cols-[auto_max-content] fixed top-0 inset-x-0 z-10">
<div data-tauri-drag-region />
<button
title="Close"
onClick={close}
className="bg-muted/20 hover:bg-muted transition-colors py-0.5 px-3"
>
<XIcon />
</button>
</div>
<DragBar />
{config ? (
<Settings resolutions={resolutions} initialConfig={config} />
) : (

176
src/pages/patcher.tsx Normal file
View File

@ -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 (
<div className="px-8 py-4 text-sm self-center">
{match(state)
.with({ error: P.not(P.nullish) }, ({ error }) => (
<pre>{JSON.stringify(error)}</pre>
))
.with({ status: Status.Awaiting }, () => (
<div className="animate-pulse">Starting patcher...</div>
))
.with({ status: Status.Fetching }, () => (
<div className="animate-pulse">Fetching checksums...</div>
))
.with({ status: Status.Checksumming }, () => (
<div className="animate-pulse">Checking local files...</div>
))
.with({ status: Status.Verifying }, () => (
<div className="animate-pulse">Verifying files...</div>
))
.with({ status: Status.Updating }, ({ download }) => (
<div>
<div className="mb-2 animate-pulse">Updating files...</div>
{download && (
<div className="grid gap-1.5">
<Progress value={download.percent} />
<span
className="text-xs opacity-75
"
>
{download.file}: {prettyBytes(download.downloaded)}/
{prettyBytes(download.total)}
</span>
</div>
)}
</div>
))
.otherwise(() => (
<div className="text-primary brightness-150">Ready!</div>
))}
</div>
);
};
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>(Status.Awaiting);
const [error, setError] = useState<string | null>(null);
const [download, setDownload] = useState<Download | null>(null);
useEffect(() => {
const setupListeners = async () => {
const statusUnlisten = await listen<Status>("status", (event) => {
setStatus(event.payload);
});
const errorUnlisten = await listen<string>("error", (event) => {
setError(event.payload);
});
const downloadUnlisten = await listen<Download>("download", (event) => {
setDownload(event.payload);
});
return () => {
statusUnlisten();
errorUnlisten();
downloadUnlisten();
};
};
let cleanup: () => void;
setupListeners().then((cleanupFn) => {
cleanup = cleanupFn;
});
return () => {
if (cleanup) cleanup();
};
}, []);
return (
<stateContext.Provider value={{ status, error, download }}>
{children}
</stateContext.Provider>
);
};
const useStatus = () => useContext(stateContext);
const Patcher = () => {
const { status, error } = useStatus();
return (
<>
<img
src={Background}
alt="Background"
className="absolute inset-0 size-full object-center object-cover"
/>
<div className="grid grid-cols-[1fr_auto] items-stretch border-t border-primary/10 absolute bottom-0 inset-x-0 z-10 bg-background/90 backdrop-blur-2xl">
<PatcherProgress />
<button
type="button"
className="px-12 py-8 bg-linear-to-tr from-primary/80 to-primary/50 font-semibold text-sm hover:brightness-125 transition-all disabled:grayscale-75 disabled:opacity-50"
disabled={status !== Status.Ready || !!error}
>
Start now
</button>
</div>
</>
);
};
function App() {
return (
<StateProvider>
<main className="w-screen h-screen relative overflow-hidden">
<DragBar isMain />
<Patcher />
</main>
</StateProvider>
);
}
export default App;