Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde145f071
|
||
|
|
846fcc0af7
|
||
|
|
4601628a55
|
||
|
|
3a9e0b0358
|
||
|
|
c45e7fb47c
|
||
|
|
c7086fbc31
|
||
|
|
33a1e4e780
|
||
|
|
066f4aae8c
|
||
|
|
2ebe127def
|
||
|
|
e0300acb6d
|
||
|
|
d18dc5afb7
|
||
|
|
d20c99651c
|
||
|
|
86a5a7b744
|
||
|
|
18f8c2316f
|
||
|
|
12914e42dd
|
||
|
|
91ff7caaed
|
||
|
|
fad7073607
|
||
|
|
beb5ce175e
|
||
|
|
b8498d36e4
|
||
|
|
141d17fea0
|
||
|
|
138275d752
|
||
|
|
1f035882f7
|
||
|
|
68cd3c891d
|
||
|
|
65bd6cfe82
|
||
|
|
d91314112d
|
||
|
|
da27bcb32c
|
||
|
|
6c8dfd1200
|
||
|
|
e09d125980
|
||
|
|
35dd842576
|
||
|
|
55714c8edb
|
||
|
|
04d1f2822b
|
||
|
|
dfd99ab12e
|
||
|
|
837c4a8863
|
||
|
|
c69060a360
|
||
|
|
f2b0358605
|
||
|
|
57441a8872
|
||
|
|
df522122ac
|
||
|
|
f4e16c12e5
|
||
|
|
e2d98307c0
|
||
|
|
387bd54ce2
|
||
|
|
e7a61872b7
|
||
|
|
ac66c78dbf
|
||
|
|
d9302319fa
|
||
|
|
5206a7f25d
|
||
|
|
3ee371998a
|
||
|
|
5733c21fbb
|
||
|
|
61b9d07fc6
|
||
|
|
59cd625238
|
||
|
|
6bf2733cec
|
||
|
|
60f9f95be4
|
||
|
|
ea1c05d2d9
|
||
|
|
49c03e82b1
|
||
|
|
0c52169533
|
||
|
|
bf6da20fd8
|
||
|
|
c1f87580c7
|
||
|
|
dd7d2b96e3
|
||
|
|
874f0627f7
|
||
|
|
e8dec1a1b2
|
||
|
|
e1f0ce1290
|
||
|
|
bc5ea81170
|
||
|
|
71b998f4bc
|
||
|
|
cbac50753f
|
||
|
|
fb65348ba3
|
||
|
|
f23d1025d2
|
||
|
|
a8573c4b6a
|
||
|
|
4054fdd06b
|
||
|
|
0b4e639630
|
||
|
|
c65fb5e1a5
|
||
|
|
6486498c74
|
||
|
|
cf72d143f3
|
||
|
|
ee7c50c6f5
|
||
|
|
f7317c8232
|
||
|
|
9856cf2426
|
No files matched your search
@@ -8,34 +8,37 @@
|
||||
"@fortawesome/fontawesome-svg-core": "7.2.0",
|
||||
"@fortawesome/free-brands-svg-icons": "7.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
||||
"@fortawesome/react-fontawesome": "3.2.0",
|
||||
"@fortawesome/react-fontawesome": "3.3.0",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "2.6.0",
|
||||
"@tauri-apps/plugin-fs": "2.4.5",
|
||||
"@tauri-apps/plugin-http": "2.5.7",
|
||||
"@tauri-apps/plugin-dialog": "2.7.0",
|
||||
"@tauri-apps/plugin-fs": "2.5.0",
|
||||
"@tauri-apps/plugin-http": "2.5.8",
|
||||
"@tauri-apps/plugin-notification": "2.3.3",
|
||||
"@tauri-apps/plugin-opener": "2.5.3",
|
||||
"@tauri-apps/plugin-os": "2.3.2",
|
||||
"@tauri-apps/plugin-store": "2.4.2",
|
||||
"@tauri-apps/plugin-updater": "2.10.1",
|
||||
"@tauri-apps/plugin-window-state": "2.4.1",
|
||||
"next": "16.1.6",
|
||||
"dompurify": "3.3.3",
|
||||
"next": "16.2.3",
|
||||
"pretty-bytes": "7.1.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@tailwindcss/postcss": "4.2.2",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"crypto-js": "4.2.0",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "4.2.1",
|
||||
"typescript": "5.9.3",
|
||||
"eslint-config-next": "16.2.3",
|
||||
"tailwindcss": "4.2.2",
|
||||
"typescript": "6.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -64,9 +67,9 @@
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
@@ -74,11 +77,11 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
@@ -106,7 +109,7 @@
|
||||
|
||||
"@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w=="],
|
||||
|
||||
"@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.2.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw=="],
|
||||
"@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.3.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-EHmHeTf8WgO29sdY3iX/7ekE3gNUdlc2RW6mm/FzELlHFKfTrA9S4MlyquRR+RRCRCn8+jXfLFpLGB2l7wCWyw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
@@ -178,25 +181,25 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
|
||||
"@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
|
||||
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.1.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ=="],
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.3", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
@@ -210,35 +213,35 @@
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
|
||||
|
||||
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
|
||||
|
||||
@@ -268,11 +271,11 @@
|
||||
|
||||
"@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="],
|
||||
|
||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw=="],
|
||||
|
||||
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="],
|
||||
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ=="],
|
||||
|
||||
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="],
|
||||
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.8", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-oxd7oypzQeu8kAfFCrw534Kq7Cw+NzozcnCY21O4rz3A+veJiIiuSCMIprgGcZOcLAXFP9GmDhKUbhuKWcunRw=="],
|
||||
|
||||
"@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
|
||||
|
||||
@@ -280,6 +283,10 @@
|
||||
|
||||
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
|
||||
|
||||
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="],
|
||||
|
||||
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.1", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA=="],
|
||||
|
||||
"@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
@@ -292,31 +299,33 @@
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
||||
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||
|
||||
@@ -390,21 +399,21 @@
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="],
|
||||
"axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
@@ -412,7 +421,7 @@
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
@@ -452,21 +461,23 @@
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
||||
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="],
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
@@ -482,9 +493,9 @@
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
|
||||
"eslint-config-next": ["eslint-config-next@16.1.6", "", { "dependencies": { "@next/eslint-plugin-next": "16.1.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA=="],
|
||||
"eslint-config-next": ["eslint-config-next@16.2.3", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA=="],
|
||||
|
||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
||||
|
||||
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
|
||||
|
||||
@@ -532,7 +543,7 @@
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="],
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
@@ -552,7 +563,7 @@
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
@@ -674,29 +685,29 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
@@ -726,11 +737,11 @@
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
|
||||
"next": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="],
|
||||
|
||||
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
@@ -766,11 +777,11 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
@@ -782,9 +793,9 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
@@ -792,7 +803,7 @@
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
"resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
@@ -826,7 +837,7 @@
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
@@ -860,15 +871,15 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||
|
||||
@@ -884,13 +895,13 @@
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||
|
||||
@@ -922,13 +933,13 @@
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -936,7 +947,7 @@
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
@@ -950,19 +961,17 @@
|
||||
|
||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-react/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
}
|
||||
|
||||
+16
-13
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "lncvrt-games-launcher",
|
||||
"private": true,
|
||||
"version": "1.6.5",
|
||||
"version": "1.9.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -14,33 +14,36 @@
|
||||
"@fortawesome/fontawesome-svg-core": "7.2.0",
|
||||
"@fortawesome/free-brands-svg-icons": "7.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
||||
"@fortawesome/react-fontawesome": "3.2.0",
|
||||
"@fortawesome/react-fontawesome": "3.3.0",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "2.6.0",
|
||||
"@tauri-apps/plugin-fs": "2.4.5",
|
||||
"@tauri-apps/plugin-http": "2.5.7",
|
||||
"@tauri-apps/plugin-dialog": "2.7.0",
|
||||
"@tauri-apps/plugin-fs": "2.5.0",
|
||||
"@tauri-apps/plugin-http": "2.5.8",
|
||||
"@tauri-apps/plugin-notification": "2.3.3",
|
||||
"@tauri-apps/plugin-opener": "2.5.3",
|
||||
"@tauri-apps/plugin-os": "2.3.2",
|
||||
"@tauri-apps/plugin-store": "2.4.2",
|
||||
"@tauri-apps/plugin-updater": "2.10.1",
|
||||
"@tauri-apps/plugin-window-state": "2.4.1",
|
||||
"next": "16.1.6",
|
||||
"dompurify": "3.3.3",
|
||||
"next": "16.2.3",
|
||||
"pretty-bytes": "7.1.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@tailwindcss/postcss": "4.2.2",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "25.3.5",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"crypto-js": "4.2.0",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "4.2.1",
|
||||
"typescript": "5.9.3"
|
||||
"eslint-config-next": "16.2.3",
|
||||
"tailwindcss": "4.2.2",
|
||||
"typescript": "6.0.2"
|
||||
}
|
||||
}
|
||||
+27
-14
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lncvrt-games-launcher"
|
||||
version = "1.6.5"
|
||||
version = "1.9.1"
|
||||
authors = ["Lncvrt"]
|
||||
edition = "2024"
|
||||
|
||||
@@ -17,28 +17,41 @@ tauri-plugin-opener = "2.5.3"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
tauri-plugin-os = "2.3.2"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = ["stream", "rustls"] }
|
||||
tokio = "1.50.0"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = [
|
||||
"stream",
|
||||
"rustls",
|
||||
] }
|
||||
tokio = "1.51.1"
|
||||
futures-util = { version = "0.3.32", features = ["io"] }
|
||||
tauri-plugin-decorum = "1.1.1"
|
||||
tauri-plugin-fs = "2.4.5"
|
||||
zip = "8.2.0"
|
||||
libc = "0.2.182"
|
||||
tauri-plugin-dialog = "2.6.0"
|
||||
tauri-plugin-fs = "2.5.0"
|
||||
zip = "8.5.1"
|
||||
tauri-plugin-dialog = "2.7.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
sysinfo = "0.38.3"
|
||||
sha2 = "0.10.9"
|
||||
sha2 = "0.11.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-prevent-default = "4.0.4"
|
||||
openssl = { version = "0.10.75", features = ["v110", "vendored"] }
|
||||
openssl = { version = "0.10.77", features = ["v111", "vendored"] }
|
||||
base64 = "0.22.1"
|
||||
tauri-plugin-http = "2.5.7"
|
||||
tauri-plugin-http = "2.5.8"
|
||||
dashmap = "6.1.0"
|
||||
tauri-plugin-store = "2.4.2"
|
||||
hex = "0.4.3"
|
||||
window-vibrancy = "0.7.1"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
shlex = "1.3.0"
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
sysinfo = "0.38.4"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.4.0"
|
||||
tauri-plugin-single-instance = "2.4.1"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
tauri-plugin-decorum = "1.1.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
@@ -2,9 +2,7 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
@@ -20,26 +18,25 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-request-user-attention",
|
||||
"core:window:allow-set-progress-bar",
|
||||
"decorum:allow-show-snap-overlay",
|
||||
"fs:default",
|
||||
"dialog:default",
|
||||
"dialog:allow-open",
|
||||
"notification:default",
|
||||
"clipboard-manager:default",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"store:default",
|
||||
"store:allow-load",
|
||||
"store:allow-save",
|
||||
"fs:allow-applocaldata-read",
|
||||
"fs:allow-applocaldata-write",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{ "path": "$APPLOCALDATA/game/**" }
|
||||
]
|
||||
"allow": [{ "path": "$APPLOCALDATA/game/**" }, { "path": "$APPCONFIG/versions.json" }]
|
||||
},
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
{ "path": "$APPLOCALDATA/game/**" }
|
||||
]
|
||||
"allow": [{ "path": "$APPLOCALDATA/game/**" }]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
@@ -52,7 +49,8 @@
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/berry" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/coin" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/legacy" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total" }
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/launcher/mods" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"window-state:default"
|
||||
]
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": ["window-state:default"]
|
||||
}
|
||||
+406
-168
@@ -1,10 +1,12 @@
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use dashmap::DashMap;
|
||||
use futures_util::stream::StreamExt;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::sign::Verifier;
|
||||
use sha2::{Digest, Sha512};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
fs::{File, create_dir_all},
|
||||
@@ -13,21 +15,47 @@ use std::{
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use sysinfo::System;
|
||||
use tauri::window::{ProgressBarState, ProgressBarStatus};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_os::platform;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::{io::AsyncWriteExt, time::timeout};
|
||||
#[cfg(target_os = "macos")]
|
||||
use window_vibrancy::{NSVisualEffectMaterial, apply_vibrancy};
|
||||
use zip::ZipArchive;
|
||||
use zip::result::ZipError;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use sysinfo::System;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
#[allow(unused)]
|
||||
struct AppState {
|
||||
custom_data_dir: RwLock<Option<PathBuf>>,
|
||||
}
|
||||
|
||||
fn data_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let state = app.state::<AppState>();
|
||||
let guard = state.custom_data_dir.read().unwrap();
|
||||
match &*guard {
|
||||
Some(p) => Ok(p.clone()),
|
||||
None => app.path().app_local_data_dir().map_err(|e| e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
static CANCEL_MAP: OnceLock<DashMap<String, bool>> = OnceLock::new();
|
||||
|
||||
fn cancel_map() -> &'static DashMap<String, bool> {
|
||||
CANCEL_MAP.get_or_init(DashMap::new)
|
||||
}
|
||||
|
||||
fn is_cancelled(name: &str) -> bool {
|
||||
cancel_map().get(name).map(|v| *v).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
fn is_running_by_path(path: &Path) -> bool {
|
||||
let sys = System::new_all();
|
||||
let target = match path.canonicalize() {
|
||||
@@ -48,20 +76,25 @@ fn should_skip(name: &str) -> bool {
|
||||
|| name.ends_with(".DS_Store")
|
||||
}
|
||||
|
||||
async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
||||
async fn unzip_to_dir(app: AppHandle, zip_path: PathBuf, out_dir: PathBuf, name: String) -> String {
|
||||
let _ = app.emit("unzip-start", format!("{}", name));
|
||||
let res = tauri::async_runtime::spawn_blocking(move || {
|
||||
let file = File::open(zip_path)?;
|
||||
let mut archive = ZipArchive::new(BufReader::new(file))?;
|
||||
let total = archive.len();
|
||||
|
||||
for i in 0..archive.len() {
|
||||
for i in 0..total {
|
||||
let mut entry = archive.by_index(i)?;
|
||||
let name = entry.name();
|
||||
let entry_name = entry.name();
|
||||
|
||||
if should_skip(name) {
|
||||
if should_skip(entry_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let outpath = out_dir.join(name);
|
||||
let outpath = out_dir.join(entry_name);
|
||||
if !outpath.starts_with(&out_dir) {
|
||||
return Err(ZipError::InvalidArchive("Path traversal detected".into()));
|
||||
}
|
||||
|
||||
if entry.is_dir() {
|
||||
create_dir_all(&outpath)?;
|
||||
@@ -83,6 +116,8 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
||||
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app.emit("unzip-progress", format!("{}:{}:{}", name, i + 1, total));
|
||||
}
|
||||
|
||||
Ok::<(), zip::result::ZipError>(())
|
||||
@@ -97,7 +132,7 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
||||
|
||||
#[tauri::command]
|
||||
fn folder_size(app: AppHandle, version: String) -> String {
|
||||
let path = match app.path().app_local_data_dir() {
|
||||
let path = match data_dir(&app) {
|
||||
Ok(p) => p.join("game").join(&version),
|
||||
Err(_) => return "-1".to_string(),
|
||||
};
|
||||
@@ -125,126 +160,130 @@ fn folder_size(app: AppHandle, version: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[tauri::command]
|
||||
async fn download(
|
||||
app: AppHandle,
|
||||
url: String,
|
||||
name: String,
|
||||
executable: String,
|
||||
hash: String,
|
||||
download_type: i8,
|
||||
mod_id: String,
|
||||
) -> String {
|
||||
println!("[download] Starting download for '{}' from '{}'", name, url);
|
||||
let _ = app.emit("download-start", format!("{}", name));
|
||||
let window = app.get_webview_window("main").expect("main window missing");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = match client.get(&url).send().await {
|
||||
Ok(r) => {
|
||||
println!("[download] HTTP request succeeded, status: {}", r.status());
|
||||
r
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[download] ERROR: HTTP request failed: {:?}", e);
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
let total_size = resp.content_length().unwrap_or(0);
|
||||
println!(
|
||||
"[download] Content-Length: {} bytes ({:.2} MB)",
|
||||
total_size,
|
||||
total_size as f64 / 1_048_576.0
|
||||
);
|
||||
let _ = app.emit("download-size", format!("{}:{}", &name, &total_size));
|
||||
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = resp.bytes_stream();
|
||||
|
||||
let downloads_path = match app.path().app_local_data_dir() {
|
||||
Ok(p) => {
|
||||
let path = p.join("downloads");
|
||||
println!("[download] Downloads path: {:?}", path);
|
||||
path
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"[download] ERROR: Failed to resolve app_local_data_dir: {}",
|
||||
e
|
||||
);
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
let game_path = match app.path().app_local_data_dir() {
|
||||
Ok(p) => {
|
||||
let path = p.join("game");
|
||||
println!("[download] Game path: {:?}", path);
|
||||
path
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[download] ERROR: Failed to resolve game path: {}", e);
|
||||
let base_dir = match data_dir(&app) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
let downloads_path = base_dir.join("downloads");
|
||||
let game_path = base_dir.join("game");
|
||||
|
||||
let download_part_path = downloads_path.join(format!("{}.part", name));
|
||||
let download_zip_path = downloads_path.join(format!("{}.zip", name));
|
||||
println!("[download] Part file: {:?}", download_part_path);
|
||||
println!("[download] Zip file: {:?}", download_zip_path);
|
||||
|
||||
if download_part_path.exists() {
|
||||
println!("[download] Stale .part file found, removing...");
|
||||
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[download] Creating downloads dir (if needed): {:?}",
|
||||
downloads_path
|
||||
);
|
||||
let _ = tokio::fs::create_dir_all(&downloads_path).await;
|
||||
|
||||
if let Ok(true) = tokio::fs::try_exists(&game_path.join(name.clone())).await {
|
||||
println!(
|
||||
"[download] Existing game dir found, removing: {:?}",
|
||||
game_path.join(&name)
|
||||
);
|
||||
let _ = tokio::fs::remove_dir_all(&game_path.join(name.clone())).await;
|
||||
let resume_from: u64 = match tokio::fs::metadata(&download_part_path).await {
|
||||
Ok(meta) => {
|
||||
let size = meta.len();
|
||||
size
|
||||
}
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.get(&url);
|
||||
if resume_from > 0 {
|
||||
request = request.header("Range", format!("bytes={}-", resume_from));
|
||||
}
|
||||
|
||||
println!("[download] Creating game dir: {:?}", game_path.join(&name));
|
||||
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||
|
||||
let mut file = match tokio::fs::File::create(download_part_path.clone()).await {
|
||||
Ok(f) => {
|
||||
println!("[download] Created .part file successfully");
|
||||
f
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[download] ERROR: Failed to create .part file: {}", e);
|
||||
let resp = match request.send().await {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
let status = resp.status();
|
||||
|
||||
let actual_resume_from = if resume_from > 0 && status == reqwest::StatusCode::OK {
|
||||
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||
0
|
||||
} else if resume_from > 0 && status == reqwest::StatusCode::PARTIAL_CONTENT {
|
||||
resume_from
|
||||
} else if resume_from > 0 {
|
||||
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||
0
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let remaining_size = resp.content_length().unwrap_or(0);
|
||||
let total_size = actual_resume_from + remaining_size;
|
||||
let _ = app.emit("download-size", format!("{}:{}", &name, &total_size));
|
||||
|
||||
let mut file = if actual_resume_from > 0 {
|
||||
match tokio::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&download_part_path)
|
||||
.await
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if download_type == 0 {
|
||||
if let Ok(true) = tokio::fs::try_exists(&game_path.join(&name)).await {
|
||||
let _ = tokio::fs::remove_dir_all(&game_path.join(&name)).await;
|
||||
}
|
||||
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||
}
|
||||
|
||||
match tokio::fs::File::create(&download_part_path).await {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut downloaded: u64 = actual_resume_from;
|
||||
let mut stream = resp.bytes_stream();
|
||||
|
||||
let start = Instant::now();
|
||||
let mut chunk_count: u64 = 0;
|
||||
|
||||
while let Ok(Some(chunk_result)) = timeout(Duration::from_secs(5), stream.next()).await {
|
||||
if is_cancelled(&name) {
|
||||
cancel_map().remove(&name);
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "0".to_string();
|
||||
}
|
||||
|
||||
let chunk = match chunk_result {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"[download] ERROR: Failed to read chunk #{}: {}",
|
||||
chunk_count, e
|
||||
);
|
||||
Err(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
chunk_count += 1;
|
||||
|
||||
if let Err(e) = file.write_all(&chunk).await {
|
||||
println!(
|
||||
"[download] ERROR: Failed to write chunk #{} to disk: {}",
|
||||
chunk_count, e
|
||||
);
|
||||
if let Err(_) = file.write_all(&chunk).await {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
@@ -256,24 +295,24 @@ async fn download(
|
||||
0.0
|
||||
};
|
||||
|
||||
let session_bytes = downloaded - actual_resume_from;
|
||||
let elapsed_secs = start.elapsed().as_secs_f64();
|
||||
let speed = (downloaded as f64) / elapsed_secs;
|
||||
let eta_secs = if total_size > downloaded {
|
||||
((total_size - downloaded) as f64) / speed
|
||||
let speed = if elapsed_secs > 0.0 {
|
||||
session_bytes as f64 / elapsed_secs
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let eta_secs = if total_size > downloaded && speed > 0.0 {
|
||||
(total_size - downloaded) as f64 / speed
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if chunk_count % 50 == 0 || downloaded == total_size {
|
||||
println!(
|
||||
"[download] [{:.1}%] {:.2} MB / {:.2} MB | Speed: {:.1} KB/s | ETA: {:.1}s | Chunks: {}",
|
||||
progress,
|
||||
downloaded as f64 / 1_048_576.0,
|
||||
total_size as f64 / 1_048_576.0,
|
||||
speed / 1024.0,
|
||||
eta_secs,
|
||||
chunk_count,
|
||||
);
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::Normal),
|
||||
progress: Some(progress as u64),
|
||||
});
|
||||
}
|
||||
|
||||
let _ = app.emit(
|
||||
@@ -286,46 +325,28 @@ async fn download(
|
||||
}
|
||||
|
||||
if total_size > 0 && downloaded < total_size {
|
||||
println!(
|
||||
"[download] ERROR: Download incomplete — got {} of {} bytes ({:.1}% complete)",
|
||||
downloaded,
|
||||
total_size,
|
||||
(downloaded as f64 / total_size as f64) * 100.0
|
||||
);
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
println!(
|
||||
"[download] Download complete: {} bytes in {:.2}s",
|
||||
downloaded,
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
println!("[download] Verifying SHA-512 hash...");
|
||||
let _ = app.emit("download-hash-checking", format!("{}", &name));
|
||||
|
||||
let download_hash = {
|
||||
let mut file = match tokio::fs::File::open(download_part_path.clone()).await {
|
||||
let mut file = match tokio::fs::File::open(&download_part_path).await {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"[download] ERROR: Failed to open .part file for hashing: {}",
|
||||
e
|
||||
);
|
||||
Err(_) => {
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
let mut hasher = Sha512::new();
|
||||
let mut bytes_hashed: u64 = 0;
|
||||
{
|
||||
let mut buffer = [0; 8192];
|
||||
let mut buffer = [0u8; 8192];
|
||||
loop {
|
||||
let bytes_read = match file.read(&mut buffer).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"[download] ERROR: Failed to read file during hashing: {}",
|
||||
e
|
||||
);
|
||||
Err(_) => {
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
@@ -333,48 +354,61 @@ async fn download(
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
bytes_hashed += bytes_read as u64;
|
||||
}
|
||||
}
|
||||
drop(file);
|
||||
println!("[download] Hashed {} bytes", bytes_hashed);
|
||||
format!("{:x}", hasher.finalize())
|
||||
hex::encode(hasher.finalize())
|
||||
};
|
||||
|
||||
println!("[download] Expected hash: {}", hash);
|
||||
println!("[download] Computed hash: {}", download_hash);
|
||||
|
||||
if hash != download_hash {
|
||||
println!("[download] ERROR: Hash mismatch! File may be corrupt or tampered with.");
|
||||
let _ = tokio::fs::remove_file(download_part_path.clone()).await;
|
||||
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
println!("[download] Hash verified OK");
|
||||
let _ = app.emit("download-finishing", format!("{}", &name));
|
||||
|
||||
println!("[download] Renaming .part -> .zip: {:?}", download_zip_path);
|
||||
if let Err(e) = tokio::fs::rename(download_part_path.clone(), download_zip_path.clone()).await {
|
||||
println!("[download] ERROR: Failed to rename .part to .zip: {}", e);
|
||||
if let Err(_) = tokio::fs::rename(&download_part_path, &download_zip_path).await {
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
println!("[download] Unzipping to: {:?}", game_path.join(&name));
|
||||
let unzip_res = unzip_to_dir(download_zip_path.clone(), game_path.join(&name)).await;
|
||||
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||
|
||||
println!("[download] Removing zip file: {:?}", download_zip_path);
|
||||
let _ = tokio::fs::remove_file(download_zip_path.clone()).await;
|
||||
let unzip_res = unzip_to_dir(
|
||||
app.clone(),
|
||||
download_zip_path.clone(),
|
||||
if download_type == 2 {
|
||||
game_path
|
||||
.join(&name)
|
||||
.join("BepInEx")
|
||||
.join("plugins")
|
||||
.join(&mod_id)
|
||||
} else {
|
||||
game_path.join(&name)
|
||||
},
|
||||
name.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = tokio::fs::remove_file(&download_zip_path).await;
|
||||
|
||||
if unzip_res == "-1" {
|
||||
println!("[download] ERROR: Unzip failed for '{}'", name);
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
println!(
|
||||
"[download] SUCCESS: '{}' downloaded, verified, and extracted",
|
||||
name
|
||||
);
|
||||
return "1".to_string();
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
"1".to_string()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
@@ -387,7 +421,7 @@ fn launch_game(
|
||||
use_wine: bool,
|
||||
wine_command: String,
|
||||
) {
|
||||
let game_folder = match app.path().app_local_data_dir() {
|
||||
let game_folder = match data_dir(&app) {
|
||||
Ok(p) => p.join("game").join(&name),
|
||||
Err(_) => return,
|
||||
};
|
||||
@@ -400,8 +434,7 @@ fn launch_game(
|
||||
//if already running on macos, it'll auto take the user to that proccess
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_dialog::MessageDialogKind;
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
|
||||
if !use_wine && is_running_by_path(&exe_path) {
|
||||
app.dialog()
|
||||
@@ -422,20 +455,34 @@ fn launch_game(
|
||||
let quoted_path = format!("\"{}\"", exe_path.to_string_lossy());
|
||||
let cmd = wine_command.replace("%path%", "ed_path);
|
||||
|
||||
if let Err(_) = Command::new("bash")
|
||||
if let Err(e) =
|
||||
Command::new(&std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()))
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.current_dir(&game_folder)
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("Failed to launch game with Wine");
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
|
||||
eprintln!("Failed to launch game with Wine: {}", e);
|
||||
|
||||
app.dialog()
|
||||
.message(format!(
|
||||
"{} failed to launch with Wine\n\n{}",
|
||||
display_name, e
|
||||
))
|
||||
.kind(MessageDialogKind::Error)
|
||||
.title("Game already running")
|
||||
.show(|_| {});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if platform() == "macos" {
|
||||
let bepinex_folder = game_folder.join("BepInEx");
|
||||
|
||||
if platform() == "macos" && !bepinex_folder.exists() && executable.ends_with(".app") {
|
||||
if let Err(_) = Command::new("open")
|
||||
.arg(&executable)
|
||||
.current_dir(&game_folder)
|
||||
@@ -443,12 +490,22 @@ fn launch_game(
|
||||
{
|
||||
eprintln!("Failed to launch game on macOS");
|
||||
}
|
||||
} else {
|
||||
if (platform() == "macos" || platform() == "linux") && bepinex_folder.exists() {
|
||||
if let Err(_) = Command::new("./run_bepinex.sh")
|
||||
.arg(&executable)
|
||||
.current_dir(&game_folder)
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("Failed to launch game");
|
||||
}
|
||||
} else {
|
||||
if let Err(_) = Command::new(&exe_path).current_dir(&game_folder).spawn() {
|
||||
eprintln!("Failed to launch game");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn verify_signature(body: String, signature: String, public_key: String) -> bool {
|
||||
@@ -472,10 +529,160 @@ fn verify_signature(body: String, signature: String, public_key: String) -> bool
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cancel_download(name: String) {
|
||||
cancel_map().insert(name.clone(), true);
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||
fs::create_dir_all(dst)?;
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
if src_path.is_dir() {
|
||||
copy_dir_recursive(&src_path, &dst_path)?;
|
||||
} else {
|
||||
fs::copy(&src_path, &dst_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn move_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||
if !src.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = dst.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
if dst.exists() {
|
||||
fs::remove_dir_all(dst)?;
|
||||
}
|
||||
match fs::rename(src, dst) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => {
|
||||
copy_dir_recursive(src, dst)?;
|
||||
fs::remove_dir_all(src)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn move_game_data(app: AppHandle, destination: String) -> Result<(), String> {
|
||||
let default_dir = app.path().app_local_data_dir().map_err(|e| e.to_string())?;
|
||||
let current_dir = data_dir(&app)?;
|
||||
|
||||
let new_dir = if destination.is_empty() {
|
||||
default_dir.clone()
|
||||
} else {
|
||||
PathBuf::from(&destination)
|
||||
};
|
||||
|
||||
if current_dir == new_dir {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src_game = current_dir.join("game");
|
||||
let dst_game = new_dir.join("game");
|
||||
let src_downloads = current_dir.join("downloads");
|
||||
let dst_downloads = new_dir.join("downloads");
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
fs::create_dir_all(&new_dir).map_err(|e| e.to_string())?;
|
||||
move_dir(&src_game, &dst_game).map_err(|e| e.to_string())?;
|
||||
move_dir(&src_downloads, &dst_downloads).map_err(|e| e.to_string())?;
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
let state = app.state::<AppState>();
|
||||
let mut guard = state.custom_data_dir.write().unwrap();
|
||||
if destination.is_empty() {
|
||||
*guard = None;
|
||||
} else {
|
||||
*guard = Some(PathBuf::from(&destination));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn restart_app(app: AppHandle) {
|
||||
app.restart();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_game_folder(app: AppHandle, version: String) -> Result<(), String> {
|
||||
let dir = data_dir(&app)?;
|
||||
let path = dir.join("game").join(&version);
|
||||
if !path.exists() || !path.is_dir() {
|
||||
return Err(format!("Game folder \"{}\" not found.", path.display()));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
Command::new("open")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
#[cfg(target_os = "windows")]
|
||||
Command::new("explorer")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
#[cfg(target_os = "linux")]
|
||||
Command::new("xdg-open")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn remove_stale_executable(app: AppHandle, version: String, executable: String) -> bool {
|
||||
let dir = match data_dir(&app) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let path = dir.join("game").join(&version).join(&executable);
|
||||
if path.exists() {
|
||||
fs::remove_file(&path).is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_new_window(
|
||||
app: tauri::AppHandle,
|
||||
title: String,
|
||||
name: String,
|
||||
url: String,
|
||||
width: f64,
|
||||
height: f64,
|
||||
) {
|
||||
if let Some(window) = app.get_webview_window(&name) {
|
||||
let _ = window.show();
|
||||
return;
|
||||
};
|
||||
|
||||
tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
name,
|
||||
tauri::WebviewUrl::External(url.parse().unwrap()),
|
||||
)
|
||||
.inner_size(width, height)
|
||||
.title(title)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[allow(unused_variables)]
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(
|
||||
tauri_plugin_prevent_default::Builder::new()
|
||||
@@ -510,17 +717,48 @@ pub fn run() {
|
||||
download,
|
||||
launch_game,
|
||||
folder_size,
|
||||
verify_signature
|
||||
verify_signature,
|
||||
cancel_download,
|
||||
open_new_window,
|
||||
move_game_data,
|
||||
restart_app,
|
||||
open_game_folder,
|
||||
remove_stale_executable
|
||||
])
|
||||
.setup(|app| {
|
||||
let custom_dir = app
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.ok()
|
||||
.map(|d| d.join("settings.json"))
|
||||
.filter(|p| p.exists())
|
||||
.and_then(|p| fs::read_to_string(p).ok())
|
||||
.and_then(|c| serde_json::from_str::<serde_json::Value>(&c).ok())
|
||||
.and_then(|v| {
|
||||
v.get("customDataLocation")
|
||||
.and_then(|s| s.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(PathBuf::from)
|
||||
});
|
||||
|
||||
app.manage(AppState {
|
||||
custom_data_dir: RwLock::new(custom_dir),
|
||||
});
|
||||
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
|
||||
.expect("Unable to apply window blur");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(main_window) = app.get_webview_window("main") {
|
||||
if let Err(e) = main_window.create_overlay_titlebar() {
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
if let Err(e) = window.create_overlay_titlebar() {
|
||||
eprintln!("Failed to create overlay titlebar: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.6.5",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
@@ -10,15 +10,7 @@
|
||||
"frontendDist": "../out"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"minWidth": 875,
|
||||
"minHeight": 500
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.6.5",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
@@ -13,9 +13,10 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"url": "/main",
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"minWidth": 875,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 500
|
||||
}
|
||||
],
|
||||
@@ -24,6 +25,20 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false
|
||||
"active": true,
|
||||
"category": "Game",
|
||||
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||
"targets": ["deb", "rpm"],
|
||||
"icon": ["icons/icon.png"],
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
|
||||
"endpoints": [
|
||||
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}&arch={{arch}}¤tVersion={{current_version}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.6.5",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
@@ -14,12 +14,14 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"url": "/main",
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"minWidth": 875,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 500,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
"hiddenTitle": true,
|
||||
"transparent": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -28,10 +30,22 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["app"],
|
||||
"category": "Game",
|
||||
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||
"targets": ["dmg", "app"],
|
||||
"icon": ["icons/icon.icns"],
|
||||
"createUpdaterArtifacts": true,
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "13.0"
|
||||
}
|
||||
},
|
||||
"icon": ["icons/icon.icns"]
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
|
||||
"endpoints": [
|
||||
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}&arch={{arch}}¤tVersion={{current_version}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.6.5",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
@@ -14,12 +14,11 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"url": "/main",
|
||||
"width": 1000,
|
||||
"height": 632,
|
||||
"minWidth": 875,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 532,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"decorations": false,
|
||||
"shadow": true
|
||||
}
|
||||
@@ -29,7 +28,20 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"icon": ["icons/icon.ico"]
|
||||
"active": true,
|
||||
"category": "Game",
|
||||
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||
"targets": ["msi"],
|
||||
"icon": ["icons/icon.ico"],
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
|
||||
"endpoints": [
|
||||
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}&arch={{arch}}¤tVersion={{current_version}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
@apply bg-(--col0) text-white select-none;
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
--col0: rgb(8, 8, 8);
|
||||
--col1: rgb(16, 16, 16);
|
||||
--col2: rgb(32, 32, 32);
|
||||
--col3: rgb(48, 48, 48);
|
||||
--col4: rgb(64, 64, 64);
|
||||
--col5: rgb(80, 80, 80);
|
||||
--col6: rgb(96, 96, 96);
|
||||
--col7: rgb(112, 112, 112);
|
||||
--col8: rgb(128, 128, 128);
|
||||
}
|
||||
|
||||
.red-theme {
|
||||
--col0: rgb(32, 8, 8);
|
||||
--col1: rgb(40, 16, 16);
|
||||
--col2: rgb(56, 32, 32);
|
||||
--col3: rgb(72, 48, 48);
|
||||
--col4: rgb(88, 64, 64);
|
||||
--col5: rgb(104, 80, 80);
|
||||
--col6: rgb(120, 96, 96);
|
||||
--col7: rgb(136, 112, 112);
|
||||
--col8: rgb(152, 128, 128);
|
||||
}
|
||||
|
||||
.blue-theme {
|
||||
--col0: rgb(8, 8, 32);
|
||||
--col1: rgb(16, 16, 40);
|
||||
--col2: rgb(32, 32, 56);
|
||||
--col3: rgb(48, 48, 72);
|
||||
--col4: rgb(64, 64, 88);
|
||||
--col5: rgb(80, 80, 104);
|
||||
--col6: rgb(96, 96, 120);
|
||||
--col7: rgb(112, 112, 136);
|
||||
--col8: rgb(128, 128, 152);
|
||||
}
|
||||
|
||||
.purple-theme {
|
||||
--col0: rgb(20, 8, 32);
|
||||
--col1: rgb(28, 16, 40);
|
||||
--col2: rgb(44, 32, 56);
|
||||
--col3: rgb(60, 48, 72);
|
||||
--col4: rgb(76, 64, 88);
|
||||
--col5: rgb(92, 80, 104);
|
||||
--col6: rgb(108, 96, 120);
|
||||
--col7: rgb(124, 112, 136);
|
||||
--col8: rgb(140, 128, 152);
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply rounded-md cursor-pointer text-[16px] py-1 px-2 transition-colors border;
|
||||
}
|
||||
|
||||
.btntheme1 {
|
||||
@apply bg-(--col2) border-(--col4) hover:bg-(--col4) hover:border-(--col6) disabled:text-gray-300 disabled:bg-(--col1) disabled:border-(--col5) disabled:hover:text-gray-100 disabled:hover:bg-(--col2) disabled:hover:border-(--col6);
|
||||
}
|
||||
|
||||
.btntheme2,
|
||||
.downloads-entry .entry-info-item,
|
||||
.downloads-entry .button {
|
||||
@apply bg-(--col3) border-(--col5) hover:bg-(--col5) hover:border-(--col7) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col6) disabled:hover:text-gray-100 disabled:hover:bg-(--col3) disabled:hover:border-(--col7);
|
||||
}
|
||||
|
||||
.btntheme3,
|
||||
.downloads-entry:hover .entry-info-item,
|
||||
.downloads-entry:hover .button {
|
||||
@apply bg-(--col4) border-(--col6) hover:bg-(--col6) hover:border-(--col8) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col7) disabled:hover:text-gray-100 disabled:hover:bg-(--col4) disabled:hover:border-(--col8);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-(--col2) rounded-lg;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-(--col4) w-1 rounded-lg active:bg-(--col5);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-overlay {
|
||||
@apply fixed w-screen h-screen z-99999 flex justify-center items-center animate-[fadeIn_0.2s_ease-out_forwards] left-0 top-0 bg-[rgba(0,0,0,0.5)];
|
||||
}
|
||||
|
||||
.popup-overlay.fade-out {
|
||||
@apply animate-[fadeOut_0.2s_ease-out_forwards];
|
||||
}
|
||||
|
||||
.popup-box {
|
||||
@apply relative w-150 h-120 rounded-lg bg-(--col1) border border-(--col3) flex flex-col p-6;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
@apply flex-1 overflow-auto bg-(--col2) border border-(--col4) rounded-lg mt-4;
|
||||
}
|
||||
|
||||
.popup-entry {
|
||||
@apply relative h-fit bg-(--col3) m-2 p-2 rounded-lg border border-(--col5);
|
||||
}
|
||||
|
||||
.popup-entry button {
|
||||
@apply absolute;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@apply flex justify-center items-center absolute text-2xl text-gray-200 hover:text-white cursor-pointer h-12 w-12 p-3 rounded-lg left-2 top-2 transition-colors border;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply border-2 border-(--col4) rounded-md bg-(--col2) py-1 px-2 focus:bg-(--col3) focus:border-(--col6) transition-colors w-full;
|
||||
}
|
||||
|
||||
.entry-info-item {
|
||||
@apply flex flex-row items-center gap-1 border text-gray-300 py-1 px-2 rounded-md w-fit text-[16px] transition-colors cursor-pointer;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.downloads-container {
|
||||
@apply flex justify-center;
|
||||
}
|
||||
|
||||
.downloads-scroll {
|
||||
@apply bg-(--col1) border border-(--col3) rounded-lg overflow-y-auto w-full;
|
||||
}
|
||||
|
||||
.downloads-entry {
|
||||
@apply flex justify-between items-center m-2 p-2 rounded-lg text-gray-200 text-lg transition-colors bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5) cursor-pointer;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.box {
|
||||
@apply bg-(--col1) border border-(--col3) rounded-lg w-auto p-1;
|
||||
}
|
||||
|
||||
.leaderboard-button,
|
||||
.leaderboard-select {
|
||||
@apply bg-(--col2) hover:bg-(--col4) border border-(--col4) hover:border-(--col6) rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.leaderboard-entry {
|
||||
@apply rounded-lg text-gray-200 text-lg px-1 py-0.5 transition-colors bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5) cursor-pointer;
|
||||
}
|
||||
@@ -1,667 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Sidebar from '@/componets/Sidebar'
|
||||
import './Globals.css'
|
||||
import { DownloadProgress } from '@/types/DownloadProgress'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronLeft, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
readNormalConfig,
|
||||
readVersionsConfig,
|
||||
writeVersionsConfig
|
||||
} from '@/lib/BazookaManager'
|
||||
import { VersionsConfig } from '@/types/VersionsConfig'
|
||||
import { NormalConfig } from '@/types/NormalConfig'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { GlobalProvider } from './GlobalProvider'
|
||||
import { Roboto } from 'next/font/google'
|
||||
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
|
||||
import { GameVersion } from '@/types/GameVersion'
|
||||
import { Game } from '@/types/Game'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||
import { notifyUser } from '@/lib/Notifications'
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission
|
||||
} from '@tauri-apps/plugin-notification'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
|
||||
import VersionsDownloadPopup from '@/componets/popups/VersionsDownload'
|
||||
import GamesDownloadPopup from '@/componets/popups/GamesDownload'
|
||||
import DownloadsPopup from '@/componets/popups/Downloads'
|
||||
import VersionVersionPopup from '@/componets/popups/VersionVersion'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { verifySignature } from '@/lib/Util'
|
||||
import {
|
||||
getCurrentWindow,
|
||||
ProgressBarStatus,
|
||||
UserAttentionType
|
||||
} from '@tauri-apps/api/window'
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ['latin']
|
||||
})
|
||||
|
||||
export default function RootLayout ({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingText, setLoadingText] = useState('Loading...')
|
||||
const [outdated, setOutdated] = useState(false)
|
||||
const [version, setVersion] = useState<string | null>(null)
|
||||
const [platformName, setPlatformName] = useState<string | null>(null)
|
||||
|
||||
const [serverVersionList, setServerVersionList] =
|
||||
useState<null | ServerVersionsResponse>(null)
|
||||
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
|
||||
|
||||
const [downloadedVersionsConfig, setDownloadedVersionsConfig] =
|
||||
useState<VersionsConfig | null>(null)
|
||||
const [normalConfig, setNormalConfig] = useState<NormalConfig | null>(null)
|
||||
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
const [popupMode, setPopupMode] = useState<null | number>(null)
|
||||
const [fadeOut, setFadeOut] = useState(false)
|
||||
|
||||
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
||||
[]
|
||||
)
|
||||
const [downloadQueue, setDownloadQueue] = useState<string[]>([])
|
||||
const [isProcessingQueue, setIsProcessingQueue] = useState<boolean>(false)
|
||||
const [managingVersion, setManagingVersion] = useState<string | null>(null)
|
||||
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
|
||||
useState<boolean>(false)
|
||||
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
||||
|
||||
const [category, setCategory] = useState<number>(-1)
|
||||
|
||||
const pathname = usePathname()
|
||||
const revisionCheck = useRef(false)
|
||||
const previousQueueLength = useRef(0)
|
||||
|
||||
function getSpecialVersionsList (game?: number): GameVersion[] {
|
||||
if (!normalConfig || !serverVersionList) return []
|
||||
|
||||
return serverVersionList.versions
|
||||
.filter(
|
||||
v => !Object.keys(downloadedVersionsConfig?.list ?? []).includes(v.id)
|
||||
)
|
||||
.filter(v => {
|
||||
if (
|
||||
platformName == 'linux' &&
|
||||
v.wine &&
|
||||
!normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
return false
|
||||
|
||||
if (game && v.game != game) return false
|
||||
if (category != -1 && v.category != category) return false
|
||||
if (downloadProgress.length != 0) {
|
||||
return !downloadProgress.some(d => d.version === v.id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.game !== a.game) return a.game - b.game
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
const getVersionInfo = useCallback(
|
||||
(id: string | undefined): GameVersion | undefined => {
|
||||
if (!id) return undefined
|
||||
return serverVersionList?.versions.find(v => v.id === id)
|
||||
},
|
||||
[serverVersionList]
|
||||
)
|
||||
|
||||
const getGameInfo = useCallback(
|
||||
(game: number | undefined): Game | undefined => {
|
||||
if (!game) return undefined
|
||||
return serverVersionList?.games.find(g => g.id === game)
|
||||
},
|
||||
[serverVersionList]
|
||||
)
|
||||
|
||||
function getListOfGames (): Game[] {
|
||||
if (!downloadedVersionsConfig?.list) return []
|
||||
|
||||
const gamesMap = new Map<number, Game>()
|
||||
|
||||
Object.keys(downloadedVersionsConfig.list).forEach(i => {
|
||||
const version = getVersionInfo(i)
|
||||
if (!version) return
|
||||
const game = getGameInfo(version.game)
|
||||
if (!game) return
|
||||
gamesMap.set(game.id, game)
|
||||
})
|
||||
|
||||
return Array.from(gamesMap.values())
|
||||
}
|
||||
|
||||
function getVersionsAmountData (gameId: number): {
|
||||
installed: number
|
||||
total: number
|
||||
} | null {
|
||||
if (!downloadedVersionsConfig || !serverVersionList) return null
|
||||
|
||||
const allowWine =
|
||||
platformName !== 'linux' || normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
|
||||
const installed = Object.keys(downloadedVersionsConfig.list).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
if (!info) return false
|
||||
if (info.wine && !allowWine) return false
|
||||
return getGameInfo(info.game)?.id === gameId
|
||||
}).length
|
||||
|
||||
const total = serverVersionList.versions.filter(v => {
|
||||
if (!v) return false
|
||||
if (v.wine && !allowWine) return false
|
||||
return getGameInfo(v.game)?.id === gameId
|
||||
}).length
|
||||
|
||||
return { installed, total }
|
||||
}
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
if (popupMode == 0 && selectedGame && pathname === '/') {
|
||||
setSelectedGame(null)
|
||||
setSelectedVersionList([])
|
||||
} else if (viewingInfoFromDownloads) {
|
||||
setViewingInfoFromDownloads(false)
|
||||
setPopupMode(0)
|
||||
} else {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}, [popupMode, selectedGame, pathname, viewingInfoFromDownloads])
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenProgress: (() => void) | null = null
|
||||
|
||||
listen<string>('download-size', event => {
|
||||
const [displayName, sizeStr] = event.payload.split(':')
|
||||
const size = Number(sizeStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
size
|
||||
}
|
||||
return copy
|
||||
})
|
||||
}).then(f => (unlistenProgress = f))
|
||||
|
||||
listen<string>('download-progress', async event => {
|
||||
const [displayName, progStr, totalSizeStr, speedStr, etaSecsStr] =
|
||||
event.payload.split(':')
|
||||
const prog = Number(progStr)
|
||||
const progBytes = Number(totalSizeStr)
|
||||
const speed = Number(speedStr)
|
||||
const etaSecs = Number(etaSecsStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
progress: prog,
|
||||
progressBytes: progBytes,
|
||||
speed,
|
||||
etaSecs
|
||||
}
|
||||
return copy
|
||||
})
|
||||
getCurrentWindow().setProgressBar({
|
||||
status: ProgressBarStatus.Normal,
|
||||
progress: Math.floor(prog)
|
||||
})
|
||||
}).then(f => (unlistenProgress = f))
|
||||
|
||||
listen<string>('download-hash-checking', event => {
|
||||
const displayName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], hash_checking: true }
|
||||
return copy
|
||||
})
|
||||
}).then(f => (unlistenProgress = f))
|
||||
|
||||
listen<string>('download-finishing', event => {
|
||||
const displayName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], hash_checking: false, finishing: true }
|
||||
return copy
|
||||
})
|
||||
}).then(f => (unlistenProgress = f))
|
||||
|
||||
return () => {
|
||||
unlistenProgress?.()
|
||||
}
|
||||
}, [downloadProgress])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
setPlatformName(platform())
|
||||
const client = await app.getVersion()
|
||||
setVersion(client)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://games.lncvrt.xyz/api/launcher/latest'
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.text()
|
||||
if (await verifySignature(data, signature)) {
|
||||
if (data !== client) {
|
||||
setOutdated(true)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
setLoadingText('Failed to check latest version.')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setLoadingText('Failed to check latest version.')
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (await verifySignature(JSON.stringify(data), signature)) {
|
||||
setServerVersionList(data)
|
||||
} else {
|
||||
setLoadingText('Failed to download versions list.')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setLoadingText('Failed to download versions list.')
|
||||
return
|
||||
}
|
||||
const normalConfig = await readNormalConfig()
|
||||
const versionsConfig = await readVersionsConfig()
|
||||
setDownloadedVersionsConfig(versionsConfig)
|
||||
setNormalConfig(normalConfig)
|
||||
setLoading(false)
|
||||
|
||||
if (!(await isPermissionGranted())) {
|
||||
await requestPermission()
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const downloadVersions = useCallback(
|
||||
async (list: string[]): Promise<void> => {
|
||||
if (list.length === 0) return
|
||||
setSelectedVersionList([])
|
||||
|
||||
const newVersions = list.filter(
|
||||
version =>
|
||||
!downloadQueue.includes(version) &&
|
||||
!downloadProgress.some(d => d.version === version)
|
||||
)
|
||||
|
||||
if (newVersions.length === 0) return
|
||||
|
||||
const newDownloads = newVersions.map(
|
||||
version =>
|
||||
new DownloadProgress(
|
||||
version,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
setDownloadProgress(prev => [...prev, ...newDownloads])
|
||||
setDownloadQueue(prev => [...prev, ...newVersions])
|
||||
},
|
||||
[downloadQueue, downloadProgress]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isProcessingQueue || downloadQueue.length === 0) return
|
||||
|
||||
const processNextDownload = async () => {
|
||||
setIsProcessingQueue(true)
|
||||
|
||||
const versionId = downloadQueue[0]
|
||||
const info = getVersionInfo(versionId)
|
||||
|
||||
if (!info) {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
return
|
||||
}
|
||||
|
||||
const gameInfo = getGameInfo(info.game)
|
||||
if (!gameInfo) {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
return
|
||||
}
|
||||
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d => (d.version === versionId ? { ...d, queued: false } : d))
|
||||
)
|
||||
|
||||
const downloadInfoRequest = await fetch(
|
||||
'https://games.lncvrt.xyz/api/launcher/download?gameId=' +
|
||||
info.id +
|
||||
'&downloadId=' +
|
||||
info.download
|
||||
)
|
||||
const signature = downloadInfoRequest.headers.get('x-signature') ?? ''
|
||||
const data = await downloadInfoRequest.json()
|
||||
if (
|
||||
!(await verifySignature(JSON.stringify(data), signature)) ||
|
||||
!data.success
|
||||
) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === versionId
|
||||
? { ...d, queued: false, failed: true, progress: 0 }
|
||||
: d
|
||||
)
|
||||
)
|
||||
if (normalConfig?.settings.allowNotifications)
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${info.displayName} has failed.`
|
||||
)
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Critical
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const res = await invoke<string>('download', {
|
||||
url: data.data.url,
|
||||
name: info.id,
|
||||
executable: info.executable,
|
||||
hash: data.data.hash
|
||||
})
|
||||
|
||||
if (res === '1') {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
|
||||
const updated = {
|
||||
...prev,
|
||||
list: {
|
||||
...prev.list,
|
||||
[versionId]: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
writeVersionsConfig(updated)
|
||||
return updated
|
||||
})
|
||||
} else {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === versionId
|
||||
? { ...d, queued: false, failed: true, progress: 0 }
|
||||
: d
|
||||
)
|
||||
)
|
||||
if (normalConfig?.settings.allowNotifications)
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${info.displayName} has failed.`
|
||||
)
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Critical
|
||||
)
|
||||
}
|
||||
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
}
|
||||
|
||||
processNextDownload()
|
||||
}, [
|
||||
downloadQueue,
|
||||
isProcessingQueue,
|
||||
getVersionInfo,
|
||||
getGameInfo,
|
||||
normalConfig
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
downloadQueue.length === 0 &&
|
||||
downloadProgress.length === 0 &&
|
||||
!isProcessingQueue &&
|
||||
previousQueueLength.current > 0 &&
|
||||
normalConfig?.settings.allowNotifications
|
||||
) {
|
||||
notifyUser('Downloads Finished', 'All downloads have finished.')
|
||||
setTimeout(() => closePopup(), 0)
|
||||
;(async () => {
|
||||
await getCurrentWindow().setProgressBar({
|
||||
status: ProgressBarStatus.None,
|
||||
progress: 0
|
||||
})
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Informational
|
||||
)
|
||||
})()
|
||||
}
|
||||
previousQueueLength.current = downloadQueue.length + downloadProgress.length
|
||||
}, [
|
||||
downloadQueue,
|
||||
downloadProgress,
|
||||
isProcessingQueue,
|
||||
normalConfig,
|
||||
closePopup
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (revisionCheck.current) return
|
||||
if (!serverVersionList || !downloadedVersionsConfig) return
|
||||
revisionCheck.current = true
|
||||
;(async () => {
|
||||
for (const [key, value] of Object.entries(
|
||||
downloadedVersionsConfig.list
|
||||
)) {
|
||||
const verInfo = serverVersionList.versions.find(item => item.id === key)
|
||||
|
||||
if (
|
||||
!verInfo ||
|
||||
(verInfo.lastRevision > 0 && value / 1000 <= verInfo.lastRevision)
|
||||
) {
|
||||
if (
|
||||
await exists('game/' + key + '/' + verInfo?.executable, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + key + '/' + verInfo?.executable, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [serverVersionList, downloadedVersionsConfig, downloadVersions])
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
<body
|
||||
className={
|
||||
normalConfig?.settings.theme === 1
|
||||
? 'red-theme'
|
||||
: normalConfig?.settings.theme === 2
|
||||
? 'blue-theme'
|
||||
: normalConfig?.settings.theme === 3
|
||||
? 'purple-theme'
|
||||
: 'dark-theme'
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div
|
||||
className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<div
|
||||
className={`w-screen ${
|
||||
platformName == 'windows'
|
||||
? 'h-[calc(100vh-64px)]'
|
||||
: 'h-screen'
|
||||
} flex items-center justify-center`}
|
||||
>
|
||||
{outdated ? (
|
||||
<div className='text-center'>
|
||||
<p className='text-8xl mb-4'>Outdated Launcher!</p>
|
||||
<p className='text-4xl mb-4'>
|
||||
Please update to the latest version to continue.
|
||||
</p>
|
||||
<button
|
||||
className='button'
|
||||
onClick={() =>
|
||||
openUrl('https://games.lncvrt.xyz/berrydash/download')
|
||||
}
|
||||
>
|
||||
Download latest version
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-7xl text-center'>{loadingText}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<GlobalProvider
|
||||
value={{
|
||||
serverVersionList,
|
||||
selectedVersionList,
|
||||
setSelectedVersionList,
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
popupMode,
|
||||
setPopupMode,
|
||||
fadeOut,
|
||||
setFadeOut,
|
||||
downloadedVersionsConfig,
|
||||
setDownloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setNormalConfig,
|
||||
managingVersion,
|
||||
setManagingVersion,
|
||||
getVersionInfo,
|
||||
getGameInfo,
|
||||
getListOfGames,
|
||||
setSelectedGame,
|
||||
getVersionsAmountData,
|
||||
viewingInfoFromDownloads,
|
||||
version,
|
||||
downloadVersions,
|
||||
category,
|
||||
setCategory,
|
||||
downloadQueue,
|
||||
setDownloadQueue,
|
||||
closePopup,
|
||||
getSpecialVersionsList,
|
||||
selectedGame,
|
||||
setViewingInfoFromDownloads
|
||||
}}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (showPopup && e.key === 'Escape') closePopup()
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div
|
||||
className='relative z-2 ml-59.75 w-[calc(100vw-239px)] border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<div className='relative z-0'>
|
||||
<main className='ml-60'>{children}</main>
|
||||
</div>
|
||||
{showPopup && (
|
||||
<div
|
||||
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
if (viewingInfoFromDownloads) {
|
||||
setPopupMode(0)
|
||||
setViewingInfoFromDownloads(false)
|
||||
setManagingVersion(null)
|
||||
setSelectedGame(null)
|
||||
}
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='popup-box'>
|
||||
<button
|
||||
className='close-button btntheme1'
|
||||
onClick={() => closePopup()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
(popupMode == 0 &&
|
||||
selectedGame &&
|
||||
pathname === '/') ||
|
||||
viewingInfoFromDownloads ||
|
||||
popupMode == 4
|
||||
? faChevronLeft
|
||||
: faXmark
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
{popupMode === 0 && selectedGame ? (
|
||||
<VersionsDownloadPopup />
|
||||
) : popupMode === 0 && !selectedGame ? (
|
||||
<GamesDownloadPopup />
|
||||
) : popupMode === 1 ? (
|
||||
<DownloadsPopup />
|
||||
) : popupMode === 2 ? (
|
||||
<VersionVersionPopup />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlobalProvider>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.downloads-container {
|
||||
@apply flex justify-center;
|
||||
}
|
||||
|
||||
.downloads-scroll {
|
||||
@apply bg-(--col1)/50 border border-(--col3)/50 rounded-lg overflow-y-auto w-full;
|
||||
}
|
||||
|
||||
.downloads-entry {
|
||||
@apply flex justify-between items-center m-2 p-2 rounded-lg text-gray-200 text-lg transition-colors bg-(--col2)/50 hover:bg-(--col3)/50 border border-(--col4)/50 hover:border-(--col5)/50 cursor-pointer;
|
||||
}
|
||||
+36
-31
@@ -1,14 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import './styles.css'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { BirdColor } from '@/types/BerryDash/BirdColor'
|
||||
import { GetIconForUser } from '@/lib/BerryDash'
|
||||
import { GetIconForUser } from '@/lib/berrydashutil'
|
||||
import Image from 'next/image'
|
||||
import './styles.css'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { verifySignature } from '@/lib/Util'
|
||||
import { verifySignature } from '@/lib/util'
|
||||
import Dropdown from '@/components/Dropdown'
|
||||
|
||||
interface BaseEntry {
|
||||
id: number
|
||||
@@ -85,10 +86,12 @@ export default function BerryDashLeaderboards () {
|
||||
const [selected, setSelected] = useState<number>(-1)
|
||||
const [selectedBerryOption, setSelectedBerryOption] = useState<number>(0)
|
||||
const [entries, setEntries] = useState<(LeaderboardEntry | Account)[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const Refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (selected == 3 || selected == 4) {
|
||||
const response = await fetch(
|
||||
@@ -145,6 +148,7 @@ export default function BerryDashLeaderboards () {
|
||||
} catch {
|
||||
setEntries([])
|
||||
}
|
||||
setLoading(false)
|
||||
}, [selected, selectedBerryOption])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -160,6 +164,25 @@ export default function BerryDashLeaderboards () {
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Berry Dash Leaderboards</p>
|
||||
<div className='flex gap-2'>
|
||||
{selected == 1 && (
|
||||
<div className='flex justify-center'>
|
||||
<Dropdown
|
||||
value={selectedBerryOption}
|
||||
options={[
|
||||
{ label: 'Normal Berry', value: 0 },
|
||||
{ label: 'Poison Berry', value: 1 },
|
||||
{ label: 'Slow Berry', value: 2 },
|
||||
{ label: 'Ultra Berry', value: 3 },
|
||||
{ label: 'Speedy Berry', value: 4 },
|
||||
{ label: 'Coin Berry', value: 5 },
|
||||
{ label: 'Random Berry', value: 6 },
|
||||
{ label: 'Anti Berry', value: 7 },
|
||||
{ label: 'Golden Berry', value: 8 }
|
||||
]}
|
||||
onChange={e => setSelectedBerryOption(Number(e))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
@@ -174,9 +197,10 @@ export default function BerryDashLeaderboards () {
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
if (selected == -1) router.push('/game?id=1')
|
||||
if (selected == -1) router.push('/main/game?id=1')
|
||||
else {
|
||||
setSelected(-1)
|
||||
setSelectedBerryOption(0)
|
||||
setEntries([])
|
||||
}
|
||||
}}
|
||||
@@ -241,15 +265,15 @@ export default function BerryDashLeaderboards () {
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : loading ? (
|
||||
<p className='flex justify-center items-center text-6xl h-full'>
|
||||
Loading
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col gap-2 overflow-y-auto ${
|
||||
selected == 1
|
||||
? platform() == 'windows'
|
||||
? 'h-[calc(100vh-168px)]'
|
||||
: 'h-[calc(100vh-136px)]'
|
||||
: platform() == 'windows'
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-128px)]'
|
||||
: 'h-[calc(100vh-96px)]'
|
||||
} px-1`}
|
||||
@@ -297,39 +321,20 @@ export default function BerryDashLeaderboards () {
|
||||
: selected === 2
|
||||
? 'Coins'
|
||||
: 'Score'}
|
||||
: {item.value.toLocaleString('en-US')}
|
||||
: {item.value.toLocaleString()}
|
||||
</p>
|
||||
) : isAccount ? (
|
||||
<p>
|
||||
{selected === 3 ? 'Level' : 'XP'}:{' '}
|
||||
{selected === 3
|
||||
? calculateLevel(item.xp).toLocaleString('en-US')
|
||||
: item.xp.toLocaleString('en-US')}
|
||||
? calculateLevel(item.xp).toLocaleString()
|
||||
: item.xp.toLocaleString()}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{selected == 1 && (
|
||||
<div className='flex justify-center'>
|
||||
<select
|
||||
value={selectedBerryOption}
|
||||
onChange={e => setSelectedBerryOption(Number(e.target.value))}
|
||||
className='leaderboard-select mt-0.75'
|
||||
>
|
||||
<option value='0'>Normal Berry</option>
|
||||
<option value='1'>Poison Berry</option>
|
||||
<option value='2'>Slow Berry</option>
|
||||
<option value='3'>Ultra Berry</option>
|
||||
<option value='4'>Speedy Berry</option>
|
||||
<option value='5'>Coin Berry</option>
|
||||
<option value='6'>Random Berry</option>
|
||||
<option value='7'>Anti Berry</option>
|
||||
<option value='8'>Golden Berry</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.box {
|
||||
@apply bg-(--col1)/50 border border-(--col3)/50 rounded-lg w-auto p-1;
|
||||
}
|
||||
|
||||
.leaderboard-button {
|
||||
@apply bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.leaderboard-entry {
|
||||
@apply rounded-lg text-gray-200 text-lg px-1 py-0.5 transition-colors bg-(--col2)/50 hover:bg-(--col3)/50 border border-(--col4)/50 hover:border-(--col5)/50 cursor-pointer;
|
||||
}
|
||||
@@ -1,36 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import '../Installs.css'
|
||||
import { useEffect } from 'react'
|
||||
import '@/app/Installs.css'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { faWarning } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { ask } from '@tauri-apps/plugin-dialog'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
import { writeVersionsConfig } from '@/lib/BazookaManager'
|
||||
import { openFolder } from '@/lib/Util'
|
||||
import { openFolder } from '@/lib/util'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
downloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setManagingVersion,
|
||||
getVersionInfo,
|
||||
getGameInfo,
|
||||
setSelectedGame,
|
||||
serverVersionList,
|
||||
category,
|
||||
setCategory,
|
||||
setDownloadedVersionsConfig,
|
||||
downloadVersions
|
||||
downloadVersions,
|
||||
downloadProgress,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
versions
|
||||
} = useGlobal()
|
||||
|
||||
const params = useSearchParams()
|
||||
@@ -42,7 +41,7 @@ export default function Installs () {
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [normalConfig, setSelectedVersionList, showPopup])
|
||||
}, [setSelectedVersionList, showPopup])
|
||||
|
||||
if (!id || !game) return <p>Invalid game</p>
|
||||
|
||||
@@ -53,14 +52,21 @@ export default function Installs () {
|
||||
if (!lastRevision) return false
|
||||
return (
|
||||
lastRevision > 0 &&
|
||||
(downloadedVersionsConfig == undefined
|
||||
? 0
|
||||
: downloadedVersionsConfig?.list[version]) /
|
||||
1000 <=
|
||||
(versionsList == undefined ? 0 : versionsList[version]) / 1000 <=
|
||||
lastRevision
|
||||
)
|
||||
}
|
||||
|
||||
const filteredVersions = Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList?.versions.find(vf => vf.id == v)
|
||||
if (!info) return false
|
||||
if (platform() == 'linux' && info.wine && !linuxUseWine) return false
|
||||
return (
|
||||
info.game === id &&
|
||||
(category == -1 ? info.category == -1 : info.category == category)
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
@@ -68,7 +74,7 @@ export default function Installs () {
|
||||
className={`text-3xl truncate ${
|
||||
category != -1
|
||||
? 'w-[calc(100vw-495px)]'
|
||||
: game.id == 1
|
||||
: game.id == 1 || game.id == 9
|
||||
? 'w-[calc(100vw-560px)]'
|
||||
: 'w-[calc(100vw-440px)]'
|
||||
}`}
|
||||
@@ -79,13 +85,29 @@ export default function Installs () {
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
router.push('/game/berrydash/leaderboards')
|
||||
router.push('/main/game/berrydash/leaderboards')
|
||||
}}
|
||||
title='View the leaderboards for this game.'
|
||||
hidden={game.id != 1}
|
||||
>
|
||||
Leaderboards
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() =>
|
||||
invoke('open_new_window', {
|
||||
title: 'XPS Dashboard',
|
||||
name: 'xpsdashboard',
|
||||
url: 'https://xps.lncvrt.xyz/dashboard/',
|
||||
width: 1280,
|
||||
height: 720
|
||||
})
|
||||
}
|
||||
title='Open the GDPS Dashboard!'
|
||||
hidden={game.id != 9 || category != -1}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
@@ -120,19 +142,14 @@ export default function Installs () {
|
||||
>
|
||||
{category == -1 &&
|
||||
Object.entries(game.categoryNames)
|
||||
.sort(([a], [b]) => Number(b) - Number(a))
|
||||
.filter(([key]) => {
|
||||
const count = Object.keys(
|
||||
downloadedVersionsConfig?.list ?? {}
|
||||
).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
const count = Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!info) return false
|
||||
|
||||
if (
|
||||
platform() == 'linux' &&
|
||||
info.wine &&
|
||||
!normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||
return false
|
||||
|
||||
return info.game === id && info.category === Number(key)
|
||||
@@ -140,6 +157,7 @@ export default function Installs () {
|
||||
|
||||
return count >= 1
|
||||
})
|
||||
.sort(([a], [b]) => Number(b) - Number(a))
|
||||
.map(([key, value]) => {
|
||||
return (
|
||||
<div
|
||||
@@ -159,16 +177,15 @@ export default function Installs () {
|
||||
<p>
|
||||
{(() => {
|
||||
const count =
|
||||
Object.keys(
|
||||
downloadedVersionsConfig?.list ?? []
|
||||
).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!info) return false
|
||||
if (
|
||||
platform() == 'linux' &&
|
||||
info.wine &&
|
||||
!normalConfig?.settings
|
||||
.useWineOnUnixWhenNeeded
|
||||
!linuxUseWine
|
||||
)
|
||||
return false
|
||||
return (
|
||||
@@ -184,48 +201,43 @@ export default function Installs () {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{Object.keys(downloadedVersionsConfig?.list ?? []).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
{filteredVersions.length === 0 &&
|
||||
(category !== -1 ||
|
||||
Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList?.versions.find(vf => vf.id == v)
|
||||
if (!info) return false
|
||||
|
||||
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||
return false
|
||||
|
||||
return info.game === id
|
||||
}).length != 0 ? (
|
||||
Object.keys(downloadedVersionsConfig?.list ?? [])
|
||||
}).length === 0) && (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No versions installed</p>
|
||||
</div>
|
||||
)}
|
||||
{filteredVersions
|
||||
.sort((a, b) => {
|
||||
const infoA = getVersionInfo(a)
|
||||
const infoB = getVersionInfo(b)
|
||||
if (!infoA || !infoB) return -1
|
||||
const infoA = serverVersionList?.versions.find(vf => vf.id == a)
|
||||
const infoB = serverVersionList?.versions.find(vf => vf.id == b)
|
||||
if (!infoA || !infoB) return 0
|
||||
return infoB.place - infoA.place
|
||||
})
|
||||
.filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
if (!info) return false
|
||||
if (
|
||||
platform() == 'linux' &&
|
||||
info.wine &&
|
||||
!normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
.map(v => {
|
||||
const versionInfo = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
return false
|
||||
if (!versionInfo) return
|
||||
|
||||
return (
|
||||
info.game === id &&
|
||||
(category == -1
|
||||
? info.category == -1
|
||||
: info.category == category)
|
||||
)
|
||||
})
|
||||
.map(entry => (
|
||||
<div
|
||||
key={entry}
|
||||
key={v}
|
||||
className={'downloads-entry'}
|
||||
title={
|
||||
'Click to launch game. Right-click to manage this version install'
|
||||
}
|
||||
onClick={async () => {
|
||||
if (
|
||||
needsRevisionUpdate(
|
||||
getVersionInfo(entry)?.lastRevision,
|
||||
entry
|
||||
)
|
||||
) {
|
||||
if (needsRevisionUpdate(versionInfo.lastRevision, v)) {
|
||||
const answer = await ask(
|
||||
'Before proceeding, if you do not want your installation directory wiped just yet, please backup the files to another directory. When you click "Yes", it will be wiped. Click "No" if you want to open the installation folder instead.',
|
||||
{
|
||||
@@ -249,78 +261,70 @@ export default function Installs () {
|
||||
setFadeOut(false)
|
||||
|
||||
//uninstall
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
const updatedList = Object.fromEntries(
|
||||
Object.entries(prev.list).filter(
|
||||
([k]) => k !== entry
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== v
|
||||
)
|
||||
)
|
||||
)
|
||||
const updatedConfig = {
|
||||
...prev,
|
||||
list: updatedList
|
||||
}
|
||||
writeVersionsConfig(updatedConfig)
|
||||
return updatedConfig
|
||||
})
|
||||
|
||||
if (
|
||||
await exists('game/' + entry, {
|
||||
await exists('game/' + v, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + entry, {
|
||||
await remove('game/' + v, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
|
||||
//reinstall
|
||||
setSelectedVersionList([entry])
|
||||
downloadVersions([entry])
|
||||
setSelectedVersionList([v])
|
||||
downloadVersions([
|
||||
{
|
||||
id: v,
|
||||
type: 0
|
||||
}
|
||||
])
|
||||
} else {
|
||||
openFolder(entry)
|
||||
openFolder(v)
|
||||
}
|
||||
return
|
||||
}
|
||||
const verInfo = getVersionInfo(entry)
|
||||
if (verInfo == undefined) return
|
||||
const gameInfo = getGameInfo(verInfo.game)
|
||||
if (gameInfo == undefined) return
|
||||
invoke('launch_game', {
|
||||
name: verInfo.id,
|
||||
executable: verInfo.executable,
|
||||
displayName: verInfo.displayName,
|
||||
name: versionInfo.id,
|
||||
executable: versionInfo.executable,
|
||||
displayName: versionInfo.displayName,
|
||||
useWine: !!(
|
||||
platform() == 'linux' &&
|
||||
verInfo.wine &&
|
||||
normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
versionInfo.wine &&
|
||||
linuxUseWine
|
||||
),
|
||||
wineCommand: normalConfig?.settings.wineOnUnixCommand
|
||||
wineCommand: linuxWineCommand
|
||||
})
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault()
|
||||
setManagingVersion(entry)
|
||||
setManagingVersion(v)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>
|
||||
{getVersionInfo(entry)?.displayName}{' '}
|
||||
</p>
|
||||
<p className='text-2xl'>{versionInfo.displayName}</p>
|
||||
|
||||
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
title='The date the game was installed.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
Installed{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
downloadedVersionsConfig?.list[entry]
|
||||
versionsList[v]
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -328,41 +332,65 @@ export default function Installs () {
|
||||
className='entry-info-item'
|
||||
title='This version is using wine. It cannot be guarenteed to work fully and might not work at all.'
|
||||
hidden={
|
||||
!(
|
||||
platform() == 'linux' && getVersionInfo(entry)?.wine
|
||||
) ||
|
||||
needsRevisionUpdate(
|
||||
getVersionInfo(entry)?.lastRevision,
|
||||
entry
|
||||
)
|
||||
!(platform() == 'linux' && versionInfo.wine) ||
|
||||
needsRevisionUpdate(versionInfo.lastRevision, v)
|
||||
}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||
<p>Uses wine</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
onClick={e => e.stopPropagation()}
|
||||
hidden={
|
||||
!needsRevisionUpdate(
|
||||
getVersionInfo(entry)?.lastRevision,
|
||||
entry
|
||||
)
|
||||
!needsRevisionUpdate(versionInfo.lastRevision, v)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||
<p>Needs revision update!</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className='absolute right-0 bottom-0 button'
|
||||
title='Click to manage mods for this game!'
|
||||
onClick={async e => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (
|
||||
!(await exists('game/' + v + '/BepInEx', {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}))
|
||||
) {
|
||||
if (
|
||||
(await ask(
|
||||
"You don't have BepInEx (the mod loader for Unity Games), would you like to install it now? It is about 1MB in size. If you choose yes, it will download the recommended BepInEx version for " +
|
||||
versionInfo.displayName +
|
||||
'.',
|
||||
{ title: 'BepInEx not found!', kind: 'error' }
|
||||
)) &&
|
||||
!downloadProgress.find(d => d.version == v)
|
||||
) {
|
||||
downloadVersions([
|
||||
{
|
||||
id: v,
|
||||
type: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
return
|
||||
}
|
||||
setManagingVersion(v)
|
||||
setPopupMode(3)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
hidden={!versionInfo.modSupportDownload}
|
||||
>
|
||||
Mod Manager
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No versions installed</p>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,841 @@
|
||||
'use client'
|
||||
|
||||
import '@/styles/globals.css'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import { DownloadProgress } from '@/types/DownloadProgress'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import { GlobalProvider } from '@/providers/GlobalProvider'
|
||||
import { Roboto } from 'next/font/google'
|
||||
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission
|
||||
} from '@tauri-apps/plugin-notification'
|
||||
import {
|
||||
BaseDirectory,
|
||||
copyFile,
|
||||
exists,
|
||||
readTextFile,
|
||||
remove
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { verifySignature } from '@/lib/util'
|
||||
import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window'
|
||||
import { Mod } from '@/types/Mod'
|
||||
import { load, Store } from '@tauri-apps/plugin-store'
|
||||
import { notifyUser } from '@/lib/notifications'
|
||||
import { GameVersion } from '@/types/GameVersion'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronLeft, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import VersionsDownloadPopup from '@/popups/VersionsDownload'
|
||||
import GamesDownloadPopup from '@/popups/GamesDownload'
|
||||
import DownloadsPopup from '@/popups/Downloads'
|
||||
import ManageVersionPopup from '@/popups/ManageVersion'
|
||||
import ModDownloadsPopup from '@/popups/ModDownloads'
|
||||
import VersionChangelogPopup from '@/popups/VersionChangelog'
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ['latin']
|
||||
})
|
||||
|
||||
export default function RootLayout ({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingText, setLoadingText] = useState('Loading...')
|
||||
const [version, setVersion] = useState<string | null>(null)
|
||||
const [platformName, setPlatformName] = useState<string | null>(null)
|
||||
|
||||
const [serverVersionList, setServerVersionList] =
|
||||
useState<null | ServerVersionsResponse>(null)
|
||||
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
|
||||
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
const [popupMode, setPopupMode] = useState<null | number>(null)
|
||||
const [fadeOut, setFadeOut] = useState(false)
|
||||
|
||||
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
||||
[]
|
||||
)
|
||||
const [downloadQueue, setDownloadQueue] = useState<string[]>([])
|
||||
const [isProcessingQueue, setIsProcessingQueue] = useState<boolean>(false)
|
||||
const [managingVersion, setManagingVersion] = useState<string | null>(null)
|
||||
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
|
||||
useState<boolean>(false)
|
||||
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
||||
|
||||
const [category, setCategory] = useState<number>(-1)
|
||||
const [showModInfo, setShowModInfo] = useState<Mod | null>(null)
|
||||
|
||||
const pathname = usePathname()
|
||||
const revisionCheck = useRef(false)
|
||||
|
||||
const [settings, setSettings] = useState<Store | null>(null)
|
||||
const [versions, setVersions] = useState<Store | null>(null)
|
||||
|
||||
const [notificationsAllowed, setNotificationsAllowed] =
|
||||
useState<boolean>(false)
|
||||
const [sidebarAlwaysShowGames, setSidebarAlwaysShowGames] =
|
||||
useState<boolean>(false)
|
||||
const [linuxUseWine, setLinuxUseWine] = useState<boolean>(false)
|
||||
const [linuxWineCommand, setLinuxWineCommand] =
|
||||
useState<string>('wine %path%')
|
||||
const [customDataLocation, setCustomDataLocation] = useState<string>('')
|
||||
|
||||
const [versionsList, setVersionsList] = useState<Record<string, number>>({})
|
||||
const [modsList, setModsList] = useState<
|
||||
Record<string, Record<string, number>>
|
||||
>({})
|
||||
const [movingData, setMovingData] = useState(false)
|
||||
|
||||
function getSpecialVersionsList (game?: number): GameVersion[] {
|
||||
if (!serverVersionList) return []
|
||||
|
||||
return serverVersionList.versions
|
||||
.filter(v => !Object.keys(versionsList).includes(v.id))
|
||||
.filter(v => {
|
||||
if (platformName == 'linux' && v.wine && !linuxUseWine) return false
|
||||
|
||||
if (game && v.game != game) return false
|
||||
if (category != -1 && v.category != category) return false
|
||||
if (downloadProgress.length != 0) {
|
||||
return !downloadProgress.some(d => d.version === v.id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.game !== a.game) return a.game - b.game
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
if (popupMode == 0 && selectedGame && pathname === '/main') {
|
||||
setSelectedGame(null)
|
||||
setSelectedVersionList([])
|
||||
} else if (viewingInfoFromDownloads) {
|
||||
setViewingInfoFromDownloads(false)
|
||||
setPopupMode(0)
|
||||
} else if (popupMode == 3 && showModInfo) {
|
||||
setShowModInfo(null)
|
||||
} else if (popupMode == 4) {
|
||||
setPopupMode(2)
|
||||
} else {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}, [popupMode, selectedGame, pathname, viewingInfoFromDownloads, showModInfo])
|
||||
|
||||
useEffect(() => {
|
||||
const unlisteners: (() => void)[] = []
|
||||
|
||||
const setupListeners = async () => {
|
||||
unlisteners.push(
|
||||
await listen<string>('download-size', event => {
|
||||
const [displayName, sizeStr] = event.payload.split(':')
|
||||
const size = Number(sizeStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], size }
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
unlisteners.push(
|
||||
await listen<string>('download-progress', async event => {
|
||||
const [displayName, progStr, totalSizeStr, speedStr, etaSecsStr] =
|
||||
event.payload.split(':')
|
||||
const prog = Number(progStr)
|
||||
const progBytes = Number(totalSizeStr)
|
||||
const speed = Number(speedStr)
|
||||
const etaSecs = Number(etaSecsStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
progress: prog,
|
||||
progressBytes: progBytes,
|
||||
speed,
|
||||
etaSecs
|
||||
}
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
unlisteners.push(
|
||||
await listen<string>('download-hash-checking', event => {
|
||||
const displayName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], hash_checking: true, downloading: false }
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
unlisteners.push(
|
||||
await listen<string>('unzip-start', event => {
|
||||
const displayName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], hash_checking: false, unzipping: true }
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
unlisteners.push(
|
||||
await listen<string>('unzip-progress', async event => {
|
||||
const [displayName, unzippedStr, unzipTotalStr] =
|
||||
event.payload.split(':')
|
||||
const unzipped = Number(unzippedStr)
|
||||
const unzipTotal = Number(unzipTotalStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
unzipped,
|
||||
unzipTotal
|
||||
}
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
unlisteners.push(
|
||||
await listen<string>('download-start', event => {
|
||||
const displayName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
downloading: true,
|
||||
paused: false,
|
||||
failed: false
|
||||
}
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
unlisteners.push(
|
||||
await listen<string>('download-stop', event => {
|
||||
const displayName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === displayName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], downloading: false }
|
||||
return copy
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
setupListeners()
|
||||
|
||||
return () => {
|
||||
unlisteners.forEach(unlisten => unlisten())
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
setPlatformName(platform())
|
||||
const client = await app.getVersion()
|
||||
setVersion(client)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (await verifySignature(JSON.stringify(data), signature)) {
|
||||
setServerVersionList(data)
|
||||
} else {
|
||||
setLoadingText('Failed to download versions list.')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setLoadingText('Failed to download versions list.')
|
||||
return
|
||||
}
|
||||
const legacyOptions = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
if (
|
||||
platform() == 'windows' &&
|
||||
(await exists('versions.json', legacyOptions))
|
||||
) {
|
||||
if (
|
||||
await exists('versions.json', {
|
||||
baseDir: BaseDirectory.AppConfig
|
||||
})
|
||||
)
|
||||
await remove('versions.json', {
|
||||
baseDir: BaseDirectory.AppConfig
|
||||
})
|
||||
|
||||
await copyFile('versions.json', 'versions.json', {
|
||||
fromPathBaseDir: BaseDirectory.AppLocalData,
|
||||
toPathBaseDir: BaseDirectory.AppConfig
|
||||
})
|
||||
await remove('versions.json', legacyOptions)
|
||||
}
|
||||
|
||||
const settingsLocal = await load('settings.json', {
|
||||
autoSave: true,
|
||||
defaults: {
|
||||
version: client,
|
||||
notificationsAllowed: true,
|
||||
sidebarAlwaysShowGames: true,
|
||||
linuxUseWine: false,
|
||||
linuxWineCommand: 'wine %path%',
|
||||
theme: 'dark',
|
||||
customDataLocation: ''
|
||||
}
|
||||
})
|
||||
const versionsLocal = await load('versions.json', {
|
||||
autoSave: true,
|
||||
defaults: {
|
||||
version: client,
|
||||
list: {},
|
||||
mods: {}
|
||||
}
|
||||
})
|
||||
settingsLocal.set('version', client)
|
||||
versionsLocal.set('version', client)
|
||||
if (await exists('config.json', legacyOptions)) {
|
||||
const config = await readTextFile('config.json', legacyOptions)
|
||||
const raw = JSON.parse(config)
|
||||
if (
|
||||
raw.settings &&
|
||||
raw.settings.theme &&
|
||||
(raw.version == '1.0.0' ||
|
||||
raw.version == '1.1.0' ||
|
||||
raw.version == '1.1.1' ||
|
||||
raw.version == '1.2.0' ||
|
||||
raw.version == '1.3.0' ||
|
||||
raw.version == '1.3.1' ||
|
||||
raw.version == '1.4.0' ||
|
||||
raw.version == '1.5.0' ||
|
||||
raw.version == '1.5.1' ||
|
||||
raw.version == '1.5.2' ||
|
||||
raw.version == '1.5.3' ||
|
||||
raw.version == '1.5.4')
|
||||
) {
|
||||
const parsed = Number(raw.settings.theme)
|
||||
if (parsed == 3) raw.settings.theme = 2
|
||||
if (parsed == 4) raw.settings.theme = 3
|
||||
else if (parsed != 0 && parsed != 1) raw.settings.theme = 0
|
||||
}
|
||||
if (raw.settings.allowNotifications)
|
||||
settingsLocal?.set(
|
||||
'notificationsAllowed',
|
||||
Boolean(raw.settings.allowNotifications)
|
||||
)
|
||||
if (raw.settings.alwaysShowGamesInSidebar)
|
||||
settingsLocal?.set(
|
||||
'sidebarAlwaysShowGames',
|
||||
Boolean(raw.settings.alwaysShowGamesInSidebar)
|
||||
)
|
||||
if (raw.settings.useWineOnUnixWhenNeeded)
|
||||
settingsLocal?.set(
|
||||
'linuxUseWine',
|
||||
Boolean(raw.settings.useWineOnUnixWhenNeeded)
|
||||
)
|
||||
if (raw.settings.wineOnUnixCommand)
|
||||
settingsLocal?.set(
|
||||
'linuxWineCommand',
|
||||
String(raw.settings.wineOnUnixCommand)
|
||||
)
|
||||
if (raw.settings.theme) {
|
||||
const tempTheme = (() => {
|
||||
switch (Number(raw.settings.theme)) {
|
||||
case 1:
|
||||
return 'red'
|
||||
case 2:
|
||||
return 'blue'
|
||||
case 3:
|
||||
return 'purple'
|
||||
default:
|
||||
return 'dark'
|
||||
}
|
||||
})()
|
||||
settingsLocal?.set('theme', tempTheme)
|
||||
}
|
||||
await remove('config.json', legacyOptions)
|
||||
}
|
||||
setSettings(settingsLocal)
|
||||
setVersions(versionsLocal)
|
||||
setLoading(false)
|
||||
|
||||
if (!(await isPermissionGranted())) {
|
||||
await requestPermission()
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const downloadVersions = useCallback(
|
||||
async (
|
||||
list: Array<{
|
||||
id: string
|
||||
type: 0 | 1 | 2
|
||||
modDownload?: number
|
||||
gameId?: number
|
||||
modId?: number
|
||||
modVersion?: string
|
||||
}>
|
||||
): Promise<void> => {
|
||||
if (list.length === 0) return
|
||||
setSelectedVersionList([])
|
||||
|
||||
const newVersions = list.filter(
|
||||
item =>
|
||||
!downloadQueue.includes(item.id) &&
|
||||
!downloadProgress.some(d => d.version === item.id)
|
||||
)
|
||||
|
||||
if (newVersions.length === 0) return
|
||||
|
||||
const newDownloads = newVersions.map(
|
||||
item =>
|
||||
new DownloadProgress(
|
||||
item.id,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
item.type,
|
||||
item.modDownload ?? null,
|
||||
item.gameId ?? null,
|
||||
item.modId ?? null,
|
||||
item.modVersion ?? null
|
||||
)
|
||||
)
|
||||
|
||||
setDownloadProgress(prev => [...prev, ...newDownloads])
|
||||
setDownloadQueue(prev => [...prev, ...newVersions.map(v => v.id)])
|
||||
},
|
||||
[downloadQueue, downloadProgress]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isProcessingQueue || downloadQueue.length === 0) return
|
||||
|
||||
const processNextDownload = async () => {
|
||||
setIsProcessingQueue(true)
|
||||
|
||||
const versionId = downloadQueue[0]
|
||||
const downloadInfo = downloadProgress.find(d => d.version == versionId)
|
||||
if (!downloadInfo) {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
return
|
||||
}
|
||||
const info = serverVersionList?.versions.find(vf => vf.id == versionId)
|
||||
|
||||
if (!info) {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
return
|
||||
}
|
||||
|
||||
const gameInfo = serverVersionList?.games.find(vf => vf.id == info.game)
|
||||
if (!gameInfo) {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
return
|
||||
}
|
||||
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d => (d.version === versionId ? { ...d, queued: false } : d))
|
||||
)
|
||||
|
||||
const downloadInfoRequest = await fetch(
|
||||
'https://games.lncvrt.xyz/api/launcher/download' +
|
||||
(downloadInfo.type == 0
|
||||
? '?versionId=' + info.id + '&downloadId=' + info.download
|
||||
: '?downloadId=' +
|
||||
(downloadInfo.type == 1
|
||||
? info.modSupportDownload
|
||||
: downloadInfo.modDownload)) +
|
||||
(downloadInfo.type == 2 ? '&modId=' + downloadInfo.modId : '')
|
||||
)
|
||||
const signature = downloadInfoRequest.headers.get('x-signature') ?? ''
|
||||
const data = await downloadInfoRequest.json()
|
||||
if (
|
||||
!(await verifySignature(JSON.stringify(data), signature)) ||
|
||||
!data.success
|
||||
) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === versionId
|
||||
? { ...d, queued: false, failed: true, progress: 0 }
|
||||
: d
|
||||
)
|
||||
)
|
||||
if (await settings?.get<boolean>('notificationsAllowed'))
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${info.displayName} has failed.`
|
||||
)
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Critical
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === versionId
|
||||
? {
|
||||
...d,
|
||||
url: data.data.url,
|
||||
executable: downloadInfo.type == 0 ? info.executable : null,
|
||||
hash: data.data.hash
|
||||
}
|
||||
: d
|
||||
)
|
||||
)
|
||||
|
||||
const res = await invoke<string>('download', {
|
||||
url: data.data.url,
|
||||
name: info.id,
|
||||
hash: data.data.hash,
|
||||
downloadType: downloadInfo.type,
|
||||
modId: String(downloadInfo.modId ?? '')
|
||||
})
|
||||
|
||||
if (res === '1') {
|
||||
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||
if (downloadInfo.type != 1) {
|
||||
if (downloadInfo.type == 2) {
|
||||
versions?.set('mods', {
|
||||
...modsList,
|
||||
[downloadInfo.modGame! + '-' + downloadInfo.modId!]: {
|
||||
[downloadInfo.modVersion!]: Date.now()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
versions?.set('list', {
|
||||
...versionsList,
|
||||
[versionId]: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (res == '0') {
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === info.id)
|
||||
if (i === -1) return prev
|
||||
if (prev[i].canceled) {
|
||||
return prev.filter((_, idx) => idx !== i)
|
||||
}
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
downloading: false,
|
||||
paused: true,
|
||||
failed: false
|
||||
}
|
||||
return copy
|
||||
})
|
||||
} else if (res == '-1') {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === versionId
|
||||
? { ...d, queued: false, failed: true, progress: 0 }
|
||||
: d
|
||||
)
|
||||
)
|
||||
if (await settings?.get<boolean>('notificationsAllowed'))
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${info.displayName} has failed.`
|
||||
)
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Critical
|
||||
)
|
||||
}
|
||||
|
||||
setDownloadQueue(prev => prev.slice(1))
|
||||
setIsProcessingQueue(false)
|
||||
}
|
||||
|
||||
processNextDownload()
|
||||
}, [
|
||||
downloadQueue,
|
||||
isProcessingQueue,
|
||||
downloadProgress,
|
||||
serverVersionList,
|
||||
settings,
|
||||
versions,
|
||||
versionsList,
|
||||
modsList
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (revisionCheck.current) return
|
||||
if (!serverVersionList) return
|
||||
revisionCheck.current = true
|
||||
;(async () => {
|
||||
for (const [key, value] of Object.entries(versionsList)) {
|
||||
const verInfo = serverVersionList.versions.find(item => item.id === key)
|
||||
|
||||
if (
|
||||
verInfo &&
|
||||
verInfo.lastRevision > 0 &&
|
||||
value / 1000 <= verInfo.lastRevision
|
||||
) {
|
||||
await invoke('remove_stale_executable', {
|
||||
version: key,
|
||||
executable: verInfo.executable
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [serverVersionList, versionsList])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
downloadProgress.length === 0 &&
|
||||
downloadQueue.length === 0 &&
|
||||
showPopup &&
|
||||
popupMode === 1
|
||||
) {
|
||||
setTimeout(async () => {
|
||||
if (notificationsAllowed) {
|
||||
await notifyUser('Downloads Complete', 'All downloads finished!')
|
||||
}
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Informational
|
||||
)
|
||||
closePopup()
|
||||
}, 0)
|
||||
}
|
||||
}, [
|
||||
downloadProgress,
|
||||
downloadQueue,
|
||||
showPopup,
|
||||
popupMode,
|
||||
closePopup,
|
||||
notificationsAllowed
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings || !versions) return
|
||||
const unlisteners: Promise<() => void>[] = []
|
||||
|
||||
function watchSettings<T> (key: string, fn: (v: T | undefined) => void) {
|
||||
settings!.get<T>(key).then(fn)
|
||||
unlisteners.push(settings!.onKeyChange<T>(key, fn))
|
||||
}
|
||||
|
||||
function watchVersions<T> (key: string, fn: (v: T | undefined) => void) {
|
||||
versions!.get<T>(key).then(fn)
|
||||
unlisteners.push(versions!.onKeyChange<T>(key, fn))
|
||||
}
|
||||
|
||||
watchSettings<boolean>('notificationsAllowed', v =>
|
||||
setNotificationsAllowed(v ?? true)
|
||||
)
|
||||
watchSettings<boolean>('sidebarAlwaysShowGames', v =>
|
||||
setSidebarAlwaysShowGames(v ?? true)
|
||||
)
|
||||
watchSettings<boolean>('linuxUseWine', v => setLinuxUseWine(v ?? false))
|
||||
watchSettings<string>('linuxWineCommand', v =>
|
||||
setLinuxWineCommand(v ?? 'wine %path%')
|
||||
)
|
||||
watchSettings<string>('customDataLocation', v =>
|
||||
setCustomDataLocation(v ?? '')
|
||||
)
|
||||
|
||||
watchVersions<Record<string, number>>('list', v => setVersionsList(v ?? {}))
|
||||
watchVersions<Record<string, Record<string, number>>>('mods', v =>
|
||||
setModsList(v ?? {})
|
||||
)
|
||||
|
||||
return () => {
|
||||
unlisteners.forEach(u => u.then(fn => fn()))
|
||||
}
|
||||
}, [settings, versions])
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
<body>
|
||||
{loading ? (
|
||||
<>
|
||||
<div
|
||||
className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<div
|
||||
className={`w-screen ${
|
||||
platformName == 'windows'
|
||||
? 'h-[calc(100vh-64px)]'
|
||||
: 'h-screen'
|
||||
} flex items-center justify-center`}
|
||||
>
|
||||
<p className='text-7xl text-center'>{loadingText}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<GlobalProvider
|
||||
value={{
|
||||
serverVersionList,
|
||||
selectedVersionList,
|
||||
setSelectedVersionList,
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
popupMode,
|
||||
setPopupMode,
|
||||
fadeOut,
|
||||
setFadeOut,
|
||||
managingVersion,
|
||||
setManagingVersion,
|
||||
setSelectedGame,
|
||||
viewingInfoFromDownloads,
|
||||
version,
|
||||
downloadVersions,
|
||||
category,
|
||||
setCategory,
|
||||
downloadQueue,
|
||||
setDownloadQueue,
|
||||
closePopup,
|
||||
selectedGame,
|
||||
setViewingInfoFromDownloads,
|
||||
showModInfo,
|
||||
setShowModInfo,
|
||||
getSpecialVersionsList,
|
||||
settings,
|
||||
versions,
|
||||
notificationsAllowed,
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
customDataLocation,
|
||||
versionsList,
|
||||
modsList,
|
||||
movingData,
|
||||
setMovingData
|
||||
}}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (showPopup && e.key === 'Escape') closePopup()
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div
|
||||
className='relative z-2 ml-59.75 w-[calc(100vw-239px)] border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<div className='relative z-0'>
|
||||
<main className='ml-60'>{children}</main>
|
||||
</div>
|
||||
{showPopup && (
|
||||
<div
|
||||
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
if (showModInfo) setShowModInfo(null)
|
||||
if (viewingInfoFromDownloads) {
|
||||
setPopupMode(0)
|
||||
setViewingInfoFromDownloads(false)
|
||||
setManagingVersion(null)
|
||||
setSelectedGame(null)
|
||||
}
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='popup-box'>
|
||||
<button
|
||||
className='popup-top-button left-2 btntheme1'
|
||||
onClick={() => closePopup()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
(popupMode == 0 &&
|
||||
selectedGame &&
|
||||
pathname === '/') ||
|
||||
viewingInfoFromDownloads ||
|
||||
(popupMode == 3 && showModInfo) ||
|
||||
popupMode == 4
|
||||
? faChevronLeft
|
||||
: faXmark
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
{popupMode === 0 && selectedGame ? (
|
||||
<VersionsDownloadPopup />
|
||||
) : popupMode === 0 && !selectedGame ? (
|
||||
<GamesDownloadPopup />
|
||||
) : popupMode === 1 ? (
|
||||
<DownloadsPopup />
|
||||
) : popupMode === 2 ? (
|
||||
<ManageVersionPopup />
|
||||
) : popupMode === 3 ? (
|
||||
<ModDownloadsPopup />
|
||||
) : popupMode === 4 ? (
|
||||
<VersionChangelogPopup />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{movingData && (
|
||||
<div className='fixed inset-0 z-999999 bg-(--col0) flex items-center justify-center'>
|
||||
<p className='text-5xl text-center'>Moving data...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlobalProvider>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
'use client'
|
||||
|
||||
import './Installs.css'
|
||||
import { useEffect } from 'react'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCheck,
|
||||
faShieldHalved,
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
setSelectedGame,
|
||||
setCategory
|
||||
} = useGlobal()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [setSelectedVersionList, showPopup])
|
||||
|
||||
const filteredGames =
|
||||
serverVersionList?.games.filter(g =>
|
||||
serverVersionList.versions
|
||||
.filter(v => v.game === g.id)
|
||||
.some(v => Object.keys(versionsList).includes(v.id))
|
||||
) ?? []
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Games</p>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setSelectedGame(null)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to download more games.'
|
||||
>
|
||||
Download game
|
||||
</button>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div
|
||||
className={`downloads-scroll ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{filteredGames.length == 0 ? (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No games installed</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredGames.map(i => (
|
||||
<div
|
||||
key={i.id}
|
||||
className={'downloads-entry'}
|
||||
title={'Click to view game installs'}
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
router.push('/main/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{i.name}</p>
|
||||
|
||||
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const gameVersions = (
|
||||
serverVersionList?.versions ?? []
|
||||
).filter(v => v.game === i.id)
|
||||
const installed = gameVersions.filter(v =>
|
||||
Object.keys(versionsList).includes(v.id)
|
||||
).length
|
||||
return gameVersions.length
|
||||
? `${installed}/${gameVersions.length}`
|
||||
: 'N/A'
|
||||
})()}{' '}
|
||||
versions installed
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={!i.official}
|
||||
title='This game is official.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={i.official}
|
||||
title={
|
||||
i.verified
|
||||
? 'This game is verified to be safe'
|
||||
: 'This game is not verified to be save. Proceed with caution.'
|
||||
}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={i.verified ? faShieldHalved : faWarning}
|
||||
color={i.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
import { Setting } from '@/components/Setting'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { copyToClipboard } from '@/lib/clipboard'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export default function Settings () {
|
||||
const {
|
||||
settings,
|
||||
version,
|
||||
notificationsAllowed,
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
customDataLocation,
|
||||
downloadProgress,
|
||||
setMovingData
|
||||
} = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||
<div className='ml-4 mt-4 bg-(--col1)/50 border border-(--col3)/50 rounded-lg p-4 w-fit h-fit'>
|
||||
<Setting
|
||||
label='Allow sending notifications'
|
||||
value={notificationsAllowed}
|
||||
onChange={async () => {
|
||||
await settings?.set('notificationsAllowed', !notificationsAllowed)
|
||||
}}
|
||||
title='This setting does as you expect, allow the launcher to send notifications for when stuff like downloading is done.'
|
||||
/>
|
||||
<Setting
|
||||
label='Always show games in sidebar'
|
||||
value={sidebarAlwaysShowGames}
|
||||
onChange={async () => {
|
||||
await settings?.set(
|
||||
'sidebarAlwaysShowGames',
|
||||
!sidebarAlwaysShowGames
|
||||
)
|
||||
}}
|
||||
title="This setting will make it so when you are on a page like this, the games won't disappear."
|
||||
/>
|
||||
<Setting
|
||||
label='Use wine if needed'
|
||||
value={linuxUseWine}
|
||||
onChange={async () => {
|
||||
await settings?.set('linuxUseWine', !linuxUseWine)
|
||||
}}
|
||||
className={platform() == 'linux' ? '' : 'hidden'}
|
||||
/>
|
||||
<p hidden={!(platform() == 'linux' && linuxUseWine)}>Wine Command:</p>
|
||||
<input
|
||||
type='text'
|
||||
value={linuxWineCommand}
|
||||
onChange={async e =>
|
||||
await settings?.set('linuxWineCommand', e.target.value)
|
||||
}
|
||||
className='input-field my-1'
|
||||
hidden={!(platform() == 'linux' && linuxUseWine)}
|
||||
></input>
|
||||
<Setting
|
||||
label='Use custom data location'
|
||||
value={!!customDataLocation}
|
||||
disabled={downloadProgress.length > 0}
|
||||
onChange={async () => {
|
||||
if (customDataLocation) {
|
||||
try {
|
||||
setMovingData(true)
|
||||
await invoke('move_game_data', { destination: '' })
|
||||
await settings?.set('customDataLocation', '')
|
||||
await settings?.save()
|
||||
await invoke('restart_app')
|
||||
} catch {
|
||||
setMovingData(false)
|
||||
}
|
||||
} else {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
title: 'Select data location'
|
||||
})
|
||||
if (!selected || typeof selected !== 'string') return
|
||||
try {
|
||||
setMovingData(true)
|
||||
await invoke('move_game_data', { destination: selected })
|
||||
await settings?.set('customDataLocation', selected)
|
||||
await settings?.save()
|
||||
await invoke('restart_app')
|
||||
} catch {
|
||||
setMovingData(false)
|
||||
}
|
||||
}
|
||||
}}
|
||||
title='Move game data to a custom folder location. Disabled during downloads.'
|
||||
/>
|
||||
{customDataLocation && (
|
||||
<p className='text-sm opacity-50 ml-6 -mt-1 mb-2'>
|
||||
{customDataLocation}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
||||
onClick={async () =>
|
||||
await copyToClipboard(`v${version}`, notificationsAllowed)
|
||||
}
|
||||
title='The current launcher version.'
|
||||
>
|
||||
v{version}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import './Installs.css'
|
||||
import { useGlobal } from './GlobalProvider'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCheck,
|
||||
faShieldHalved,
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
downloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setSelectedGame,
|
||||
getListOfGames,
|
||||
getVersionsAmountData,
|
||||
setCategory
|
||||
} = useGlobal()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [normalConfig, setSelectedVersionList, showPopup])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Games</p>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setSelectedGame(null)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to download more games.'
|
||||
>
|
||||
Download game
|
||||
</button>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div
|
||||
className={`downloads-scroll ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{downloadedVersionsConfig &&
|
||||
Object.keys(downloadedVersionsConfig.list).length ? (
|
||||
getListOfGames()
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
.map(i => (
|
||||
<div
|
||||
key={i.id}
|
||||
className={'downloads-entry'}
|
||||
title={'Click to view game installs'}
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
router.push('/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{i.name}</p>
|
||||
|
||||
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const data = getVersionsAmountData(i.id)
|
||||
if (!data) return 'N/A'
|
||||
return `${data.installed}/${data.total}`
|
||||
})()}{' '}
|
||||
versions installed
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={!i.official}
|
||||
title='This game is official.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={i.official}
|
||||
title={
|
||||
i.verified
|
||||
? 'This game is verified to be safe'
|
||||
: 'This game is not verified to be save. Proceed with caution.'
|
||||
}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={i.verified ? faShieldHalved : faWarning}
|
||||
color={i.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No games installed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Setting } from '@/componets/Setting'
|
||||
import { writeNormalConfig } from '@/lib/BazookaManager'
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import { copyToClipboard } from '@/lib/Clipboard'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
|
||||
export default function Settings () {
|
||||
const [allowNotifications, setAllowNotifications] = useState(false)
|
||||
const [alwaysShowGamesInSidebar, setAlwaysShowGamesInSidebar] =
|
||||
useState(false)
|
||||
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
|
||||
const [wineOnUnixCommand, setWineOnUnixCommand] = useState('wine %path%')
|
||||
const [theme, setTheme] = useState(0)
|
||||
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const { normalConfig, setNormalConfig, version } = useGlobal()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
while (normalConfig != null) {
|
||||
setAllowNotifications(normalConfig.settings.allowNotifications)
|
||||
setAlwaysShowGamesInSidebar(
|
||||
normalConfig.settings.alwaysShowGamesInSidebar
|
||||
)
|
||||
setUseWineOnUnixWhenNeeded(
|
||||
normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
setWineOnUnixCommand(normalConfig.settings.wineOnUnixCommand)
|
||||
setTheme(normalConfig.settings.theme)
|
||||
setLoaded(true)
|
||||
break
|
||||
}
|
||||
})()
|
||||
}, [normalConfig])
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||
{loaded && (
|
||||
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||
<Setting
|
||||
label='Allow sending notifications'
|
||||
value={allowNotifications}
|
||||
onChange={async () => {
|
||||
if (!normalConfig) return
|
||||
setAllowNotifications(!allowNotifications)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
allowNotifications: !allowNotifications
|
||||
}
|
||||
})
|
||||
await writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
allowNotifications: !allowNotifications
|
||||
}
|
||||
})
|
||||
}}
|
||||
title='This setting does as you expect, allow the launcher to send notifications for when stuff like downloading is done.'
|
||||
/>
|
||||
<Setting
|
||||
label='Always show games in sidebar'
|
||||
value={alwaysShowGamesInSidebar}
|
||||
onChange={async () => {
|
||||
if (!normalConfig) return
|
||||
setAlwaysShowGamesInSidebar(!alwaysShowGamesInSidebar)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
|
||||
}
|
||||
})
|
||||
await writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
|
||||
}
|
||||
})
|
||||
}}
|
||||
title="This setting will make it so when you are on a page like this, the games won't disappear."
|
||||
/>
|
||||
<Setting
|
||||
label='Use wine when needed to launch games'
|
||||
value={useWineOnUnixWhenNeeded}
|
||||
onChange={async () => {
|
||||
if (!normalConfig) return
|
||||
setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
useWineOnUnixWhenNeeded: !useWineOnUnixWhenNeeded
|
||||
}
|
||||
})
|
||||
await writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
useWineOnUnixWhenNeeded: !useWineOnUnixWhenNeeded
|
||||
}
|
||||
})
|
||||
}}
|
||||
className={platform() == 'linux' ? '' : 'hidden'}
|
||||
/>
|
||||
<p hidden={!(platform() == 'linux' && useWineOnUnixWhenNeeded)}>
|
||||
Wine Command:
|
||||
</p>
|
||||
<input
|
||||
type='text'
|
||||
value={wineOnUnixCommand}
|
||||
onChange={async e => {
|
||||
if (!normalConfig) return
|
||||
setWineOnUnixCommand(e.target.value)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
wineOnUnixCommand: e.target.value
|
||||
}
|
||||
})
|
||||
await writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
wineOnUnixCommand: e.target.value
|
||||
}
|
||||
})
|
||||
}}
|
||||
className={`input-field my-1 ${
|
||||
platform() == 'linux' && useWineOnUnixWhenNeeded ? '' : 'hidden'
|
||||
}`}
|
||||
></input>
|
||||
<div title='The theme you want the launcher to use.'>
|
||||
<label className='text-lg'>Theme:</label>
|
||||
<select
|
||||
className='ml-2 bg-(--col2) border border-(--col4) rounded-md'
|
||||
value={theme}
|
||||
onChange={async e => {
|
||||
if (!normalConfig) return
|
||||
const newTheme = parseInt(e.target.value)
|
||||
setTheme(newTheme)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
theme: newTheme
|
||||
}
|
||||
})
|
||||
await writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
theme: newTheme
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option value={0}>Dark (default)</option>
|
||||
<option value={1}>Red</option>
|
||||
<option value={2}>Blue</option>
|
||||
<option value={3}>Purple</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
||||
onClick={async () => {
|
||||
await copyToClipboard(`v${version}`, normalConfig)
|
||||
}}
|
||||
title='The current launcher version.'
|
||||
>
|
||||
v{version}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import '@/styles/dropdown.css'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
type Option = {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: string | number
|
||||
options: Option[]
|
||||
onChange: (value: string | number) => void | Promise<void>
|
||||
}
|
||||
|
||||
export default function Dropdown ({ value, options, onChange }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const selected = options.find(o => o.value === value)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (!ref.current?.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousedown', handleClick)
|
||||
return () => window.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={ref} className='dropdown'>
|
||||
<button className='dropdown-btn' onClick={() => setOpen(o => !o)}>
|
||||
<span>{selected?.label ?? 'Select...'}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className={`dropdown-arrow ${open ? 'open' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<div className={`dropdown-menu ${open ? 'open' : ''}`}>
|
||||
{options.map((option, i) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`dropdown-item ${
|
||||
option.value === value ? 'active' : ''
|
||||
}`}
|
||||
style={{ ['--i']: i } as React.CSSProperties}
|
||||
onClick={async () => {
|
||||
await onChange(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
import './Setting.css'
|
||||
import '@/styles/setting.css'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { SettingProps } from '@/types/SettingProps'
|
||||
@@ -8,16 +8,18 @@ export function Setting ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
title
|
||||
title,
|
||||
disabled
|
||||
}: SettingProps) {
|
||||
return (
|
||||
<div className={`flex items-center gap-2 mb-2 ${className}`} title={title}>
|
||||
<div className={`flex items-center gap-2 mb-2 ${disabled ? 'opacity-50 pointer-events-none' : ''} ${className}`} title={title}>
|
||||
<label className='text-white text-lg'>{label}</label>
|
||||
<div className='setting-checkbox-wrapper'>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='setting-checkbox'
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!value)}
|
||||
/>
|
||||
{value && <FontAwesomeIcon icon={faCheck} className='fa-check-icon' />}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import './Sidebar.css'
|
||||
import '@/styles/sidebar.css'
|
||||
import Icon from '@/assets/Icon.png'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
@@ -27,16 +27,16 @@ const lexend = Lexend({
|
||||
|
||||
export default function Sidebar () {
|
||||
const {
|
||||
normalConfig,
|
||||
getListOfGames,
|
||||
serverVersionList,
|
||||
versionsList,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
downloadProgress,
|
||||
downloadedVersionsConfig,
|
||||
getVersionInfo,
|
||||
category,
|
||||
setCategory
|
||||
setCategory,
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine
|
||||
} = useGlobal()
|
||||
|
||||
const pathname = usePathname()
|
||||
@@ -59,10 +59,11 @@ export default function Sidebar () {
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className={`flex items-center h-10 w-60 ${
|
||||
className={
|
||||
'flex items-center h-10 w-60 ' +
|
||||
(platform() == 'windows' ? 'pl-1 pt-1' : 'pl-2 pt-2') +
|
||||
(platform() == 'macos' ? ' mt-7' : '')
|
||||
}`}
|
||||
}
|
||||
onMouseDown={e => {
|
||||
if (platform() != 'macos') return
|
||||
if (e.buttons === 1) {
|
||||
@@ -82,37 +83,42 @@ export default function Sidebar () {
|
||||
<nav className='nav-links overflow-auto pt-2'>
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/'
|
||||
href='/main'
|
||||
className={`link relative flex items-center ${
|
||||
pathname === '/' || pathname.startsWith('/game') ? 'active' : ''
|
||||
pathname === '/main' || pathname.startsWith('/main/game')
|
||||
? 'active'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Games
|
||||
</Link>
|
||||
{getListOfGames()
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
{serverVersionList?.games
|
||||
.filter(g =>
|
||||
serverVersionList.versions
|
||||
.filter(v => v.game === g.id)
|
||||
.some(v => Object.keys(versionsList).includes(v.id))
|
||||
)
|
||||
.map(i => (
|
||||
<React.Fragment key={i.id}>
|
||||
<div
|
||||
draggable={false}
|
||||
className={`link ${
|
||||
(pathname === '/game' &&
|
||||
(pathname === '/main/game' &&
|
||||
Number(params.get('id') || 0) == i.id) ||
|
||||
(i.id == 1 && pathname === '/game/berrydash/leaderboards')
|
||||
(i.id == 1 &&
|
||||
pathname === '/main/game/berrydash/leaderboards')
|
||||
? 'active'
|
||||
: ''
|
||||
} ml-auto w-50 ${
|
||||
normalConfig?.settings.alwaysShowGamesInSidebar ||
|
||||
sidebarAlwaysShowGames ||
|
||||
pathname === '/' ||
|
||||
pathname.startsWith('/game')
|
||||
pathname.startsWith('/main/game')
|
||||
? ''
|
||||
: 'hidden'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
router.push('/game?id=' + i.id)
|
||||
router.push('/main/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
@@ -130,17 +136,13 @@ export default function Sidebar () {
|
||||
{Object.entries(i.categoryNames)
|
||||
.sort(([a], [b]) => Number(b) - Number(a))
|
||||
.filter(([key]) => {
|
||||
const count = Object.keys(
|
||||
downloadedVersionsConfig?.list ?? {}
|
||||
).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
const count = Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!info) return false
|
||||
|
||||
if (
|
||||
platform() == 'linux' &&
|
||||
info.wine &&
|
||||
!normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||
return false
|
||||
|
||||
return info.game === i.id && info.category === Number(key)
|
||||
@@ -153,23 +155,23 @@ export default function Sidebar () {
|
||||
key={`${i.id}-${key}`}
|
||||
draggable={false}
|
||||
className={`link ${
|
||||
(pathname === '/game' &&
|
||||
((pathname === '/main/game' &&
|
||||
Number(params.get('id') || 0) == i.id) ||
|
||||
(i.id == 1 &&
|
||||
pathname === '/game/berrydash/leaderboards' &&
|
||||
category == Number(key))
|
||||
pathname === '/main/game/berrydash/leaderboards')) &&
|
||||
category == Number(key)
|
||||
? 'active'
|
||||
: ''
|
||||
} ml-auto w-47.5 ${
|
||||
normalConfig?.settings.alwaysShowGamesInSidebar ||
|
||||
sidebarAlwaysShowGames ||
|
||||
pathname === '/' ||
|
||||
pathname.startsWith('/game')
|
||||
pathname.startsWith('/main/game')
|
||||
? ''
|
||||
: 'hidden'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCategory(Number(key))
|
||||
router.push('/game?id=' + i.id)
|
||||
router.push('/main/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
@@ -182,8 +184,8 @@ export default function Sidebar () {
|
||||
))}
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/settings'
|
||||
className={`link ${pathname === '/settings' ? 'active' : ''}`}
|
||||
href='/main/settings'
|
||||
className={`link ${pathname === '/main/settings' ? 'active' : ''}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
|
||||
</Link>
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import ProgressBar from '../ProgressBar'
|
||||
import { formatEtaSmart } from '@/lib/Util'
|
||||
|
||||
export default function DownloadsPopup () {
|
||||
const {
|
||||
downloadProgress,
|
||||
getVersionInfo,
|
||||
setDownloadProgress,
|
||||
downloadQueue,
|
||||
setDownloadQueue
|
||||
} = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>Downloads</p>
|
||||
<div className='popup-content'>
|
||||
{downloadProgress.map((v, i) => {
|
||||
const queuePosition = downloadQueue.indexOf(v.version)
|
||||
return (
|
||||
<div key={i} className='popup-entry flex flex-col justify-between'>
|
||||
<p className='text-2xl text-center'>
|
||||
{getVersionInfo(v.version)?.displayName}
|
||||
</p>
|
||||
<div className='mt-6.25 flex items-center justify-between'>
|
||||
{v.failed || v.queued ? (
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<span
|
||||
className={`${
|
||||
v.failed ? 'text-red-500' : 'text-yellow-300'
|
||||
} inline-block text-center flex-1`}
|
||||
>
|
||||
{v.failed
|
||||
? 'Download failed'
|
||||
: queuePosition === 0
|
||||
? 'Starting soon...'
|
||||
: `Queued (Position ${queuePosition + 1})`}
|
||||
</span>
|
||||
<button
|
||||
className='button btntheme3 -ml-1.25'
|
||||
onClick={() => {
|
||||
setDownloadQueue(prev =>
|
||||
prev.filter(id => id !== v.version)
|
||||
)
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== v.version)
|
||||
)
|
||||
}}
|
||||
title='Click to remove this version from the download queue.'
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
) : v.hash_checking || v.finishing ? (
|
||||
<span
|
||||
className={`${
|
||||
v.hash_checking ? 'text-blue-300' : 'text-green-300'
|
||||
} inline-block w-full text-center`}
|
||||
>
|
||||
{v.hash_checking ? 'Verifying file integerty' : 'Finishing'}
|
||||
...
|
||||
</span>
|
||||
) : (
|
||||
<div className='flex flex-col gap-1 w-full'>
|
||||
<span className='text-center'>
|
||||
Downloaded{' '}
|
||||
{prettyBytes(v.progressBytes, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})}{' '}
|
||||
of{' '}
|
||||
{prettyBytes(v.size, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})}{' '}
|
||||
(ETA: {formatEtaSmart(v.etaSecs)} • Speed:{' '}
|
||||
{prettyBytes(v.speed, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})}
|
||||
/s)
|
||||
</span>
|
||||
<ProgressBar progress={v.progress} className='w-full' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { app } from '@tauri-apps/api'
|
||||
import { NormalConfig } from '@/types/NormalConfig'
|
||||
import {
|
||||
BaseDirectory,
|
||||
create,
|
||||
exists,
|
||||
mkdir,
|
||||
readTextFile,
|
||||
writeFile
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { VersionsConfig } from '@/types/VersionsConfig'
|
||||
|
||||
export async function readNormalConfig (): Promise<NormalConfig> {
|
||||
const version = await app.getVersion()
|
||||
try {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('config.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('config.json', options)
|
||||
await file.write(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify(new NormalConfig(version), null, 2)
|
||||
)
|
||||
)
|
||||
await file.close()
|
||||
return new NormalConfig(version)
|
||||
}
|
||||
const config = await readTextFile('config.json', options)
|
||||
const raw = JSON.parse(config)
|
||||
if (
|
||||
raw.settings &&
|
||||
raw.settings.theme &&
|
||||
(raw.version == '1.0.0' ||
|
||||
raw.version == '1.1.0' ||
|
||||
raw.version == '1.1.1' ||
|
||||
raw.version == '1.2.0' ||
|
||||
raw.version == '1.3.0' ||
|
||||
raw.version == '1.3.1' ||
|
||||
raw.version == '1.4.0' ||
|
||||
raw.version == '1.5.0' ||
|
||||
raw.version == '1.5.1' ||
|
||||
raw.version == '1.5.2' ||
|
||||
raw.version == '1.5.3' ||
|
||||
raw.version == '1.5.4')
|
||||
) {
|
||||
const parsed = Number(raw.settings.theme)
|
||||
if (parsed == 3) raw.settings.theme = 2
|
||||
if (parsed == 4) raw.settings.theme = 3
|
||||
else if (parsed != 0 && parsed != 1) raw.settings.theme = 0
|
||||
}
|
||||
raw.version = version
|
||||
writeNormalConfig(raw)
|
||||
return NormalConfig.import(raw)
|
||||
} catch {
|
||||
return new NormalConfig(version)
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeNormalConfig (data: NormalConfig) {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('config.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('config.json', options)
|
||||
await file.write(new TextEncoder().encode(JSON.stringify(data, null, 2)))
|
||||
await file.close()
|
||||
} else {
|
||||
await writeFile(
|
||||
'config.json',
|
||||
new TextEncoder().encode(JSON.stringify(data, null, 2)),
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function readVersionsConfig (): Promise<VersionsConfig> {
|
||||
const version = await app.getVersion()
|
||||
try {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('versions.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('versions.json', options)
|
||||
await file.write(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify(new VersionsConfig(version), null, 2)
|
||||
)
|
||||
)
|
||||
await file.close()
|
||||
return new VersionsConfig(version)
|
||||
}
|
||||
const config = await readTextFile('versions.json', options)
|
||||
const raw = JSON.parse(config)
|
||||
if (raw.list && raw.timestamps) {
|
||||
raw.list = raw.timestamps
|
||||
delete raw.timestamps
|
||||
|
||||
await writeFile(
|
||||
'versions.json',
|
||||
new TextEncoder().encode(JSON.stringify(raw, null, 2)),
|
||||
options
|
||||
)
|
||||
}
|
||||
raw.version = version
|
||||
writeVersionsConfig(raw)
|
||||
return VersionsConfig.import(raw)
|
||||
} catch {
|
||||
return new VersionsConfig(version)
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeVersionsConfig (data: VersionsConfig) {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('versions.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('versions.json', options)
|
||||
await file.write(new TextEncoder().encode(JSON.stringify(data, null, 2)))
|
||||
await file.close()
|
||||
} else {
|
||||
await writeFile(
|
||||
'versions.json',
|
||||
new TextEncoder().encode(JSON.stringify(data, null, 2)),
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { NormalConfig } from '@/types/NormalConfig'
|
||||
import { notifyUser } from './Notifications'
|
||||
|
||||
export async function copyToClipboard (
|
||||
text: string,
|
||||
normalConfig: NormalConfig | null
|
||||
) {
|
||||
if (normalConfig?.settings.allowNotifications)
|
||||
await notifyUser('Copied', 'Text "' + text + '" copied to clipboard')
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,11 @@
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { notifyUser } from './notifications'
|
||||
|
||||
export async function copyToClipboard (
|
||||
text: string,
|
||||
notificationsAllowed: boolean
|
||||
) {
|
||||
await writeText(text)
|
||||
if (notificationsAllowed)
|
||||
await notifyUser('Copied', 'Text "' + text + '" copied to clipboard')
|
||||
}
|
||||
File renamed without changes.
@@ -1,39 +1,15 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { BaseDirectory, exists, stat } from '@tauri-apps/plugin-fs'
|
||||
import { openPath } from '@tauri-apps/plugin-opener'
|
||||
import { join, appLocalDataDir } from '@tauri-apps/api/path'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export const openFolder = async (name: string) => {
|
||||
const relativePath = await join('game', name)
|
||||
const basePath = await appLocalDataDir()
|
||||
const absolutePath = await join(basePath, relativePath)
|
||||
|
||||
const folderExists = await exists(relativePath, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
|
||||
if (!folderExists) {
|
||||
await message(`Game folder "${absolutePath}" not found.`, {
|
||||
try {
|
||||
await invoke('open_game_folder', { version: name })
|
||||
} catch (e) {
|
||||
await message(String(e), {
|
||||
title: 'Folder not found',
|
||||
kind: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const folderStat = await stat(relativePath, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
|
||||
if (folderStat.isFile) {
|
||||
await message(`Game folder "${absolutePath}" not found.`, {
|
||||
title: 'Folder not found',
|
||||
kind: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await openPath(absolutePath)
|
||||
}
|
||||
|
||||
export const formatEtaSmart = (seconds: number) => {
|
||||
@@ -0,0 +1,248 @@
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import ProgressBar from '@/components/ProgressBar'
|
||||
import { formatEtaSmart } from '@/lib/util'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCancel,
|
||||
faPause,
|
||||
faPlay,
|
||||
faTrash
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { notifyUser } from '@/lib/notifications'
|
||||
import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window'
|
||||
|
||||
export default function DownloadsPopup () {
|
||||
const {
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
downloadQueue,
|
||||
setDownloadQueue,
|
||||
notificationsAllowed,
|
||||
versionsList,
|
||||
modsList,
|
||||
versions,
|
||||
serverVersionList
|
||||
} = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>Downloads</p>
|
||||
<div className='popup-content'>
|
||||
{downloadProgress.map((v, i) => {
|
||||
const queuePosition = downloadQueue.indexOf(v.version)
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className='popup-entry flex flex-col justify-between relative'
|
||||
>
|
||||
<div className='absolute right-2 top-2 flex flex-row gap-2'>
|
||||
<div
|
||||
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||
hidden={!v.downloading && !v.paused}
|
||||
onClick={async () => {
|
||||
if (v.downloading) {
|
||||
await invoke('cancel_download', { name: v.version })
|
||||
} else {
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === v.version)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
paused: false,
|
||||
downloading: true
|
||||
}
|
||||
return copy
|
||||
})
|
||||
const res = await invoke<string>('download', {
|
||||
url: v.url,
|
||||
name: v.version,
|
||||
hash: v.hash,
|
||||
downloadType: v.type,
|
||||
modId: String(v.modId ?? '')
|
||||
})
|
||||
|
||||
if (res === '1') {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== v.version)
|
||||
)
|
||||
if (v.type != 1) {
|
||||
if (v.type == 2) {
|
||||
versions?.set('mods', {
|
||||
...modsList,
|
||||
[v.modGame! + '-' + v.modId!]: {
|
||||
[v.modVersion!]: Date.now()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
versions?.set('list', {
|
||||
...versionsList,
|
||||
[v.version]: Date.now()
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (res == '0') {
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === v.version)
|
||||
if (i === -1) return prev
|
||||
if (prev[i].canceled) {
|
||||
return prev.filter((_, idx) => idx !== i)
|
||||
}
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
downloading: false,
|
||||
paused: true,
|
||||
failed: false
|
||||
}
|
||||
return copy
|
||||
})
|
||||
} else if (res == '-1') {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === v.version
|
||||
? {
|
||||
...d,
|
||||
queued: false,
|
||||
failed: true,
|
||||
progress: 0
|
||||
}
|
||||
: d
|
||||
)
|
||||
)
|
||||
if (notificationsAllowed)
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${
|
||||
serverVersionList?.versions.find(
|
||||
vf => vf.id == v.version
|
||||
)?.displayName
|
||||
} has failed.`
|
||||
)
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Critical
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={v.downloading ? faPause : faPlay}
|
||||
className='w-6 h-6'
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||
hidden={!v.downloading && !v.paused}
|
||||
onClick={async () => {
|
||||
if (!v.paused) {
|
||||
await invoke('cancel_download', { name: v.version })
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === v.version)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = {
|
||||
...copy[i],
|
||||
canceled: true
|
||||
}
|
||||
return copy
|
||||
})
|
||||
} else {
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === v.version)
|
||||
if (i === -1) return prev
|
||||
return prev.filter((_, idx) => idx !== i)
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCancel} className='w-6 h-6' />
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||
hidden={!v.failed && !v.queued}
|
||||
onClick={() => {
|
||||
setDownloadQueue(prev =>
|
||||
prev.filter(id => id !== v.version)
|
||||
)
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== v.version)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className='w-6 h-6' />
|
||||
</div>
|
||||
</div>
|
||||
<p className='text-2xl text-center'>
|
||||
{v.type == 1
|
||||
? 'Mod loader for '
|
||||
: v.type == 2
|
||||
? 'Mod for '
|
||||
: null}{' '}
|
||||
{
|
||||
serverVersionList?.versions.find(vf => vf.id == v.version)
|
||||
?.displayName
|
||||
}
|
||||
</p>
|
||||
<div className='mt-6.25 flex items-center justify-between'>
|
||||
{v.failed || v.queued ? (
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<span
|
||||
className={`${
|
||||
v.failed ? 'text-red-500' : 'text-yellow-300'
|
||||
} inline-block text-center flex-1`}
|
||||
>
|
||||
{v.failed
|
||||
? 'Download failed'
|
||||
: queuePosition === 0
|
||||
? 'Starting soon...'
|
||||
: `Queued (Position ${queuePosition + 1})`}
|
||||
</span>
|
||||
</div>
|
||||
) : v.hash_checking ? (
|
||||
<span className='text-blue-300 inline-block w-full text-center'>
|
||||
Verifying file integrity...
|
||||
</span>
|
||||
) : v.unzipping ? (
|
||||
<div className='flex flex-col gap-1 w-full'>
|
||||
<span className='text-center'>
|
||||
Unzipped {v.unzipped} / {v.unzipTotal} files
|
||||
</span>
|
||||
<ProgressBar
|
||||
progress={(v.unzipped / v.unzipTotal) * 100}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col gap-1 w-full'>
|
||||
<span className='text-center'>
|
||||
Downloaded{' '}
|
||||
{prettyBytes(v.progressBytes, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})}{' '}
|
||||
of{' '}
|
||||
{prettyBytes(v.size, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})}{' '}
|
||||
(ETA: {formatEtaSmart(v.etaSecs)} • Speed:{' '}
|
||||
{prettyBytes(v.speed, {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1
|
||||
})}
|
||||
/s)
|
||||
</span>
|
||||
<ProgressBar progress={v.progress} className='w-full' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import {
|
||||
faCheck,
|
||||
faCode,
|
||||
@@ -9,18 +9,18 @@ import {
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
export default function GamesDownloadPopup () {
|
||||
const { serverVersionList, getVersionsAmountData, setSelectedGame } =
|
||||
useGlobal()
|
||||
const { serverVersionList, setSelectedGame, versionsList } = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>Select a game to download</p>
|
||||
<div className='popup-content'>
|
||||
{serverVersionList?.games
|
||||
.filter(v => {
|
||||
const data = getVersionsAmountData(v.id)
|
||||
if (!data) return false
|
||||
if (data.total > 0) return true
|
||||
.filter(i => {
|
||||
const gameVersions = serverVersionList.versions.filter(
|
||||
vf => vf.game === i.id
|
||||
)
|
||||
if (gameVersions.length > 0) return true
|
||||
})
|
||||
.map((v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
@@ -32,9 +32,15 @@ export default function GamesDownloadPopup () {
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const data = getVersionsAmountData(v.id)
|
||||
if (!data) return 'N/A'
|
||||
return `${data.installed}/${data.total}`
|
||||
const gameVersions = serverVersionList.versions.filter(
|
||||
vf => vf.game === v.id
|
||||
)
|
||||
const installed = gameVersions.filter(v =>
|
||||
Object.keys(versionsList).includes(v.id)
|
||||
).length
|
||||
return gameVersions.length
|
||||
? `${installed}/${gameVersions.length}`
|
||||
: 'N/A'
|
||||
})()}{' '}
|
||||
versions installed
|
||||
</p>
|
||||
@@ -74,11 +80,7 @@ export default function GamesDownloadPopup () {
|
||||
<button
|
||||
className='button btntheme3 right-2 bottom-2'
|
||||
onClick={() => setSelectedGame(v.id)}
|
||||
title={`Click to download specific versions of the game. You have ${(() => {
|
||||
const data = getVersionsAmountData(v.id)
|
||||
if (!data) return 'N/A'
|
||||
return `${data.installed} of ${data.total}`
|
||||
})()} versions downloaded.`}
|
||||
title='Click to download more versions of the game.'
|
||||
>
|
||||
<>
|
||||
<FontAwesomeIcon icon={faDownload} /> Download
|
||||
@@ -9,28 +9,25 @@ import {
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useEffect, useState } from 'react'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
import { writeVersionsConfig } from '@/lib/BazookaManager'
|
||||
import { openFolder } from '@/lib/Util'
|
||||
import { openFolder } from '@/lib/util'
|
||||
|
||||
export default function VersionVersionPopup () {
|
||||
export default function ManageVersionPopup () {
|
||||
const {
|
||||
getGameInfo,
|
||||
getVersionInfo,
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
managingVersion,
|
||||
downloadedVersionsConfig,
|
||||
viewingInfoFromDownloads,
|
||||
setManagingVersion,
|
||||
closePopup,
|
||||
setDownloadedVersionsConfig,
|
||||
setPopupMode,
|
||||
setSelectedVersionList,
|
||||
downloadVersions
|
||||
downloadVersions,
|
||||
versions
|
||||
} = useGlobal()
|
||||
const [versionSize, setVersionSize] = useState<number>(0)
|
||||
|
||||
@@ -43,10 +40,14 @@ export default function VersionVersionPopup () {
|
||||
})
|
||||
}, [managingVersion, setVersionSize, viewingInfoFromDownloads])
|
||||
|
||||
if (!managingVersion || !downloadedVersionsConfig) return <></>
|
||||
if (!managingVersion) return <></>
|
||||
|
||||
const versionInfo = getVersionInfo(managingVersion)
|
||||
const gameInfo = getGameInfo(versionInfo?.game)
|
||||
const versionInfo = serverVersionList?.versions.find(
|
||||
vf => vf.id == managingVersion
|
||||
)
|
||||
const gameInfo = serverVersionList?.games.find(
|
||||
vf => vf.id == versionInfo?.game
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,7 +60,7 @@ export default function VersionVersionPopup () {
|
||||
<p>
|
||||
Installed{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
downloadedVersionsConfig.list[managingVersion] ?? 0
|
||||
versionsList[managingVersion] ?? 0
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -109,13 +110,7 @@ export default function VersionVersionPopup () {
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
onClick={async () => {
|
||||
if (!versionInfo) return
|
||||
await message(atob(versionInfo.changelog), {
|
||||
title: 'Changelog for ' + versionInfo.displayName,
|
||||
kind: 'info'
|
||||
})
|
||||
}}
|
||||
onClick={() => setPopupMode(4)}
|
||||
hidden={!versionInfo?.changelog}
|
||||
>
|
||||
<p>View Changelog</p>
|
||||
@@ -135,18 +130,14 @@ export default function VersionVersionPopup () {
|
||||
onClick={async () => {
|
||||
closePopup()
|
||||
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
const updatedList = Object.fromEntries(
|
||||
Object.entries(prev.list).filter(([k]) => k !== managingVersion)
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== managingVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
const updatedConfig = {
|
||||
...prev,
|
||||
list: updatedList
|
||||
}
|
||||
writeVersionsConfig(updatedConfig)
|
||||
return updatedConfig
|
||||
})
|
||||
|
||||
if (
|
||||
await exists('game/' + managingVersion, {
|
||||
@@ -171,18 +162,14 @@ export default function VersionVersionPopup () {
|
||||
setPopupMode(1)
|
||||
|
||||
//uninstall
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
const updatedList = Object.fromEntries(
|
||||
Object.entries(prev.list).filter(([k]) => k !== managingVersion)
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== managingVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
const updatedConfig = {
|
||||
...prev,
|
||||
list: updatedList
|
||||
}
|
||||
writeVersionsConfig(updatedConfig)
|
||||
return updatedConfig
|
||||
})
|
||||
|
||||
if (
|
||||
await exists('game/' + managingVersion, {
|
||||
@@ -196,7 +183,12 @@ export default function VersionVersionPopup () {
|
||||
|
||||
//reinstall
|
||||
setSelectedVersionList([managingVersion])
|
||||
downloadVersions([managingVersion])
|
||||
downloadVersions([
|
||||
{
|
||||
id: managingVersion,
|
||||
type: 0
|
||||
}
|
||||
])
|
||||
}}
|
||||
title="Click to reinstall this game. This will NOT remove any progress or any save files. This WILL uninstall any modifications to the game's executable files."
|
||||
hidden={viewingInfoFromDownloads}
|
||||
@@ -0,0 +1,368 @@
|
||||
'use client'
|
||||
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { verifySignature } from '@/lib/util'
|
||||
import { Mod } from '@/types/Mod'
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faClock,
|
||||
faCodeBranch,
|
||||
faDownload,
|
||||
faGlobe,
|
||||
faPeopleGroup,
|
||||
faRefresh,
|
||||
faTrash
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function ModDownloadsPopup () {
|
||||
const {
|
||||
modsList,
|
||||
serverVersionList,
|
||||
managingVersion,
|
||||
showModInfo,
|
||||
setShowModInfo,
|
||||
downloadVersions,
|
||||
downloadProgress,
|
||||
versions
|
||||
} = useGlobal()
|
||||
|
||||
const [mods, setMods] = useState<Mod[] | 0 | 1>(0)
|
||||
const [tab, setTab] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!managingVersion) return
|
||||
;(async () => {
|
||||
const response = await fetch(
|
||||
`https://games.lncvrt.xyz/api/launcher/mods?platform=${platform()}&arch=${arch()}&version=${managingVersion}`
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (await verifySignature(JSON.stringify(data), signature)) {
|
||||
setMods(data as Mod[])
|
||||
} else {
|
||||
setMods(1)
|
||||
}
|
||||
})()
|
||||
}, [managingVersion])
|
||||
|
||||
if (!managingVersion || !serverVersionList) return <></>
|
||||
|
||||
const versionInfo = serverVersionList.versions.find(
|
||||
v => v.id == managingVersion
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className='popup-top-button btntheme1 right-2'
|
||||
onClick={async () => {
|
||||
setMods(0)
|
||||
const response = await fetch(
|
||||
`https://games.lncvrt.xyz/api/launcher/mods?platform=${platform()}&arch=${arch()}&version=${managingVersion}`
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (await verifySignature(JSON.stringify(data), signature)) {
|
||||
setMods(data as Mod[])
|
||||
} else {
|
||||
setMods(1)
|
||||
}
|
||||
}}
|
||||
hidden={typeof mods == 'number' || showModInfo != null}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRefresh} />
|
||||
</button>
|
||||
<p className='text-xl text-center'>
|
||||
{!showModInfo
|
||||
? versionInfo?.displayName + ' Mod Manager'
|
||||
: showModInfo.name +
|
||||
' by ' +
|
||||
showModInfo.creators[0] +
|
||||
(showModInfo.creators.length > 1
|
||||
? ' + ' + (showModInfo.creators.length - 1) + ' more'
|
||||
: '')}
|
||||
</p>
|
||||
{typeof mods != 'number' && !showModInfo ? (
|
||||
<div className='flex flex-row justify-center items-center -mb-2 mt-0.5 gap-1.5'>
|
||||
<button
|
||||
className={`button ${tab == 0 ? 'btntheme3' : 'btntheme2'}`}
|
||||
onClick={() => setTab(0)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Installed
|
||||
</button>
|
||||
<button
|
||||
className={`button ${tab == 1 ? 'btntheme3' : 'btntheme2'}`}
|
||||
onClick={() => setTab(1)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className='popup-content h-full'>
|
||||
{typeof mods == 'number' ||
|
||||
(!showModInfo &&
|
||||
mods.filter(v =>
|
||||
tab == 0
|
||||
? Object.keys(modsList).includes(
|
||||
String(versionInfo?.game + '-' + v.id)
|
||||
)
|
||||
: !Object.keys(modsList).includes(
|
||||
String(versionInfo?.game + '-' + v.id)
|
||||
)
|
||||
).length == 0) ? (
|
||||
<p className='text-2xl flex justify-center items-center h-full'>
|
||||
{typeof mods == 'number'
|
||||
? mods == 0
|
||||
? 'Loading'
|
||||
: 'Failed to load mods'
|
||||
: tab == 0
|
||||
? 'No mods installed'
|
||||
: 'No mods available to download'}
|
||||
</p>
|
||||
) : !showModInfo ? (
|
||||
<div className='flex flex-col items-center justify-center gap-2 p-2'>
|
||||
{mods
|
||||
.filter(v =>
|
||||
tab == 0
|
||||
? Object.keys(modsList).includes(
|
||||
String(versionInfo?.game + '-' + v.id)
|
||||
)
|
||||
: !Object.keys(modsList).includes(
|
||||
String(versionInfo?.game + '-' + v.id)
|
||||
)
|
||||
)
|
||||
.map(v => {
|
||||
const localVersion =
|
||||
tab == 0
|
||||
? Object.keys(
|
||||
modsList[versionInfo?.game + '-' + v.id] ?? {}
|
||||
)[0]
|
||||
: null
|
||||
return (
|
||||
<div
|
||||
key={v.id}
|
||||
className='bg-(--col3) border border-(--col5) rounded-lg w-full h-16 flex flex-row'
|
||||
>
|
||||
<div className='flex flex-col justify-center h-full px-3 w-fit'>
|
||||
<p>
|
||||
<span className='text-lg'>{v.name}</span>{' '}
|
||||
<span
|
||||
className={
|
||||
tab != 0 || v.latestVersion == localVersion
|
||||
? 'text-green-300'
|
||||
: 'text-red-300'
|
||||
}
|
||||
>
|
||||
v{tab == 0 ? localVersion : v.latestVersion}
|
||||
</span>
|
||||
<span
|
||||
className='text-green-300'
|
||||
hidden={tab != 0 || v.latestVersion == localVersion}
|
||||
>
|
||||
{' '}
|
||||
(available: v{v.latestVersion})
|
||||
</span>
|
||||
</p>
|
||||
<p className='text-yellow-200'>
|
||||
Made by {v.creators[0]}
|
||||
{v.creators.length > 1
|
||||
? ' + ' + (v.creators.length - 1) + ' more'
|
||||
: null}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-row items-center h-full gap-2 px-3 ml-auto'>
|
||||
<p className='text-green-400' hidden={tab == 0}>
|
||||
<FontAwesomeIcon icon={faDownload} />{' '}
|
||||
{v.downloads.toLocaleString()}
|
||||
</p>
|
||||
<button
|
||||
className='button btntheme3'
|
||||
onClick={async () => {
|
||||
const path =
|
||||
'game/' +
|
||||
versionInfo?.id +
|
||||
'/BepInEx/plugins/' +
|
||||
v.id
|
||||
if (
|
||||
await exists(path, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove(path, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
versions?.set(
|
||||
'mods',
|
||||
Object.fromEntries(
|
||||
Object.entries(modsList).filter(
|
||||
([k]) => k !== versionInfo?.game + '-' + v.id
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
downloadVersions([
|
||||
{
|
||||
id: managingVersion,
|
||||
type: 2,
|
||||
modDownload: v.latestDownload,
|
||||
gameId: versionInfo?.game,
|
||||
modId: v.id,
|
||||
modVersion: v.latestVersion
|
||||
}
|
||||
])
|
||||
}}
|
||||
hidden={tab != 0 || v.latestVersion == localVersion}
|
||||
disabled={downloadProgress.length != 0}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme3'
|
||||
onClick={() => setShowModInfo(v)}
|
||||
>
|
||||
{tab == 0 ? 'View' : 'Get'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col h-full w-full items-center'>
|
||||
<div className='flex flex-col p-2 h-fit w-fit bg-(--col3) border border-(--col5) rounded-lg m-2 items-center'>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faDownload} className='text-green-400' />{' '}
|
||||
Downloads {showModInfo.downloads.toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faClock} /> Released{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
showModInfo.released * 1000
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faClock} /> Updated{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
showModInfo.updated * 1000
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faCodeBranch} /> Version{' '}
|
||||
{showModInfo.latestVersion}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-col p-2 bg-(--col3) border border-(--col5) rounded-lg h-full w-[calc(100%-16px)]'>
|
||||
{showModInfo.description
|
||||
? atob(showModInfo.description)
|
||||
: '(No description added)'}
|
||||
</div>
|
||||
<div className='flex flex-row h-fit w-full gap-2 justify-center my-2'>
|
||||
{Object.keys(modsList).includes(
|
||||
String(versionInfo?.game + '-' + showModInfo.id)
|
||||
) ? (
|
||||
<button
|
||||
className='button btntheme2 w-fit'
|
||||
disabled={downloadProgress.length != 0}
|
||||
onClick={async () => {
|
||||
const path =
|
||||
'game/' +
|
||||
versionInfo?.id +
|
||||
'/BepInEx/plugins/' +
|
||||
showModInfo.id
|
||||
if (
|
||||
await exists(path, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove(path, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
versions?.set(
|
||||
'mods',
|
||||
Object.fromEntries(
|
||||
Object.entries(modsList).filter(
|
||||
([k]) =>
|
||||
k !== versionInfo?.game + '-' + showModInfo.id
|
||||
)
|
||||
)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className='text-red-400' />{' '}
|
||||
Uninstall
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className='button btntheme2 w-fit'
|
||||
onClick={() => {
|
||||
downloadVersions([
|
||||
{
|
||||
id: managingVersion,
|
||||
type: 2,
|
||||
modDownload: showModInfo.latestDownload,
|
||||
gameId: versionInfo?.game,
|
||||
modId: showModInfo.id,
|
||||
modVersion: showModInfo.latestVersion
|
||||
}
|
||||
])
|
||||
}}
|
||||
disabled={downloadProgress.length != 0}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faDownload}
|
||||
className='text-green-400'
|
||||
/>{' '}
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className='button btntheme2 w-fit'
|
||||
onClick={async () => {
|
||||
await message(
|
||||
!showModInfo.changelog
|
||||
? '(No changelog provided)'
|
||||
: atob(showModInfo?.changelog ?? ''),
|
||||
{
|
||||
title: 'Changelog for ' + showModInfo.name,
|
||||
kind: 'info'
|
||||
}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
color='lightgray'
|
||||
/>{' '}
|
||||
View changelog
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme2 w-fit'
|
||||
hidden={showModInfo.creators.length < 2}
|
||||
onClick={async () => {
|
||||
if (!versionInfo) return
|
||||
await message(showModInfo.creators.join(', '), {
|
||||
title: 'Creators for ' + showModInfo.name,
|
||||
kind: 'info'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPeopleGroup} color='lightgray' /> View
|
||||
all creators
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export default function VersionChangelogPopup () {
|
||||
const { serverVersionList, managingVersion } = useGlobal()
|
||||
|
||||
if (!managingVersion) return <></>
|
||||
|
||||
const versionInfo = serverVersionList?.versions.find(
|
||||
vf => vf.id == managingVersion
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Viewing changelog {versionInfo?.displayName}
|
||||
</p>
|
||||
<div className='popup-content h-full w-full p-2'>
|
||||
<p
|
||||
className='whitespace-pre-line'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
atob(versionInfo?.changelog ?? '')
|
||||
.replace(/^- /gm, '• ')
|
||||
.replaceAll(/`([^`]+)`/g, '<code>$1</code>')
|
||||
)
|
||||
}}
|
||||
></p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useGlobal } from '@/app/GlobalProvider'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { faAdd, faInfo, faRemove } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
export default function VersionsDownloadPopup () {
|
||||
const {
|
||||
serverVersionList,
|
||||
getSpecialVersionsList,
|
||||
selectedVersionList,
|
||||
setSelectedVersionList,
|
||||
setManagingVersion,
|
||||
setPopupMode,
|
||||
getSpecialVersionsList,
|
||||
selectedGame,
|
||||
setViewingInfoFromDownloads,
|
||||
downloadedVersionsConfig,
|
||||
downloadVersions,
|
||||
getGameInfo
|
||||
downloadVersions
|
||||
} = useGlobal()
|
||||
if (!selectedGame) return <></>
|
||||
|
||||
const game = serverVersionList?.games.find(g => g.id === selectedGame)
|
||||
|
||||
if (!selectedGame || !game) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -77,19 +79,18 @@ export default function VersionsDownloadPopup () {
|
||||
<button
|
||||
className='button btntheme1 w-fit mt-2 -mb-4'
|
||||
onClick={() => {
|
||||
if (downloadedVersionsConfig) {
|
||||
downloadVersions(selectedVersionList)
|
||||
downloadVersions(
|
||||
selectedVersionList.map(versionId => ({
|
||||
id: versionId,
|
||||
type: 0
|
||||
}))
|
||||
)
|
||||
setPopupMode(1)
|
||||
}
|
||||
}}
|
||||
disabled={selectedVersionList.length == 0}
|
||||
title={
|
||||
selectedVersionList.length == 0
|
||||
? 'Select at least one version to download'
|
||||
: `Download ${selectedVersionList.length} version${
|
||||
title={`Download ${selectedVersionList.length} version${
|
||||
selectedVersionList.length == 1 ? '' : 's'
|
||||
} of ${getGameInfo(selectedGame)?.name}`
|
||||
}
|
||||
} of ${game.name}`}
|
||||
>
|
||||
Download {selectedVersionList.length} version
|
||||
{selectedVersionList.length == 1 ? '' : 's'}
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
SetStateAction
|
||||
} from 'react'
|
||||
import { DownloadProgress } from '@/types/DownloadProgress'
|
||||
import { VersionsConfig } from '@/types/VersionsConfig'
|
||||
import { NormalConfig } from '@/types/NormalConfig'
|
||||
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
|
||||
import { Mod } from '@/types/Mod'
|
||||
import { Store } from '@tauri-apps/plugin-store'
|
||||
import { GameVersion } from '@/types/GameVersion'
|
||||
import { Game } from '@/types/Game'
|
||||
|
||||
type GlobalCtxType = {
|
||||
serverVersionList: ServerVersionsResponse | null
|
||||
@@ -26,31 +25,42 @@ type GlobalCtxType = {
|
||||
setPopupMode: Dispatch<SetStateAction<number | null>>
|
||||
fadeOut: boolean
|
||||
setFadeOut: Dispatch<SetStateAction<boolean>>
|
||||
downloadedVersionsConfig: VersionsConfig | null
|
||||
setDownloadedVersionsConfig: Dispatch<SetStateAction<VersionsConfig | null>>
|
||||
normalConfig: NormalConfig | null
|
||||
setNormalConfig: Dispatch<SetStateAction<NormalConfig | null>>
|
||||
managingVersion: string | null
|
||||
setManagingVersion: Dispatch<SetStateAction<string | null>>
|
||||
setSelectedGame: Dispatch<SetStateAction<number | null>>
|
||||
getVersionInfo: (id: string | undefined) => GameVersion | undefined
|
||||
getGameInfo: (game: number | undefined) => Game | undefined
|
||||
getListOfGames(): Game[]
|
||||
getVersionsAmountData: (gameId: number) => {
|
||||
installed: number
|
||||
total: number
|
||||
} | null
|
||||
viewingInfoFromDownloads: boolean
|
||||
version: string | null
|
||||
downloadVersions: (list: string[]) => Promise<void>
|
||||
downloadVersions: (
|
||||
list: Array<{
|
||||
id: string
|
||||
type: 0 | 1 | 2
|
||||
modDownload?: number
|
||||
gameId?: number
|
||||
modId?: number
|
||||
modVersion?: string
|
||||
}>
|
||||
) => Promise<void>
|
||||
category: number
|
||||
setCategory: Dispatch<SetStateAction<number>>
|
||||
downloadQueue: string[]
|
||||
setDownloadQueue: Dispatch<SetStateAction<string[]>>
|
||||
closePopup: () => void
|
||||
getSpecialVersionsList(game?: number | undefined): GameVersion[]
|
||||
selectedGame: number | null
|
||||
setViewingInfoFromDownloads: Dispatch<SetStateAction<boolean>>
|
||||
showModInfo: Mod | null
|
||||
setShowModInfo: Dispatch<SetStateAction<Mod | null>>
|
||||
getSpecialVersionsList: (game?: number) => GameVersion[]
|
||||
settings: Store | null
|
||||
versions: Store | null
|
||||
notificationsAllowed: boolean
|
||||
sidebarAlwaysShowGames: boolean
|
||||
linuxUseWine: boolean
|
||||
linuxWineCommand: string
|
||||
customDataLocation: string
|
||||
versionsList: Record<string, number>
|
||||
modsList: Record<string, Record<string, number>>
|
||||
movingData: boolean
|
||||
setMovingData: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const GlobalCtx = createContext<GlobalCtxType | null>(null)
|
||||
@@ -0,0 +1,60 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.dropdown {
|
||||
@apply relative w-40;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
@apply w-full flex items-center justify-between px-2 py-1 rounded-md cursor-pointer
|
||||
bg-(--col2) hover:bg-(--col3)
|
||||
border border-(--col4) hover:border-(--col5)
|
||||
text-white;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
@apply w-3 h-3 transition-transform stroke-current stroke-2 fill-none;
|
||||
}
|
||||
|
||||
.dropdown-arrow.open {
|
||||
@apply rotate-180;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply absolute z-50 mt-1 w-full rounded-md shadow-2xl overflow-hidden
|
||||
bg-(--col2) border border-(--col4)
|
||||
origin-top
|
||||
opacity-0 scale-95 -translate-y-1 pointer-events-none
|
||||
transition-[opacity,transform] duration-150 ease-out;
|
||||
}
|
||||
|
||||
.dropdown-menu.open {
|
||||
@apply opacity-100 scale-100 translate-y-0 pointer-events-auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@apply px-2 py-1 cursor-pointer text-white transition-colors border-b border-(--col4) last:border-b-0;
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item.active {
|
||||
@apply bg-(--col3);
|
||||
}
|
||||
|
||||
.dropdown-menu.open .dropdown-item {
|
||||
animation: dropIn 220ms ease-out forwards;
|
||||
animation-delay: calc(var(--i) * 40ms);
|
||||
}
|
||||
|
||||
@keyframes dropIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-transparent text-white select-none;
|
||||
|
||||
--col0: rgb(8, 8, 8);
|
||||
--col1: rgb(16, 16, 16);
|
||||
--col2: rgb(32, 32, 32);
|
||||
--col3: rgb(48, 48, 48);
|
||||
--col4: rgb(64, 64, 64);
|
||||
--col5: rgb(80, 80, 80);
|
||||
--col6: rgb(96, 96, 96);
|
||||
--col7: rgb(112, 112, 112);
|
||||
--col8: rgb(128, 128, 128);
|
||||
--col9: rgb(144, 144, 144);
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply rounded-md cursor-pointer text-[16px] py-1 px-2 transition-colors border;
|
||||
}
|
||||
|
||||
.btntheme1 {
|
||||
@apply bg-(--col2)/50 border-(--col4)/50 hover:bg-(--col4)/50 hover:border-(--col6)/50 disabled:text-gray-300 disabled:bg-(--col1)/50 disabled:border-(--col5)/50 disabled:hover:text-gray-100 disabled:hover:bg-(--col2) disabled:hover:border-(--col6);
|
||||
}
|
||||
|
||||
.btntheme2,
|
||||
.downloads-entry .entry-info-item,
|
||||
.downloads-entry .button {
|
||||
@apply bg-(--col3)/50 border-(--col5)/50 hover:bg-(--col5)/50 hover:border-(--col7)/50 disabled:text-gray-300 disabled:bg-(--col2)/50 disabled:border-(--col6)/50 disabled:hover:text-gray-100 disabled:hover:bg-(--col3) disabled:hover:border-(--col7);
|
||||
}
|
||||
|
||||
.btntheme3,
|
||||
.downloads-entry:hover .entry-info-item,
|
||||
.downloads-entry:hover .button {
|
||||
@apply bg-(--col4)/50 border-(--col6)/50 hover:bg-(--col6)/50 hover:border-(--col8)/50 disabled:text-gray-300 disabled:bg-(--col2)/50 disabled:border-(--col7)/50 disabled:hover:text-gray-100 disabled:hover:bg-(--col4) disabled:hover:border-(--col8);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-(--col2) rounded-lg;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-(--col4) w-1 rounded-lg active:bg-(--col5);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-overlay {
|
||||
@apply fixed w-screen h-screen z-99999 flex justify-center items-center animate-[fadeIn_0.2s_ease-out_forwards] left-0 top-0 bg-[rgba(0,0,0,0.5)];
|
||||
}
|
||||
|
||||
.popup-overlay.fade-out {
|
||||
@apply animate-[fadeOut_0.2s_ease-out_forwards];
|
||||
}
|
||||
|
||||
.popup-box {
|
||||
@apply relative w-150 h-120 rounded-lg bg-(--col1)/50 border border-(--col3)/50 flex flex-col p-6;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
@apply flex-1 overflow-auto bg-(--col2)/50 border border-(--col4)/50 rounded-lg mt-4;
|
||||
}
|
||||
|
||||
.popup-entry {
|
||||
@apply relative h-fit bg-(--col3)/50 m-2 p-2 rounded-lg border border-(--col5)/50;
|
||||
}
|
||||
|
||||
.popup-entry button {
|
||||
@apply absolute;
|
||||
}
|
||||
|
||||
.popup-top-button {
|
||||
@apply flex justify-center items-center absolute text-2xl text-gray-200 hover:text-white cursor-pointer h-12 w-12 p-3 rounded-lg top-2 transition-colors border;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply border-2 border-(--col4)/50 rounded-md bg-(--col2)/50 py-1 px-2 focus:bg-(--col3)/50 focus:border-(--col6)/50 transition-colors w-full;
|
||||
}
|
||||
|
||||
.entry-info-item {
|
||||
@apply flex flex-row items-center gap-1 border text-gray-300 py-1 px-2 rounded-md w-fit text-[16px] transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply bg-(--col4) border border-(--col6) rounded-lg px-1 py-0.5 text-sm;
|
||||
}
|
||||
@@ -5,11 +5,11 @@
|
||||
}
|
||||
|
||||
.setting-checkbox {
|
||||
@apply appearance-none w-full h-full border-2 border-(--col4) rounded-md bg-(--col2) transition-colors cursor-pointer;
|
||||
@apply appearance-none w-full h-full border-2 border-(--col4)/50 rounded-md bg-(--col2)/50 transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
.setting-checkbox:checked {
|
||||
@apply bg-(--col4) border-(--col6);
|
||||
@apply bg-(--col4)/50 border-(--col6)/50;
|
||||
}
|
||||
|
||||
.fa-check-icon {
|
||||
@@ -1,16 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.sidebar {
|
||||
@apply fixed top-0 left-0 w-60 h-screen bg-(--col1) flex flex-col border-e border-(--col2) z-1;
|
||||
@apply fixed top-0 left-0 w-60 h-screen bg-(--col1)/50 flex flex-col border-e border-(--col2)/50 z-1;
|
||||
}
|
||||
|
||||
.sidebar-downloads {
|
||||
@apply text-[#bdbdbd] fixed bottom-2 left-2 bg-(--col2) rounded-lg border border-(--col3) px-4 py-3 text-xl cursor-pointer transition-colors duration-[0.25s] opacity-75;
|
||||
@apply text-[#bdbdbd] fixed bottom-2 left-2 bg-(--col2)/50 rounded-lg border border-(--col3)/50 px-4 py-3 text-xl cursor-pointer transition-colors duration-[0.25s] opacity-75;
|
||||
}
|
||||
|
||||
.sidebar-downloads:hover {
|
||||
@apply text-white opacity-100;
|
||||
@apply bg-(--col3) border-(--col4);
|
||||
@apply bg-(--col3)/50 border-(--col4)/50;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.link.active {
|
||||
@apply bg-(--col2) border-(--col3);
|
||||
@apply bg-(--col2)/50 border-(--col3)/50;
|
||||
}
|
||||
|
||||
.link.active,
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.link.active:hover {
|
||||
@apply bg-(--col3) border-(--col4);
|
||||
@apply bg-(--col3)/50 border-(--col4)/50;
|
||||
}
|
||||
|
||||
.sidebar ::-webkit-scrollbar {
|
||||
@@ -4,11 +4,24 @@ export class DownloadProgress {
|
||||
public progress: number,
|
||||
public progressBytes: number,
|
||||
public failed: boolean,
|
||||
public downloading: boolean,
|
||||
public paused: boolean,
|
||||
public canceled: boolean,
|
||||
public queued: boolean,
|
||||
public hash_checking: boolean,
|
||||
public finishing: boolean,
|
||||
public speed: number,
|
||||
public size: number,
|
||||
public etaSecs: number
|
||||
public etaSecs: number,
|
||||
public unzipping: boolean,
|
||||
public unzipTotal: number,
|
||||
public unzipped: number,
|
||||
public url: string | null,
|
||||
public executable: string | null,
|
||||
public hash: string | null,
|
||||
public type: 0 | 1 | 2,
|
||||
public modDownload: number | null,
|
||||
public modGame: number | null,
|
||||
public modId: number | null,
|
||||
public modVersion: string | null
|
||||
) {}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export interface GameVersion {
|
||||
wine: number | undefined
|
||||
category: number
|
||||
lastRevision: number
|
||||
download: number
|
||||
download: number | undefined
|
||||
executable: string
|
||||
modSupportDownload: number | undefined
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface Mod {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
creators: string[]
|
||||
downloads: number
|
||||
released: number
|
||||
updated: number
|
||||
latestVersion: string
|
||||
latestDownload: number
|
||||
changelog: string | null
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { SettingsType } from './SettingsType'
|
||||
|
||||
type NormalConfigData = {
|
||||
version: string
|
||||
settings?: Partial<SettingsType>
|
||||
}
|
||||
|
||||
export class NormalConfig {
|
||||
constructor (
|
||||
public version: string,
|
||||
public settings: SettingsType = new SettingsType()
|
||||
) {}
|
||||
|
||||
static import (data: NormalConfigData) {
|
||||
const cfg = new NormalConfig(data.version)
|
||||
if (data.settings) {
|
||||
cfg.settings = { ...cfg.settings, ...data.settings }
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,5 @@ export type SettingProps = {
|
||||
onChange: (val: boolean) => void
|
||||
className?: string
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export class SettingsType {
|
||||
constructor (
|
||||
public allowNotifications: boolean = true,
|
||||
public alwaysShowGamesInSidebar: boolean = true,
|
||||
public useWineOnUnixWhenNeeded: boolean = false,
|
||||
public wineOnUnixCommand: string = 'wine %path%',
|
||||
public theme: number = 0
|
||||
) {}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export type VersionsConfigData = {
|
||||
version: string
|
||||
list: Record<string, number>
|
||||
}
|
||||
|
||||
export class VersionsConfig {
|
||||
constructor (
|
||||
public version: string,
|
||||
public list: Record<string, number> = {}
|
||||
) {}
|
||||
|
||||
static import (data: VersionsConfigData) {
|
||||
const cfg = new VersionsConfig(data.version)
|
||||
cfg.list = { ...data.list }
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user