Compare 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
|
||
|
|
f46d9c67b8
|
||
|
|
132616dc29
|
||
|
|
8b22fdb648
|
||
|
|
dc55c7549d
|
||
|
|
9d9c6147c5
|
||
|
|
48c8365b7c
|
||
|
|
8884d08c7c
|
||
|
|
b516a9cc43
|
||
|
|
6b7b2b198a
|
||
|
|
fbf87dceba
|
||
|
|
9ae28690a7
|
||
|
|
f83cc118fe
|
||
|
|
19788a8a3c
|
||
|
|
e066dd0681
|
||
|
|
29b3c3c58b
|
||
|
|
a8390216d0
|
||
|
|
513f1a0c16
|
||
|
|
e7e624f042
|
||
|
|
654405e016
|
||
|
|
c461a0a459
|
||
|
|
16ce04ba7a
|
||
|
|
1700d127c3
|
||
|
|
7c47b32c68
|
||
|
|
56781b5d1a
|
||
|
|
6ce7c2a8ae
|
||
|
|
15dd9ad013
|
||
|
|
217222a22b
|
||
|
|
39da88a856
|
||
|
|
29c90d2f06
|
||
|
|
467dd1b0a1
|
||
|
|
a1a69c9ac5
|
||
|
|
0163e09b02
|
||
|
|
fbb4b15971
|
||
|
|
77933a2298
|
||
|
|
7209a36752
|
||
|
|
f00b78996a
|
||
|
|
38779c1715
|
||
|
|
fa67d26fe9
|
||
|
|
81d47ea71d
|
||
|
|
70433b76ab
|
||
|
|
105913503c
|
||
|
|
2ad54f976b
|
||
|
|
d8d59febbb
|
||
|
|
a618c76292
|
||
|
|
118fad49d8
|
||
|
|
62c0f84d14
|
||
|
|
4a96911885
|
||
|
|
51cd3c3620
|
||
|
|
dfbfac33d3
|
||
|
|
03ec5a220a
|
||
|
|
abebc0bb08
|
||
|
|
3c7487f20f
|
||
|
|
97231c6b38
|
||
|
|
a93e4ab521
|
||
|
|
e7c48b182f
|
||
|
|
16dd3fd613
|
||
|
|
c91303d194
|
||
|
|
a76e81789b
|
||
|
|
c909973aab
|
||
|
|
28f694e4e7
|
||
|
|
508e4d1640
|
||
|
|
41a66eaf86
|
||
|
|
ac4b76fa46
|
||
|
|
2e02275c29
|
||
|
|
dbaac6a6a6
|
||
|
|
68525f161e
|
||
|
|
d386454e91
|
||
|
|
6e05aac871
|
||
|
|
fbe3feec9d
|
||
|
|
1c540ad35b
|
||
|
|
4ec597967a
|
||
|
|
be4b8fc3ee
|
||
|
|
d9fa967ade
|
||
|
|
0f4ee9f78c
|
||
|
|
cc5d814740
|
||
|
|
53ecbbd07f
|
No files matched your search
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_PUBLIC_SIGNING_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQVJiK1N5c0VQemRYaGN4OWtyMy9rY3NNNURheDZRS1Q0VXZ3bW9PbWd2UHc9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=
|
||||
@@ -30,9 +30,6 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
|
||||
@@ -5,50 +5,53 @@
|
||||
"": {
|
||||
"name": "lncvrt-games-launcher",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/react-fontawesome": "3.1.1",
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@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.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-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",
|
||||
"axios": "1.13.4",
|
||||
"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.3",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tauri-apps/cli": "2.9.6",
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@tailwindcss/postcss": "4.2.2",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "25.0.10",
|
||||
"@types/react": "19.2.10",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"crypto-js": "4.2.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "4.1.18",
|
||||
"typescript": "5.9.3",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-next": "16.2.3",
|
||||
"tailwindcss": "4.2.2",
|
||||
"typescript": "6.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
@@ -64,49 +67,49 @@
|
||||
|
||||
"@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.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||
"@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=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
|
||||
"@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=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="],
|
||||
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.2.0", "", {}, "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw=="],
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
|
||||
|
||||
"@fortawesome/free-brands-svg-icons": ["@fortawesome/free-brands-svg-icons@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA=="],
|
||||
"@fortawesome/free-brands-svg-icons": ["@fortawesome/free-brands-svg-icons@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ=="],
|
||||
|
||||
"@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA=="],
|
||||
"@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.1.1", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-EDllr9hpodc21odmUywHS1alXNiCd4E8sp5GJ5s7wYINz8vSmMiNWpALTiuYODb865YyQ/NlyiN4mbXp7HCNqg=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -116,7 +119,7 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
@@ -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,67 +213,69 @@
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||
"@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.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||
"@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.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||
"@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.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||
"@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.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||
"@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.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||
"@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.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||
"@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.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||
"@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.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||
"@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.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||
"@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.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||
"@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.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||
"@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.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||
"@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.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||
"@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.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
|
||||
"@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.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
|
||||
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
|
||||
|
||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="],
|
||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ=="],
|
||||
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw=="],
|
||||
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.6", "", { "os": "linux", "cpu": "arm" }, "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg=="],
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg=="],
|
||||
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw=="],
|
||||
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="],
|
||||
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.6", "", { "os": "linux", "cpu": "none" }, "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ=="],
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA=="],
|
||||
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ=="],
|
||||
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="],
|
||||
|
||||
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ=="],
|
||||
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="],
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg=="],
|
||||
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="],
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="],
|
||||
|
||||
"@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.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=="],
|
||||
|
||||
@@ -278,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=="],
|
||||
@@ -290,31 +299,33 @@
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
|
||||
"@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.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
|
||||
"@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.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
|
||||
"@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.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
|
||||
"@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.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
|
||||
"@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.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
|
||||
"@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.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
|
||||
"@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.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
|
||||
"@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.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -354,11 +365,11 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
@@ -386,27 +397,23 @@
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
|
||||
"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.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="],
|
||||
"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=="],
|
||||
|
||||
@@ -414,7 +421,7 @@
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
|
||||
"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=="],
|
||||
|
||||
@@ -424,8 +431,6 @@
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
@@ -452,27 +457,27 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"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.279", "", {}, "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg=="],
|
||||
"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.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||
"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=="],
|
||||
|
||||
@@ -486,11 +491,11 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@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.12.4", "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.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -538,14 +543,10 @@
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
@@ -562,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.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
"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=="],
|
||||
|
||||
@@ -684,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.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||
"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.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||
"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.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||
"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.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||
"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.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||
"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.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||
"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.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||
"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.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||
"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.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||
"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.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||
"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.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -724,11 +725,7 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
@@ -740,9 +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-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
"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.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
@@ -778,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.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
"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=="],
|
||||
|
||||
@@ -790,15 +789,13 @@
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -806,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=="],
|
||||
|
||||
@@ -840,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=="],
|
||||
|
||||
@@ -874,15 +871,15 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -898,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.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
|
||||
"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.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -936,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=="],
|
||||
|
||||
@@ -950,9 +947,11 @@
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
"@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.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"eslint-config-next/globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
|
||||
|
||||
@@ -962,18 +961,18 @@
|
||||
|
||||
"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.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||
|
||||
"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.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"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.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
"@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=="],
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ const eslintConfig = defineConfig([
|
||||
'.next/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'next-env.d.ts'
|
||||
'next-env.d.ts',
|
||||
'src-tauri/**'
|
||||
])
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "lncvrt-games-launcher",
|
||||
"private": true,
|
||||
"version": "1.5.1",
|
||||
"version": "1.9.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -11,36 +11,39 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/react-fontawesome": "3.1.1",
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@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.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-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",
|
||||
"axios": "1.13.4",
|
||||
"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.3",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tauri-apps/cli": "2.9.6",
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@tailwindcss/postcss": "4.2.2",
|
||||
"@tauri-apps/cli": "2.10.1",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/node": "25.0.10",
|
||||
"@types/react": "19.2.10",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"crypto-js": "4.2.0",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "4.1.18",
|
||||
"typescript": "5.9.3"
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-next": "16.2.3",
|
||||
"tailwindcss": "4.2.2",
|
||||
"typescript": "6.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lncvrt-games-launcher"
|
||||
version = "1.5.1"
|
||||
version = "1.9.1"
|
||||
authors = ["Lncvrt"]
|
||||
edition = "2024"
|
||||
|
||||
@@ -9,32 +9,49 @@ name = "lncvrt_games_launcher_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.3", features = [] }
|
||||
tauri-build = { version = "2.5.6", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.9.5", features = ["macos-private-api"] }
|
||||
tauri = { version = "2.10.3", features = ["macos-private-api"] }
|
||||
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.1", default-features = false, features = ["stream", "rustls"] }
|
||||
tokio = "1.49.0"
|
||||
futures-util = { version = "0.3.31", features = ["io"] }
|
||||
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 = "7.2.0"
|
||||
libc = "0.2.180"
|
||||
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.0"
|
||||
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.77", features = ["v111", "vendored"] }
|
||||
base64 = "0.22.1"
|
||||
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.3.7"
|
||||
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",
|
||||
@@ -19,18 +17,40 @@
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-request-user-attention",
|
||||
"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/**" }, { "path": "$APPCONFIG/versions.json" }]
|
||||
},
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [{ "path": "$APPLOCALDATA/game/**" }]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "path": "$APPLOCALDATA/game/**" }
|
||||
{ "url": "https://games.lncvrt.xyz/api/launcher/latest" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/launcher/versions" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/launcher/download" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/account" },
|
||||
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/score" },
|
||||
{ "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/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"]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 248 KiB |
@@ -1,7 +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},
|
||||
@@ -10,26 +15,53 @@ use std::{
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use sysinfo::System;
|
||||
use tauri::window::{ProgressBarState, ProgressBarStatus};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
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;
|
||||
|
||||
#[allow(unused)]
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
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 = path.canonicalize().ok();
|
||||
if target.is_none() {
|
||||
return false;
|
||||
}
|
||||
let target = target.unwrap();
|
||||
let target = match path.canonicalize() {
|
||||
Ok(t) => t,
|
||||
Err(_) => return false,
|
||||
};
|
||||
sys.processes().values().any(|proc| {
|
||||
proc.exe()
|
||||
.and_then(|exe| exe.canonicalize().ok())
|
||||
@@ -37,26 +69,57 @@ fn is_running_by_path(path: &Path) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
||||
fn should_skip(name: &str) -> bool {
|
||||
name.starts_with("__MACOSX/")
|
||||
|| name == "__MACOSX"
|
||||
|| name.ends_with("/.DS_Store")
|
||||
|| name.ends_with(".DS_Store")
|
||||
}
|
||||
|
||||
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() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let outpath = out_dir.join(file.name());
|
||||
for i in 0..total {
|
||||
let mut entry = archive.by_index(i)?;
|
||||
let entry_name = entry.name();
|
||||
|
||||
if file.is_dir() {
|
||||
if should_skip(entry_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Some(mode) = entry.unix_mode() {
|
||||
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?;
|
||||
}
|
||||
} else {
|
||||
if let Some(parent) = outpath.parent() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut outfile = File::create(&outpath)?;
|
||||
copy(&mut file, &mut outfile)?;
|
||||
copy(&mut entry, &mut outfile)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Some(mode) = entry.unix_mode() {
|
||||
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>(())
|
||||
})
|
||||
.await;
|
||||
@@ -69,12 +132,10 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
||||
|
||||
#[tauri::command]
|
||||
fn folder_size(app: AppHandle, version: String) -> String {
|
||||
let path = app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.unwrap()
|
||||
.join("game")
|
||||
.join(&version);
|
||||
let path = match data_dir(&app) {
|
||||
Ok(p) => p.join("game").join(&version),
|
||||
Err(_) => return "-1".to_string(),
|
||||
};
|
||||
fn inner(path: &Path) -> u64 {
|
||||
let mut size = 0;
|
||||
if let Ok(entries) = fs::read_dir(path) {
|
||||
@@ -99,57 +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 {
|
||||
let client = reqwest::Client::new();
|
||||
let resp = match client.get(&url).send().await {
|
||||
Ok(r) => r,
|
||||
let _ = app.emit("download-start", format!("{}", name));
|
||||
let window = app.get_webview_window("main").expect("main window missing");
|
||||
|
||||
let base_dir = match data_dir(&app) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
let total_size = resp.content_length().unwrap_or(0);
|
||||
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = resp.bytes_stream();
|
||||
|
||||
let downloads_path = app.path().app_local_data_dir().unwrap().join("downloads");
|
||||
let game_path = app.path().app_local_data_dir().unwrap().join("game");
|
||||
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));
|
||||
|
||||
if download_part_path.exists() {
|
||||
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||
}
|
||||
|
||||
let _ = tokio::fs::create_dir_all(&downloads_path).await;
|
||||
if let Ok(true) = tokio::fs::try_exists(&game_path.join(name.clone())).await {
|
||||
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
|
||||
}
|
||||
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||
let mut file = tokio::fs::File::create(download_part_path.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.get(&url);
|
||||
if resume_from > 0 {
|
||||
request = request.header("Range", format!("bytes={}-", resume_from));
|
||||
}
|
||||
|
||||
while let Ok(Some(chunk_result)) = timeout(Duration::from_secs(5), stream.next()).await {
|
||||
let chunk = match chunk_result {
|
||||
Ok(c) => c,
|
||||
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(_) => {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
chunk_count += 1;
|
||||
|
||||
if let Err(_) = file.write_all(&chunk).await {
|
||||
let _ = &app.emit("download-stop", format!("{}", name));
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
@@ -161,96 +295,120 @@ 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
|
||||
};
|
||||
|
||||
app.emit(
|
||||
if chunk_count % 50 == 0 || downloaded == total_size {
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::Normal),
|
||||
progress: Some(progress as u64),
|
||||
});
|
||||
}
|
||||
|
||||
let _ = app.emit(
|
||||
"download-progress",
|
||||
format!(
|
||||
"{}:{:.8}:{}:{}:{:.2}",
|
||||
&name, progress, downloaded, speed, eta_secs
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
}
|
||||
|
||||
if total_size > 0 && downloaded < total_size {
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
app.emit("download-hash-checking", format!("{}", &name))
|
||||
.unwrap();
|
||||
let _ = app.emit("download-hash-checking", format!("{}", &name));
|
||||
|
||||
let download_hash = {
|
||||
let mut file = tokio::fs::File::open(download_part_path.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut file = match tokio::fs::File::open(&download_part_path).await {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
let mut hasher = Sha512::new();
|
||||
{
|
||||
let mut buffer = [0; 8192];
|
||||
let mut buffer = [0u8; 8192];
|
||||
loop {
|
||||
let bytes_read = file.read(&mut buffer).await.unwrap();
|
||||
let bytes_read = match file.read(&mut buffer).await {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return "-1".to_string();
|
||||
}
|
||||
};
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
}
|
||||
}
|
||||
drop(file);
|
||||
format!("{:x}", hasher.finalize())
|
||||
hex::encode(hasher.finalize())
|
||||
};
|
||||
|
||||
if hash != download_hash {
|
||||
tokio::fs::remove_file(download_part_path.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
return "-1".to_string();
|
||||
}
|
||||
app.emit("download-finishing", format!("{}", &name))
|
||||
.unwrap();
|
||||
|
||||
tokio::fs::rename(download_part_path.clone(), download_zip_path.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let unzip_res = unzip_to_dir(download_zip_path.clone(), game_path.join(&name)).await;
|
||||
tokio::fs::remove_file(download_zip_path.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
if unzip_res == "-1" {
|
||||
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();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
{
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
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();
|
||||
}
|
||||
|
||||
let executable_path = if cfg!(target_os = "linux") {
|
||||
game_path.join(&name).join(&executable)
|
||||
} else {
|
||||
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||
|
||||
let unzip_res = unzip_to_dir(
|
||||
app.clone(),
|
||||
download_zip_path.clone(),
|
||||
if download_type == 2 {
|
||||
game_path
|
||||
.join(&name)
|
||||
.join(&executable)
|
||||
.join("Contents")
|
||||
.join("MacOS")
|
||||
.join(
|
||||
&executable
|
||||
.chars()
|
||||
.take(executable.chars().count() - 4)
|
||||
.collect::<String>(),
|
||||
.join("BepInEx")
|
||||
.join("plugins")
|
||||
.join(&mod_id)
|
||||
} else {
|
||||
game_path.join(&name)
|
||||
},
|
||||
name.clone(),
|
||||
)
|
||||
};
|
||||
.await;
|
||||
|
||||
let mut perms = fs::metadata(&executable_path).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&executable_path, perms).unwrap();
|
||||
let _ = tokio::fs::remove_file(&download_zip_path).await;
|
||||
|
||||
if unzip_res == "-1" {
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
return "-1".to_string();
|
||||
}
|
||||
|
||||
return "1".to_string();
|
||||
let _ = window.set_progress_bar(ProgressBarState {
|
||||
status: Some(ProgressBarStatus::None),
|
||||
progress: Some(0),
|
||||
});
|
||||
"1".to_string()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
@@ -263,12 +421,10 @@ fn launch_game(
|
||||
use_wine: bool,
|
||||
wine_command: String,
|
||||
) {
|
||||
let game_folder = app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.unwrap()
|
||||
.join("game")
|
||||
.join(&name);
|
||||
let game_folder = match data_dir(&app) {
|
||||
Ok(p) => p.join("game").join(&name),
|
||||
Err(_) => return,
|
||||
};
|
||||
if !game_folder.exists() {
|
||||
return;
|
||||
}
|
||||
@@ -278,6 +434,8 @@ 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, MessageDialogKind};
|
||||
|
||||
if !use_wine && is_running_by_path(&exe_path) {
|
||||
app.dialog()
|
||||
.message(format!(
|
||||
@@ -297,67 +455,258 @@ fn launch_game(
|
||||
let quoted_path = format!("\"{}\"", exe_path.to_string_lossy());
|
||||
let cmd = wine_command.replace("%path%", "ed_path);
|
||||
|
||||
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()
|
||||
.unwrap();
|
||||
{
|
||||
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" {
|
||||
Command::new("open")
|
||||
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)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
{
|
||||
eprintln!("Failed to launch game on macOS");
|
||||
}
|
||||
} else {
|
||||
Command::new(&exe_path)
|
||||
if (platform() == "macos" || platform() == "linux") && bepinex_folder.exists() {
|
||||
if let Err(_) = Command::new("./run_bepinex.sh")
|
||||
.arg(&executable)
|
||||
.current_dir(&game_folder)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
{
|
||||
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]
|
||||
async fn open_folder(app: AppHandle, name: String) {
|
||||
let game_path = app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.unwrap()
|
||||
.join("game")
|
||||
.join(&name);
|
||||
if game_path.exists() {
|
||||
app.opener()
|
||||
.open_path(game_path.to_string_lossy(), None::<&str>)
|
||||
.unwrap();
|
||||
} else {
|
||||
app.dialog()
|
||||
.message(format!(
|
||||
"Game folder \"{}\" not found.",
|
||||
game_path.display()
|
||||
))
|
||||
.kind(MessageDialogKind::Error)
|
||||
.title("Folder not found")
|
||||
.show(|_| {});
|
||||
fn verify_signature(body: String, signature: String, public_key: String) -> bool {
|
||||
let Ok(pem) = general_purpose::STANDARD.decode(public_key) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(pubkey) = PKey::public_key_from_pem(&pem) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(sig) = general_purpose::STANDARD.decode(signature) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut verifier = match Verifier::new_without_digest(&pubkey) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
verifier
|
||||
.verify_oneshot(&sig, body.as_bytes())
|
||||
.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()
|
||||
.with_flags(
|
||||
Flags::FIND
|
||||
| Flags::CARET_BROWSING
|
||||
| Flags::DEV_TOOLS
|
||||
| Flags::DOWNLOADS
|
||||
| Flags::FOCUS_MOVE
|
||||
| Flags::RELOAD
|
||||
| Flags::SOURCE
|
||||
| Flags::OPEN
|
||||
| Flags::PRINT
|
||||
| Flags::CONTEXT_MENU,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_window_state::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
let _ = app
|
||||
.get_webview_window("main")
|
||||
.expect("no main window")
|
||||
.set_focus();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
@@ -367,15 +716,49 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
download,
|
||||
launch_game,
|
||||
open_folder,
|
||||
folder_size
|
||||
folder_size,
|
||||
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")]
|
||||
{
|
||||
let main_window = app.get_webview_window("main").unwrap();
|
||||
main_window.create_overlay_titlebar().unwrap();
|
||||
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,24 +1,16 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.5.1",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev",
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeBuildCommand": "next build",
|
||||
"frontendDist": "../out"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"minWidth": 875,
|
||||
"minHeight": 500
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.5.1",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev",
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeBuildCommand": "next build",
|
||||
"frontendDist": "../out"
|
||||
@@ -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,10 +1,10 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.5.1",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev",
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeBuildCommand": "next build",
|
||||
"frontendDist": "../out"
|
||||
@@ -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": "15.0"
|
||||
"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,10 +1,10 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Lncvrt Games Launcher",
|
||||
"version": "1.5.1",
|
||||
"version": "1.9.1",
|
||||
"identifier": "xyz.lncvrt.games-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "next dev",
|
||||
"beforeDevCommand": "next dev --webpack",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeBuildCommand": "next build",
|
||||
"frontendDist": "../out"
|
||||
@@ -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,57 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, ReactNode } from 'react'
|
||||
import { DownloadProgress } from './types/DownloadProgress'
|
||||
import { VersionsConfig } from './types/VersionsConfig'
|
||||
import { NormalConfig } from './types/NormalConfig'
|
||||
import { ServerVersionsResponse } from './types/ServerVersionsResponse'
|
||||
import { GameVersion } from './types/GameVersion'
|
||||
import { Game } from './types/Game'
|
||||
|
||||
type GlobalCtxType = {
|
||||
serverVersionList: ServerVersionsResponse | null
|
||||
selectedVersionList: string[]
|
||||
setSelectedVersionList: (v: string[]) => void
|
||||
downloadProgress: DownloadProgress[]
|
||||
setDownloadProgress: (v: DownloadProgress[]) => void
|
||||
showPopup: boolean
|
||||
setShowPopup: (v: boolean) => void
|
||||
popupMode: number | null
|
||||
setPopupMode: (v: number | null) => void
|
||||
fadeOut: boolean
|
||||
setFadeOut: (v: boolean) => void
|
||||
downloadedVersionsConfig: VersionsConfig | null
|
||||
setDownloadedVersionsConfig: (v: VersionsConfig | null) => void
|
||||
normalConfig: NormalConfig | null
|
||||
setNormalConfig: (v: NormalConfig | null) => void
|
||||
managingVersion: string | null
|
||||
setManagingVersion: (v: string | null) => void
|
||||
setSelectedGame: (v: number | null) => void
|
||||
getVersionInfo: (id: string | undefined) => GameVersion | undefined
|
||||
getGameInfo: (id: number | undefined) => Game | undefined
|
||||
getListOfGames: () => Game[]
|
||||
getVersionsAmountData: (gameId: number) => {
|
||||
installed: number
|
||||
total: number
|
||||
} | null
|
||||
viewingInfoFromDownloads: boolean
|
||||
version: string | null
|
||||
}
|
||||
|
||||
const GlobalCtx = createContext<GlobalCtxType | null>(null)
|
||||
|
||||
export const useGlobal = () => {
|
||||
const ctx = useContext(GlobalCtx)
|
||||
if (!ctx) throw new Error('useGlobal must be inside GlobalProvider')
|
||||
return ctx
|
||||
}
|
||||
|
||||
export const GlobalProvider = ({
|
||||
children,
|
||||
value
|
||||
}: {
|
||||
children: ReactNode
|
||||
value: GlobalCtxType
|
||||
}) => {
|
||||
return <GlobalCtx.Provider value={value}>{children}</GlobalCtx.Provider>
|
||||
}
|
||||
@@ -1,167 +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);
|
||||
}
|
||||
|
||||
.green-theme {
|
||||
--col0: rgb(8, 32, 8);
|
||||
--col1: rgb(16, 40, 16);
|
||||
--col2: rgb(32, 56, 32);
|
||||
--col3: rgb(48, 72, 48);
|
||||
--col4: rgb(64, 88, 64);
|
||||
--col5: rgb(80, 104, 80);
|
||||
--col6: rgb(96, 120, 96);
|
||||
--col7: rgb(112, 136, 112);
|
||||
--col8: rgb(128, 152, 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);
|
||||
}
|
||||
|
||||
.mint-theme {
|
||||
--col0: rgb(8, 32, 16);
|
||||
--col1: rgb(16, 48, 32);
|
||||
--col2: rgb(32, 56, 40);
|
||||
--col3: rgb(48, 72, 48);
|
||||
--col4: rgb(64, 88, 66);
|
||||
--col5: rgb(80, 104, 65);
|
||||
--col6: rgb(96, 120, 80);
|
||||
--col7: rgb(112, 136, 85);
|
||||
--col8: rgb(128, 152, 95);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import BerryNoColor from '../assets/berries/BerryNoColor.png'
|
||||
|
||||
export default function RandomBerry () {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const imgRef = useRef<HTMLImageElement>(document.createElement('img'))
|
||||
|
||||
useEffect(() => {
|
||||
imgRef.current.src = BerryNoColor.src
|
||||
const canvas = canvasRef.current
|
||||
const ctx = canvas?.getContext('2d')
|
||||
if (!canvas || !ctx) return
|
||||
|
||||
canvas.width = 24
|
||||
canvas.height = 24
|
||||
|
||||
let frame: number
|
||||
const frequency = 5
|
||||
|
||||
const update = () => {
|
||||
const t = (performance.now() / 1000) * frequency
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(imgRef.current, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
const data = imageData.data
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const gray = data[i]
|
||||
const r = (Math.sin(t) * 0.5 + 0.5) * 255 * (gray / 255)
|
||||
const g = (Math.sin(t + 2) * 0.5 + 0.5) * 255 * (gray / 255)
|
||||
const b = (Math.sin(t + 4) * 0.5 + 0.5) * 255 * (gray / 255)
|
||||
data[i] = r
|
||||
data[i + 1] = g
|
||||
data[i + 2] = b
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
frame = requestAnimationFrame(update)
|
||||
}
|
||||
|
||||
imgRef.current.onload = () => update()
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [])
|
||||
|
||||
return <canvas ref={canvasRef} style={{ width: 24, height: 24 }} />
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import './Sidebar.css'
|
||||
import Icon from '@/assets/Icon.png'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCog,
|
||||
faDownload,
|
||||
faGamepad,
|
||||
faHexagonNodes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Lexend } from 'next/font/google'
|
||||
|
||||
const lexend = Lexend({
|
||||
subsets: ['latin']
|
||||
})
|
||||
|
||||
export default function Sidebar () {
|
||||
const {
|
||||
normalConfig,
|
||||
getListOfGames,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
downloadProgress
|
||||
} = useGlobal()
|
||||
|
||||
const pathname = usePathname()
|
||||
const params = useSearchParams()
|
||||
|
||||
return (
|
||||
<aside className='sidebar'>
|
||||
<div
|
||||
className='macos-drag'
|
||||
hidden={platform() != 'macos'}
|
||||
onMouseDown={e => {
|
||||
if (e.buttons === 1) {
|
||||
e.detail === 2
|
||||
? getCurrentWindow().toggleMaximize()
|
||||
: getCurrentWindow().startDragging()
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className='logo'
|
||||
style={{
|
||||
marginTop:
|
||||
platform() == 'windows'
|
||||
? '32px'
|
||||
: platform() == 'macos'
|
||||
? '28px'
|
||||
: ''
|
||||
}}
|
||||
onMouseDown={e => {
|
||||
if (platform() != 'macos') return
|
||||
if (e.buttons === 1) {
|
||||
e.detail === 2
|
||||
? getCurrentWindow().toggleMaximize()
|
||||
: getCurrentWindow().startDragging()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image draggable={false} src={Icon} width={36} height={36} alt='' />
|
||||
<p className={`ml-1 text-[16px] whitespace-nowrap ${lexend.className}`}>
|
||||
Lncvrt Games Launcher
|
||||
</p>
|
||||
</div>
|
||||
<nav className='nav-links overflow-auto pt-2'>
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/'
|
||||
className={`link relative flex items-center ${
|
||||
pathname === '/' || pathname === '/game' ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Games
|
||||
</Link>
|
||||
{getListOfGames()
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
.map(i => (
|
||||
<Link
|
||||
key={i.id}
|
||||
draggable={false}
|
||||
href={'/game?id=' + i.id}
|
||||
className={`link ${
|
||||
pathname === '/game' && Number(params.get('id') || 0) == i.id
|
||||
? 'active'
|
||||
: ''
|
||||
} ml-auto w-50 ${
|
||||
normalConfig?.settings.alwaysShowGamesInSidebar ||
|
||||
pathname === '/' ||
|
||||
pathname === '/game'
|
||||
? ''
|
||||
: 'hidden'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<FontAwesomeIcon icon={faGamepad} className='mr-1' />
|
||||
<span className='truncate max-w-full'>{i.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/settings'
|
||||
className={`link ${pathname === '/settings' ? 'active' : ''}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
|
||||
className='link mr-auto'
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className='mr-1' /> Community
|
||||
</button>
|
||||
</nav>
|
||||
<div
|
||||
className='sidebar-downloads'
|
||||
hidden={downloadProgress.length == 0}
|
||||
onClick={() => {
|
||||
setPopupMode(1)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faDownload} /> Downloads
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
|
||||
export default function VersionChangelog () {
|
||||
const {
|
||||
getGameInfo,
|
||||
getVersionInfo,
|
||||
managingVersion,
|
||||
downloadedVersionsConfig
|
||||
} = useGlobal()
|
||||
if (!managingVersion || !downloadedVersionsConfig) return <></>
|
||||
|
||||
const versionInfo = getVersionInfo(managingVersion)
|
||||
const gameInfo = getGameInfo(versionInfo?.game)
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Viewing changelog for {versionInfo?.displayName}
|
||||
</p>
|
||||
<div className='popup-content text-center p-2'>
|
||||
<span
|
||||
className='whitespace-pre-wrap'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: versionInfo?.changelog ? atob(versionInfo.changelog) : ''
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faCheck,
|
||||
faCode,
|
||||
faHardDrive,
|
||||
faShieldHalved,
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useEffect, useState } from 'react'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
export default function VersionInfo () {
|
||||
const {
|
||||
getGameInfo,
|
||||
getVersionInfo,
|
||||
managingVersion,
|
||||
downloadedVersionsConfig,
|
||||
viewingInfoFromDownloads,
|
||||
setPopupMode
|
||||
} = useGlobal()
|
||||
if (!managingVersion || !downloadedVersionsConfig) return <></>
|
||||
|
||||
const versionInfo = getVersionInfo(managingVersion)
|
||||
const gameInfo = getGameInfo(versionInfo?.game)
|
||||
const [versionSize, setVersionSize] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
invoke<string>('folder_size', {
|
||||
version: managingVersion
|
||||
}).then(size => {
|
||||
setVersionSize(parseInt(size, 10))
|
||||
})
|
||||
}, [managingVersion, setVersionSize])
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Viewing info for {versionInfo?.displayName}
|
||||
</p>
|
||||
<div className='popup-content flex flex-col items-center justify-center gap-2 h-full'>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={viewingInfoFromDownloads}
|
||||
>
|
||||
<p>
|
||||
Installed{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
downloadedVersionsConfig.list[managingVersion] ?? 0
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={!versionInfo || versionInfo.releaseDate == 0}
|
||||
>
|
||||
<p>
|
||||
Released{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
versionInfo?.releaseDate ? versionInfo.releaseDate * 1000 : 0
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='entry-info-item btntheme2' hidden={!gameInfo?.official}>
|
||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div className='entry-info-item btntheme2' hidden={gameInfo?.official}>
|
||||
<FontAwesomeIcon
|
||||
icon={gameInfo?.verified ? faShieldHalved : faWarning}
|
||||
color={gameInfo?.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>{gameInfo?.verified ? 'Verified' : 'Unverified'}</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={gameInfo?.developer == null}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
||||
<p>Developer: {gameInfo?.developer}</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={viewingInfoFromDownloads || versionSize === null}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
||||
<p>
|
||||
Size on disk:{' '}
|
||||
{versionSize !== null
|
||||
? prettyBytes(versionSize, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
: 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='entry-info-item btntheme2' hidden={!versionInfo}>
|
||||
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
||||
<p>
|
||||
Size when downloaded (zipped):{' '}
|
||||
{versionInfo
|
||||
? prettyBytes(versionInfo.size, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
: 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
onClick={() => setPopupMode(4)}
|
||||
hidden={!versionInfo?.changelog}
|
||||
>
|
||||
<p>View Changelog</p>
|
||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare} color='lightgray' />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import '../Installs.css'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
import { 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'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
downloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setManagingVersion,
|
||||
getVersionInfo,
|
||||
getGameInfo,
|
||||
setSelectedGame,
|
||||
serverVersionList
|
||||
} = useGlobal()
|
||||
|
||||
const params = useSearchParams()
|
||||
const [category, setCategory] = useState<number>(-1)
|
||||
|
||||
const id = Number(params.get('id') || 0)
|
||||
if (!id) return <p>Invalid game</p>
|
||||
const game = serverVersionList?.games.find(g => g.id === id)
|
||||
if (!game) return <p>Invalid game</p>
|
||||
|
||||
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'>Installs</p>
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
}}
|
||||
title='Click to go up a level.'
|
||||
hidden={category == -1}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setSelectedGame(id)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to download more versions of this game.'
|
||||
>
|
||||
Download versions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div
|
||||
className={`downloads-scroll ${
|
||||
platform() === 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{category == -1 &&
|
||||
Object.entries(game.categoryNames).map(([key, value]) => {
|
||||
return (
|
||||
<div
|
||||
key={crypto.randomUUID()}
|
||||
className={`downloads-entry ${
|
||||
normalConfig?.settings.useLegacyInteractButtons
|
||||
? ''
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
title={
|
||||
normalConfig?.settings.useLegacyInteractButtons
|
||||
? ''
|
||||
: 'Click to view category'
|
||||
}
|
||||
onClick={() => {
|
||||
if (normalConfig?.settings.useLegacyInteractButtons) return
|
||||
setCategory(Number(key))
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{value}</p>
|
||||
|
||||
<div
|
||||
className='entry-info-item flex absolute left-0 bottom-0'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const count =
|
||||
Object.keys(
|
||||
downloadedVersionsConfig?.list ?? []
|
||||
).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
if (!info) return false
|
||||
if (
|
||||
platform() === 'linux' &&
|
||||
info.wine &&
|
||||
!normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
return false
|
||||
return (
|
||||
info.game === id && info.category == Number(key)
|
||||
)
|
||||
}).length ?? 0
|
||||
return `${count} install${count === 1 ? '' : 's'}`
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className='button absolute right-0 bottom-0'
|
||||
hidden={!normalConfig?.settings.useLegacyInteractButtons}
|
||||
title='Click to view category'
|
||||
onClick={() => setCategory(Number(key))}
|
||||
>
|
||||
Installs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{Object.keys(downloadedVersionsConfig?.list ?? []).filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
if (!info) return false
|
||||
return info.game === id
|
||||
}).length != 0 ? (
|
||||
Object.keys(downloadedVersionsConfig?.list ?? [])
|
||||
.sort((a, b) => {
|
||||
const infoA = getVersionInfo(a)
|
||||
const infoB = getVersionInfo(b)
|
||||
if (!infoA || !infoB) return -1
|
||||
return infoB.place - infoA.place
|
||||
})
|
||||
.filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
if (!info) return false
|
||||
if (
|
||||
platform() === 'linux' &&
|
||||
info.wine &&
|
||||
!normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
return false
|
||||
return (
|
||||
info.game === id &&
|
||||
(category == -1
|
||||
? info.category == -1
|
||||
: info.category == category)
|
||||
)
|
||||
})
|
||||
.map((entry, i) => (
|
||||
<div
|
||||
key={entry}
|
||||
className={`downloads-entry ${
|
||||
normalConfig?.settings.useLegacyInteractButtons
|
||||
? ''
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
title={
|
||||
normalConfig?.settings.useLegacyInteractButtons
|
||||
? ''
|
||||
: 'Click to launch game. Right-click to manage this version install'
|
||||
}
|
||||
onClick={async () => {
|
||||
if (normalConfig?.settings.useLegacyInteractButtons) 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,
|
||||
useWine: !!(
|
||||
platform() === 'linux' &&
|
||||
verInfo.wine &&
|
||||
normalConfig?.settings.useWineOnUnixWhenNeeded
|
||||
),
|
||||
wineCommand: normalConfig?.settings.wineOnUnixCommand
|
||||
})
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault()
|
||||
if (normalConfig?.settings.useLegacyInteractButtons) return
|
||||
|
||||
setManagingVersion(entry)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>
|
||||
{getVersionInfo(entry)?.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]
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
)
|
||||
}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||
<p>Uses wine</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2 absolute right-0 bottom-0'>
|
||||
<button
|
||||
className='button'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
setManagingVersion(entry)
|
||||
setPopupMode(3)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to view version info'
|
||||
>
|
||||
View Info
|
||||
</button>
|
||||
<button
|
||||
className='button'
|
||||
hidden={
|
||||
!normalConfig?.settings.useLegacyInteractButtons
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
setManagingVersion(entry)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to manage this version install'
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
<button
|
||||
className='button'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
const verInfo = getVersionInfo(entry)
|
||||
if (verInfo == undefined) return
|
||||
invoke('launch_game', {
|
||||
name: verInfo.id,
|
||||
executable: verInfo.executable
|
||||
})
|
||||
}}
|
||||
hidden={
|
||||
!normalConfig?.settings.useLegacyInteractButtons
|
||||
}
|
||||
title='Click to launch game'
|
||||
>
|
||||
Launch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No versions installed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,854 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, 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 {
|
||||
faAdd,
|
||||
faCheck,
|
||||
faChevronLeft,
|
||||
faCode,
|
||||
faDownload,
|
||||
faInfo,
|
||||
faRemove,
|
||||
faShieldHalved,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
readNormalConfig,
|
||||
readVersionsConfig,
|
||||
writeVersionsConfig
|
||||
} from './util/BazookaManager'
|
||||
import { VersionsConfig } from './types/VersionsConfig'
|
||||
import { NormalConfig } from './types/NormalConfig'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import axios from 'axios'
|
||||
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 VersionInfo from './componets/VersionInfo'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import ProgressBar from './componets/ProgressBar'
|
||||
import { notifyUser } from './util/Notifications'
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission
|
||||
} from '@tauri-apps/plugin-notification'
|
||||
import VersionChangelog from './componets/VersionChangelog'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
|
||||
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 [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 [managingVersion, setManagingVersion] = useState<string | null>(null)
|
||||
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
|
||||
useState<boolean>(false)
|
||||
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
||||
|
||||
const pathname = usePathname()
|
||||
|
||||
function getSpecialVersionsList (game?: number): GameVersion[] {
|
||||
if (!normalConfig || !serverVersionList) return []
|
||||
|
||||
return serverVersionList.versions
|
||||
.filter(
|
||||
v => !Object.keys(downloadedVersionsConfig?.list ?? []).includes(v.id)
|
||||
)
|
||||
.filter(v => {
|
||||
if (
|
||||
platform() === 'linux' &&
|
||||
v.wine &&
|
||||
!normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
return false
|
||||
|
||||
if (game && v.game != game) 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
|
||||
})
|
||||
}
|
||||
|
||||
function getVersionInfo (id: string | undefined): GameVersion | undefined {
|
||||
if (!id) return undefined
|
||||
return serverVersionList?.versions.find(v => v.id === id)
|
||||
}
|
||||
|
||||
function getGameInfo (game: number | undefined): Game | undefined {
|
||||
if (!game) return undefined
|
||||
return serverVersionList?.games.find(g => g.id === game)
|
||||
}
|
||||
|
||||
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 =
|
||||
platform() !== '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 }
|
||||
}
|
||||
|
||||
function formatEtaSmart (seconds: number) {
|
||||
if (seconds < 60) return `${Math.floor(seconds)}s`
|
||||
if (seconds < 3600)
|
||||
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
||||
if (seconds < 86400) {
|
||||
const h = Math.floor(seconds / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
return `${h}h ${m}m`
|
||||
}
|
||||
const d = Math.floor(seconds / 86400)
|
||||
const h = Math.floor((seconds % 86400) / 3600)
|
||||
return `${d}d ${h}h`
|
||||
}
|
||||
|
||||
function closePopup () {
|
||||
if (popupMode == 0 && selectedGame && pathname === '/') {
|
||||
setSelectedGame(null)
|
||||
setSelectedVersionList([])
|
||||
} else if (viewingInfoFromDownloads) {
|
||||
setViewingInfoFromDownloads(false)
|
||||
setPopupMode(0)
|
||||
} else if (popupMode == 4) {
|
||||
setPopupMode(3)
|
||||
} else {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenProgress: (() => void) | null = null
|
||||
|
||||
listen<string>('download-progress', 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
|
||||
})
|
||||
}).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?.()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const client = await app.getVersion()
|
||||
setVersion(client)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
setLoadingText('Checking latest version...')
|
||||
try {
|
||||
const response = await axios.get(
|
||||
'https://games.lncvrt.xyz/api/launcher/latest'
|
||||
)
|
||||
if (response.data !== client) {
|
||||
setOutdated(true)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setLoadingText('Failed to check latest version.')
|
||||
return
|
||||
}
|
||||
}
|
||||
setLoadingText('Downloading version list...')
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
|
||||
)
|
||||
setServerVersionList(res.data)
|
||||
} catch {
|
||||
setLoadingText('Failed to download versions list.')
|
||||
return
|
||||
}
|
||||
setLoadingText('Loading configs...')
|
||||
const normalConfig = await readNormalConfig()
|
||||
const versionsConfig = await readVersionsConfig()
|
||||
setDownloadedVersionsConfig(versionsConfig)
|
||||
setNormalConfig(normalConfig)
|
||||
setLoadingText('Finishing...')
|
||||
setLoading(false)
|
||||
|
||||
if (!(await isPermissionGranted())) {
|
||||
await requestPermission()
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'production') return
|
||||
const handler = (e: MouseEvent) => e.preventDefault()
|
||||
document.addEventListener('contextmenu', handler)
|
||||
return () => document.removeEventListener('contextmenu', handler)
|
||||
}, [])
|
||||
|
||||
async function downloadVersions (): Promise<void> {
|
||||
const list = selectedVersionList
|
||||
setSelectedVersionList([])
|
||||
|
||||
const newDownloads = list.map(
|
||||
version =>
|
||||
new DownloadProgress(version, 0, 0, false, true, false, false, 0, 0)
|
||||
)
|
||||
|
||||
setDownloadProgress(newDownloads)
|
||||
|
||||
for (const download of newDownloads) {
|
||||
const info = getVersionInfo(download.version)
|
||||
if (!info) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
const gameInfo = getGameInfo(info.game)
|
||||
if (!gameInfo) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === download.version ? { ...d, queued: false } : d
|
||||
)
|
||||
)
|
||||
const res = await invoke<string>('download', {
|
||||
url: info.downloadUrl,
|
||||
name: info.id,
|
||||
executable: info.executable,
|
||||
hash: info.sha512sum
|
||||
})
|
||||
if (res == '1') {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
let data = downloadedVersionsConfig
|
||||
if (!data) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
const date = Date.now()
|
||||
data.list = { ...data.list, [download.version]: date }
|
||||
setDownloadedVersionsConfig(data)
|
||||
writeVersionsConfig(data)
|
||||
} else {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === download.version
|
||||
? { ...d, queued: false, failed: true, progress: 0 }
|
||||
: d
|
||||
)
|
||||
)
|
||||
if (normalConfig?.settings.allowNotifications)
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${info.displayName} has failed.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (normalConfig?.settings.allowNotifications)
|
||||
await notifyUser('Downloads Finished', 'All downloads have finished.')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
<body
|
||||
className={
|
||||
normalConfig?.settings.theme === 1
|
||||
? 'red-theme'
|
||||
: normalConfig?.settings.theme === 2
|
||||
? 'green-theme'
|
||||
: normalConfig?.settings.theme === 3
|
||||
? 'blue-theme'
|
||||
: normalConfig?.settings.theme === 4
|
||||
? 'purple-theme'
|
||||
: normalConfig?.settings.theme === 5
|
||||
? 'mint-theme'
|
||||
: 'dark-theme'
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<div className='w-screen 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
|
||||
}}
|
||||
>
|
||||
<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-(--col3) h-8.25 bg-(--col1)'
|
||||
style={{
|
||||
display: platform() === 'windows' ? 'block' : 'none'
|
||||
}}
|
||||
/>
|
||||
<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 ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Select versions to download
|
||||
</p>
|
||||
<div className='popup-content'>
|
||||
{getSpecialVersionsList(selectedGame).map(
|
||||
(v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
<div className='flex items-center'>
|
||||
<p
|
||||
className={`text-2xl truncate ${
|
||||
selectedVersionList.includes(v.id)
|
||||
? 'max-w-84.5'
|
||||
: 'max-w-91.5'
|
||||
}`}
|
||||
>
|
||||
{v.displayName}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className='button btntheme3 right-20.75 bottom-1.75'
|
||||
onClick={() => {
|
||||
setSelectedVersionList(prev =>
|
||||
prev.includes(v.id)
|
||||
? prev.filter(i => i !== v.id)
|
||||
: [...prev, v.id]
|
||||
)
|
||||
}}
|
||||
title={
|
||||
selectedVersionList.includes(v.id)
|
||||
? 'This version will be downloaded. Click to remove from the list of versions that will be downloaded.'
|
||||
: 'This version will NOT be downloaded. Click to add from the list of versions that will be downloaded.'
|
||||
}
|
||||
>
|
||||
{selectedVersionList.includes(v.id) ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faRemove} />{' '}
|
||||
Remove
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAdd} /> Add
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme3 right-1.5 bottom-1.75'
|
||||
onClick={() => {
|
||||
setManagingVersion(v.id)
|
||||
setViewingInfoFromDownloads(true)
|
||||
setPopupMode(3)
|
||||
}}
|
||||
title='Click to view version info'
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfo} /> Info
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : popupMode === 0 && !selectedGame ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Select a game to download
|
||||
</p>
|
||||
<div className='popup-content'>
|
||||
{serverVersionList?.games.map((v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
<p className='text-2xl'>{v.name}</p>
|
||||
<div className='flex gap-2'>
|
||||
<div
|
||||
className='entry-info-item btntheme3'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const data = getVersionsAmountData(v.id)
|
||||
if (!data) return 'N/A'
|
||||
return `${data.installed}/${data.total}`
|
||||
})()}{' '}
|
||||
versions installed
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme3'
|
||||
hidden={!v.official}
|
||||
title='This game is official.'
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
color='#19c84b'
|
||||
/>
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme3'
|
||||
hidden={v.official}
|
||||
title={
|
||||
v.verified
|
||||
? 'This game is verified to be safe'
|
||||
: 'This game is NOT verified to be save. Proceed with caution.'
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
v.verified ? faShieldHalved : faWarning
|
||||
}
|
||||
color={v.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>
|
||||
{v.verified ? 'Verified' : 'Unverified'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme3 mt-2'
|
||||
hidden={v.developer == null}
|
||||
title={`The developer of ${v.name} is ${v.developer}.`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCode}
|
||||
color='lightgray'
|
||||
/>
|
||||
<p>Developer: {v.developer}</p>
|
||||
</div>
|
||||
<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.`}
|
||||
>
|
||||
<>
|
||||
<FontAwesomeIcon icon={faDownload} />{' '}
|
||||
Download
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : popupMode === 1 ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>Downloads</p>
|
||||
<div className='popup-content'>
|
||||
{downloadProgress.map((v, i) => (
|
||||
<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 ? (
|
||||
<>
|
||||
<div className='flex items-center'>
|
||||
<span className='text-red-500 inline-block w-full text-center'>
|
||||
Download failed
|
||||
</span>
|
||||
<button
|
||||
className='button btntheme3 ml-30 mb-2'
|
||||
onClick={() => {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(
|
||||
d => d.version !== v.version
|
||||
)
|
||||
)
|
||||
}}
|
||||
title='Click to remove this version from this menu.'
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : v.queued ? (
|
||||
<span className='text-yellow-500 inline-block w-full text-center'>
|
||||
Queued…
|
||||
</span>
|
||||
) : v.queued ? (
|
||||
<span className='text-yellow-500 inline-block w-full text-center'>
|
||||
Queued…
|
||||
</span>
|
||||
) : v.hash_checking ? (
|
||||
<span className='text-blue-500 inline-block w-full text-center'>
|
||||
Checking hash...
|
||||
</span>
|
||||
) : v.finishing ? (
|
||||
<span className='text-green-500 inline-block w-full text-center'>
|
||||
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(
|
||||
getVersionInfo(v.version)?.size ?? 0,
|
||||
{
|
||||
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>
|
||||
</>
|
||||
) : popupMode === 2 ? (
|
||||
managingVersion ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Manage{' '}
|
||||
{getVersionInfo(managingVersion)?.displayName}
|
||||
</p>
|
||||
<div className='popup-content flex flex-col items-center justify-center gap-2 h-full'>
|
||||
<button
|
||||
className='button btntheme2'
|
||||
disabled={downloadProgress.length != 0}
|
||||
onClick={async () => {
|
||||
closePopup()
|
||||
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
const updatedList = Object.fromEntries(
|
||||
Object.entries(prev.list).filter(
|
||||
([k]) => k !== managingVersion
|
||||
)
|
||||
)
|
||||
const updatedConfig = {
|
||||
...prev,
|
||||
list: updatedList
|
||||
}
|
||||
writeVersionsConfig(updatedConfig)
|
||||
setManagingVersion(null)
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
return updatedConfig
|
||||
})
|
||||
|
||||
if (
|
||||
await exists('game/' + managingVersion, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + managingVersion, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
}}
|
||||
title='Click to uninstall this game. This will NOT remove any progress or any save files.'
|
||||
>
|
||||
Uninstall
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme2'
|
||||
onClick={async () =>
|
||||
invoke('open_folder', {
|
||||
name: managingVersion
|
||||
})
|
||||
}
|
||||
title="Click to browse the game's files."
|
||||
>
|
||||
Open Folder
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className='text-xl text-center'>
|
||||
No version selected
|
||||
</p>
|
||||
)
|
||||
) : popupMode === 3 ? (
|
||||
managingVersion && downloadedVersionsConfig ? (
|
||||
<VersionInfo />
|
||||
) : (
|
||||
<p className='text-xl text-center'>
|
||||
No version selected
|
||||
</p>
|
||||
)
|
||||
) : popupMode === 4 ? (
|
||||
managingVersion && downloadedVersionsConfig ? (
|
||||
<VersionChangelog />
|
||||
) : (
|
||||
<p className='text-xl text-center'>
|
||||
No version selected
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
{popupMode == 0 &&
|
||||
selectedGame &&
|
||||
serverVersionList != null && (
|
||||
<div className='flex justify-center'>
|
||||
<button
|
||||
className='button btntheme1 w-fit mt-2 -mb-4'
|
||||
onClick={() => {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
downloadVersions()
|
||||
}}
|
||||
disabled={downloadProgress.length != 0}
|
||||
title={
|
||||
downloadProgress.length != 0
|
||||
? "You cannot download the versions as you have another download that hasn't completed."
|
||||
: `Click to download ${
|
||||
selectedVersionList.length
|
||||
} version${
|
||||
selectedVersionList.length == 1 ? '' : 's'
|
||||
} of ${getGameInfo(selectedGame)?.name}`
|
||||
}
|
||||
>
|
||||
Download {selectedVersionList.length} version
|
||||
{selectedVersionList.length == 1 ? '' : 's'}
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1 w-fit mt-2 ml-2 -mb-4'
|
||||
onClick={() => {
|
||||
const allIds = getSpecialVersionsList(
|
||||
selectedGame
|
||||
).map(v => v.id)
|
||||
setSelectedVersionList(prev =>
|
||||
prev.length === allIds.length ? [] : allIds
|
||||
)
|
||||
}}
|
||||
title={
|
||||
selectedVersionList.length ===
|
||||
getSpecialVersionsList(selectedGame).length
|
||||
? 'Click to remove all selected versions for download.'
|
||||
: 'Click to add all selected versions for download.'
|
||||
}
|
||||
>
|
||||
{selectedVersionList.length ===
|
||||
getSpecialVersionsList(selectedGame).length
|
||||
? 'Deselect All'
|
||||
: 'Select All'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</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;
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
'use client'
|
||||
|
||||
import './styles.css'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { BirdColor } from '@/types/BerryDash/BirdColor'
|
||||
import { GetIconForUser } from '@/lib/berrydashutil'
|
||||
import Image from 'next/image'
|
||||
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 Dropdown from '@/components/Dropdown'
|
||||
|
||||
interface BaseEntry {
|
||||
id: number
|
||||
username: string
|
||||
value: number
|
||||
icon: number
|
||||
overlay: number
|
||||
birdColor: BirdColor
|
||||
overlayColor: BirdColor
|
||||
customIcon: string | null
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
highScore: string
|
||||
totalNormalBerries: string
|
||||
totalPoisonBerries: string
|
||||
totalSlowBerries: string
|
||||
totalUltraBerries: string
|
||||
totalSpeedyBerries: string
|
||||
totalCoinBerries: string
|
||||
totalRandomBerries: string
|
||||
totalAntiBerries: string
|
||||
totalGoldenBerries: string
|
||||
coins: string
|
||||
}
|
||||
|
||||
interface LeaderboardEntry extends BaseEntry {
|
||||
type: 'leaderboard'
|
||||
value: number
|
||||
}
|
||||
|
||||
interface Account extends BaseEntry {
|
||||
type: 'account'
|
||||
stats: Stats
|
||||
xp: bigint
|
||||
}
|
||||
|
||||
export function calculateXP (
|
||||
normalBerries: bigint,
|
||||
poisonBerries: bigint,
|
||||
slowBerries: bigint,
|
||||
ultraBerries: bigint,
|
||||
speedyBerries: bigint,
|
||||
coinBerries: bigint,
|
||||
randomBerries: bigint,
|
||||
antiBerries: bigint,
|
||||
goldenBerries: bigint
|
||||
): bigint {
|
||||
let totalXp = 0n
|
||||
totalXp += normalBerries
|
||||
totalXp -= poisonBerries
|
||||
totalXp -= slowBerries
|
||||
totalXp += ultraBerries * 5n
|
||||
totalXp += speedyBerries * 10n
|
||||
totalXp += coinBerries * 10n
|
||||
totalXp += randomBerries
|
||||
totalXp -= antiBerries
|
||||
totalXp += goldenBerries * 4n
|
||||
|
||||
if (totalXp < 0n) totalXp = 0n
|
||||
return totalXp
|
||||
}
|
||||
|
||||
export function calculateLevel (xp: bigint): number {
|
||||
const levelDivisor = 50.0
|
||||
|
||||
const xpNumber = Number(xp)
|
||||
const discriminant = 95 * 95 + levelDivisor * 2 * xpNumber
|
||||
const level = (-95 + Math.sqrt(discriminant)) / levelDivisor
|
||||
return Math.floor(level) + 1
|
||||
}
|
||||
|
||||
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(
|
||||
'https://games.lncvrt.xyz/api/berrydash/account?username='
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (
|
||||
(await verifySignature(JSON.stringify(data), signature)) &&
|
||||
data.success
|
||||
) {
|
||||
let accounts = data.data as Account[]
|
||||
|
||||
accounts = accounts.map(acc => {
|
||||
const xp = calculateXP(
|
||||
BigInt(acc.stats.totalNormalBerries),
|
||||
BigInt(acc.stats.totalPoisonBerries),
|
||||
BigInt(acc.stats.totalSlowBerries),
|
||||
BigInt(acc.stats.totalUltraBerries),
|
||||
BigInt(acc.stats.totalSpeedyBerries),
|
||||
BigInt(acc.stats.totalCoinBerries),
|
||||
BigInt(acc.stats.totalRandomBerries),
|
||||
BigInt(acc.stats.totalAntiBerries),
|
||||
BigInt(acc.stats.totalGoldenBerries)
|
||||
)
|
||||
return { ...acc, xp }
|
||||
})
|
||||
|
||||
accounts.sort((a, b) => (b.xp > a.xp ? 1 : b.xp < a.xp ? -1 : 0))
|
||||
|
||||
setEntries(accounts)
|
||||
} else console.log('Failed')
|
||||
} else {
|
||||
const response = await fetch(
|
||||
'https://games.lncvrt.xyz/api/berrydash/leaderboard/' +
|
||||
(selected == 0
|
||||
? 'score'
|
||||
: selected == 1
|
||||
? 'berry?berry=' + selectedBerryOption
|
||||
: selected == 2
|
||||
? 'coin'
|
||||
: selected == 5
|
||||
? 'legacy'
|
||||
: 'total')
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (
|
||||
(await verifySignature(JSON.stringify(data), signature)) &&
|
||||
data.success
|
||||
)
|
||||
setEntries(data.data as LeaderboardEntry[])
|
||||
}
|
||||
} catch {
|
||||
setEntries([])
|
||||
}
|
||||
setLoading(false)
|
||||
}, [selected, selectedBerryOption])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Lncvrt Games - Berry Dash Leaderboards'
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selected != -1) setTimeout(() => Refresh(), 0)
|
||||
}, [selected, Refresh])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<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={() => {
|
||||
setEntries([])
|
||||
Refresh()
|
||||
}}
|
||||
title='Click to refresh the leaderboards.'
|
||||
hidden={selected == -1}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
if (selected == -1) router.push('/main/game?id=1')
|
||||
else {
|
||||
setSelected(-1)
|
||||
setSelectedBerryOption(0)
|
||||
setEntries([])
|
||||
}
|
||||
}}
|
||||
title='Click to go up a level.'
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`box ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{selected == -1 ? (
|
||||
<>
|
||||
<p className='text-center mt-2 text-xl'>Select a Leaderboard</p>
|
||||
<div className='flex flex-col gap-2 mt-2 items-center justify-center'>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(0)}
|
||||
>
|
||||
Score Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(1)}
|
||||
>
|
||||
Berry Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(2)}
|
||||
>
|
||||
Coins Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(3)}
|
||||
>
|
||||
Level Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(4)}
|
||||
>
|
||||
Total XP Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(5)}
|
||||
>
|
||||
Legacy Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(6)}
|
||||
>
|
||||
Total Berries Leaderboard
|
||||
</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 ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-128px)]'
|
||||
: 'h-[calc(100vh-96px)]'
|
||||
} px-1`}
|
||||
>
|
||||
{entries.map((item, index) => {
|
||||
const isAccount = 'stats' in item
|
||||
const isLeaderboard = 'value' in item && !isAccount
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className='leaderboard-entry flex justify-between items-center'
|
||||
>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Image
|
||||
src={
|
||||
!item.customIcon
|
||||
? `https://games.lncvrt.xyz/api/berrydash/render-icon?icon=${
|
||||
item.icon == 1
|
||||
? GetIconForUser(item.id)
|
||||
: item.icon
|
||||
}&overlay=${item.overlay}&birdR=${
|
||||
item.birdColor[0]
|
||||
}&birdG=${item.birdColor[1]}&birdB=${
|
||||
item.birdColor[2]
|
||||
}&overlayR=${item.overlayColor[0]}&overlayG=${
|
||||
item.overlayColor[1]
|
||||
}&overlayB=${item.overlayColor[2]}`
|
||||
: `https://games.lncvrt.xyz/api/berrydash/icon-marketplace/icon?id=${item.customIcon}&raw=true`
|
||||
}
|
||||
width={48}
|
||||
height={48}
|
||||
alt=''
|
||||
className='pointer-events-none scale-x-[-1]'
|
||||
/>
|
||||
<p>
|
||||
{item.username} (#{index + 1})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLeaderboard ? (
|
||||
<p>
|
||||
{selected === 1 || selected === 6
|
||||
? 'Berries'
|
||||
: selected === 2
|
||||
? 'Coins'
|
||||
: 'Score'}
|
||||
: {item.value.toLocaleString()}
|
||||
</p>
|
||||
) : isAccount ? (
|
||||
<p>
|
||||
{selected === 3 ? 'Level' : 'XP'}:{' '}
|
||||
{selected === 3
|
||||
? calculateLevel(item.xp).toLocaleString()
|
||||
: item.xp.toLocaleString()}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</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;
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
'use client'
|
||||
|
||||
import '../Installs.css'
|
||||
import { useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
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 { openFolder } from '@/lib/util'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
setManagingVersion,
|
||||
setSelectedGame,
|
||||
category,
|
||||
setCategory,
|
||||
downloadVersions,
|
||||
downloadProgress,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
versions
|
||||
} = useGlobal()
|
||||
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
|
||||
const id = Number(params.get('id') || 0)
|
||||
const game = serverVersionList?.games.find(g => g.id === id)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [setSelectedVersionList, showPopup])
|
||||
|
||||
if (!id || !game) return <p>Invalid game</p>
|
||||
|
||||
const needsRevisionUpdate = (
|
||||
lastRevision: number | undefined,
|
||||
version: string
|
||||
) => {
|
||||
if (!lastRevision) return false
|
||||
return (
|
||||
lastRevision > 0 &&
|
||||
(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'>
|
||||
<p
|
||||
className={`text-3xl truncate ${
|
||||
category != -1
|
||||
? 'w-[calc(100vw-495px)]'
|
||||
: game.id == 1 || game.id == 9
|
||||
? 'w-[calc(100vw-560px)]'
|
||||
: 'w-[calc(100vw-440px)]'
|
||||
}`}
|
||||
>
|
||||
{game.name} Installs
|
||||
</p>
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
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={() => {
|
||||
setCategory(-1)
|
||||
}}
|
||||
title='Click to go up a level.'
|
||||
hidden={category == -1}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setSelectedGame(id)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to download more versions of this game.'
|
||||
>
|
||||
Download versions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div
|
||||
className={`downloads-scroll ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{category == -1 &&
|
||||
Object.entries(game.categoryNames)
|
||||
.filter(([key]) => {
|
||||
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 && !linuxUseWine)
|
||||
return false
|
||||
|
||||
return info.game === id && info.category === Number(key)
|
||||
}).length
|
||||
|
||||
return count >= 1
|
||||
})
|
||||
.sort(([a], [b]) => Number(b) - Number(a))
|
||||
.map(([key, value]) => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={'downloads-entry'}
|
||||
title={'Click to view category'}
|
||||
onClick={() => setCategory(Number(key))}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{value}</p>
|
||||
|
||||
<div
|
||||
className='entry-info-item flex absolute left-0 bottom-0'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
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 &&
|
||||
!linuxUseWine
|
||||
)
|
||||
return false
|
||||
return (
|
||||
info.game === id &&
|
||||
info.category == Number(key)
|
||||
)
|
||||
}).length ?? 0
|
||||
return `${count} install${count === 1 ? '' : 's'}`
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{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) && (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No versions installed</p>
|
||||
</div>
|
||||
)}
|
||||
{filteredVersions
|
||||
.sort((a, b) => {
|
||||
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
|
||||
})
|
||||
.map(v => {
|
||||
const versionInfo = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!versionInfo) return
|
||||
|
||||
return (
|
||||
<div
|
||||
key={v}
|
||||
className={'downloads-entry'}
|
||||
title={
|
||||
'Click to launch game. Right-click to manage this version install'
|
||||
}
|
||||
onClick={async () => {
|
||||
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.',
|
||||
{
|
||||
title: 'Revision Update',
|
||||
kind: 'warning'
|
||||
}
|
||||
)
|
||||
if (answer) {
|
||||
const answer2 = await ask(
|
||||
'Are you sure you want to update? If you did not read the last popup, please go back and read it.',
|
||||
{
|
||||
title: 'Revision Update',
|
||||
kind: 'warning'
|
||||
}
|
||||
)
|
||||
if (!answer2) return
|
||||
|
||||
//open downloads popup
|
||||
setPopupMode(1)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
|
||||
//uninstall
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== v
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
await exists('game/' + v, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + v, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
|
||||
//reinstall
|
||||
setSelectedVersionList([v])
|
||||
downloadVersions([
|
||||
{
|
||||
id: v,
|
||||
type: 0
|
||||
}
|
||||
])
|
||||
} else {
|
||||
openFolder(v)
|
||||
}
|
||||
return
|
||||
}
|
||||
invoke('launch_game', {
|
||||
name: versionInfo.id,
|
||||
executable: versionInfo.executable,
|
||||
displayName: versionInfo.displayName,
|
||||
useWine: !!(
|
||||
platform() == 'linux' &&
|
||||
versionInfo.wine &&
|
||||
linuxUseWine
|
||||
),
|
||||
wineCommand: linuxWineCommand
|
||||
})
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault()
|
||||
setManagingVersion(v)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<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.'
|
||||
>
|
||||
<p>
|
||||
Installed{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
versionsList[v]
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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' && versionInfo.wine) ||
|
||||
needsRevisionUpdate(versionInfo.lastRevision, v)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||
<p>Uses wine</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={
|
||||
!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>
|
||||
</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,150 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import './Installs.css'
|
||||
import { useGlobal } from './GlobalProvider'
|
||||
import Link from 'next/link'
|
||||
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
|
||||
} = 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 && downloadedVersionsConfig.list.length ? (
|
||||
getListOfGames()
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
.map(i => (
|
||||
<div
|
||||
key={i.id}
|
||||
className={`downloads-entry ${
|
||||
normalConfig?.settings.useLegacyInteractButtons
|
||||
? ''
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
title={
|
||||
normalConfig?.settings.useLegacyInteractButtons
|
||||
? ''
|
||||
: 'Click to view game installs'
|
||||
}
|
||||
onClick={() => {
|
||||
if (normalConfig?.settings.useLegacyInteractButtons) return
|
||||
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>
|
||||
|
||||
<Link
|
||||
className='button absolute right-0 bottom-0'
|
||||
href={'/game?id=' + i.id}
|
||||
hidden={!normalConfig?.settings.useLegacyInteractButtons}
|
||||
title='Click to view game installs'
|
||||
>
|
||||
Installs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No games installed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Setting } from '../componets/Setting'
|
||||
import { writeNormalConfig } from '../util/BazookaManager'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
import { copyToClipboard } from '../util/Clipboard'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
|
||||
export default function Settings () {
|
||||
const [allowNotifications, setAllowNotifications] = useState(false)
|
||||
const [alwaysShowGamesInSidebar, setAlwaysShowGamesInSidebar] =
|
||||
useState(false)
|
||||
const [useLegacyInteractButtons, setUseLegacyInteractButtons] =
|
||||
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
|
||||
)
|
||||
setUseLegacyInteractButtons(
|
||||
normalConfig.settings.useLegacyInteractButtons
|
||||
)
|
||||
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 () => {
|
||||
while (normalConfig != null) {
|
||||
setAllowNotifications(!allowNotifications)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
allowNotifications: !allowNotifications
|
||||
}
|
||||
})
|
||||
writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
allowNotifications: !allowNotifications
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}}
|
||||
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 () => {
|
||||
while (normalConfig != null) {
|
||||
setAlwaysShowGamesInSidebar(!alwaysShowGamesInSidebar)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
|
||||
}
|
||||
})
|
||||
writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}}
|
||||
title="This setting will make it so when you are on a page like this, the games won't disappear."
|
||||
/>
|
||||
<Setting
|
||||
label='Show Installs/Launch/Manage Buttons'
|
||||
value={useLegacyInteractButtons}
|
||||
onChange={async () => {
|
||||
while (normalConfig != null) {
|
||||
setUseLegacyInteractButtons(!useLegacyInteractButtons)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
useLegacyInteractButtons: !useLegacyInteractButtons
|
||||
}
|
||||
})
|
||||
writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
useLegacyInteractButtons: !useLegacyInteractButtons
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}}
|
||||
title='Enable the legacy method of using the installs/launch/manage buttons. In the future this setting may be removed so try and get used to the new method.'
|
||||
/>
|
||||
<Setting
|
||||
label='Use wine when needed to launch games'
|
||||
value={useWineOnUnixWhenNeeded}
|
||||
onChange={async () => {
|
||||
while (normalConfig != null) {
|
||||
setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded)
|
||||
normalConfig.settings.useWineOnUnixWhenNeeded =
|
||||
!useWineOnUnixWhenNeeded
|
||||
await writeNormalConfig(normalConfig)
|
||||
break
|
||||
}
|
||||
}}
|
||||
className={platform() == 'linux' ? '' : 'hidden'}
|
||||
/>
|
||||
<p hidden={!(platform() == 'linux' && useWineOnUnixWhenNeeded)}>
|
||||
Wine Command:
|
||||
</p>
|
||||
<input
|
||||
type='text'
|
||||
value={wineOnUnixCommand}
|
||||
onChange={async e => {
|
||||
while (normalConfig != null) {
|
||||
setWineOnUnixCommand(e.target.value)
|
||||
normalConfig.settings.wineOnUnixCommand = e.target.value
|
||||
await writeNormalConfig(normalConfig)
|
||||
break
|
||||
}
|
||||
}}
|
||||
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 => {
|
||||
const newTheme = parseInt(e.target.value)
|
||||
while (normalConfig != null) {
|
||||
setTheme(newTheme)
|
||||
setNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
theme: newTheme
|
||||
}
|
||||
})
|
||||
writeNormalConfig({
|
||||
...normalConfig,
|
||||
settings: {
|
||||
...normalConfig.settings,
|
||||
theme: newTheme
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value={0}>Dark (default)</option>
|
||||
<option value={1}>Red</option>
|
||||
<option value={2}>Green</option>
|
||||
<option value={3}>Blue</option>
|
||||
<option value={4}>Purple</option>
|
||||
<option value={5}>Mint</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export class DownloadProgress {
|
||||
constructor (
|
||||
public version: string,
|
||||
public progress: number,
|
||||
public progressBytes: number,
|
||||
public failed: boolean,
|
||||
public queued: boolean,
|
||||
public hash_checking: boolean,
|
||||
public finishing: boolean,
|
||||
public speed: number,
|
||||
public etaSecs: number
|
||||
) {}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface LeaderboardEntry {
|
||||
username: string
|
||||
userid: bigint
|
||||
value: bigint
|
||||
icon: number
|
||||
overlay: number
|
||||
birdColor: number[]
|
||||
overlayColor: number[]
|
||||
customIcon: string | null
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { LeaderboardEntry } from './LeaderboardEntry'
|
||||
|
||||
export interface LeaderboardResponse {
|
||||
entries: LeaderboardEntry[]
|
||||
customIcons: Record<string, string>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export class SettingsType {
|
||||
constructor (
|
||||
public allowNotifications: boolean = true,
|
||||
public alwaysShowGamesInSidebar: boolean = true,
|
||||
public useLegacyInteractButtons: boolean = false,
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,123 +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, VersionsConfigData } 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)
|
||||
return NormalConfig.import(JSON.parse(config))
|
||||
} 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.version && raw.list && raw.timestamps) {
|
||||
raw.version = version
|
||||
raw.list = raw.timestamps
|
||||
delete raw.timestamps
|
||||
|
||||
await writeFile(
|
||||
'versions.json',
|
||||
new TextEncoder().encode(JSON.stringify(raw, null, 2)),
|
||||
options
|
||||
)
|
||||
}
|
||||
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')
|
||||
}
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 1.2 MiB |
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +1,25 @@
|
||||
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'
|
||||
import { SettingProps } from '@/types/SettingProps'
|
||||
|
||||
export function Setting ({
|
||||
label,
|
||||
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' />}
|
||||
@@ -0,0 +1,214 @@
|
||||
'use client'
|
||||
|
||||
import '@/styles/sidebar.css'
|
||||
import Icon from '@/assets/Icon.png'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCog,
|
||||
faDownload,
|
||||
faGamepad,
|
||||
faHexagonNodes,
|
||||
faLayerGroup
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Lexend } from 'next/font/google'
|
||||
import React from 'react'
|
||||
|
||||
const lexend = Lexend({
|
||||
subsets: ['latin']
|
||||
})
|
||||
|
||||
export default function Sidebar () {
|
||||
const {
|
||||
serverVersionList,
|
||||
versionsList,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
downloadProgress,
|
||||
category,
|
||||
setCategory,
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine
|
||||
} = useGlobal()
|
||||
|
||||
const pathname = usePathname()
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<aside className='sidebar'>
|
||||
<div
|
||||
className='macos-drag'
|
||||
hidden={platform() != 'macos'}
|
||||
onMouseDown={e => {
|
||||
if (e.buttons === 1) {
|
||||
if (e.detail === 2) {
|
||||
getCurrentWindow().toggleMaximize()
|
||||
} else {
|
||||
getCurrentWindow().startDragging()
|
||||
}
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
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) {
|
||||
if (e.detail === 2) {
|
||||
getCurrentWindow().toggleMaximize()
|
||||
} else {
|
||||
getCurrentWindow().startDragging()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image draggable={false} src={Icon} width={36} height={36} alt='' />
|
||||
<p className={`ml-1 text-[16px] whitespace-nowrap ${lexend.className}`}>
|
||||
Lncvrt Games Launcher
|
||||
</p>
|
||||
</div>
|
||||
<nav className='nav-links overflow-auto pt-2'>
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/main'
|
||||
className={`link relative flex items-center ${
|
||||
pathname === '/main' || pathname.startsWith('/main/game')
|
||||
? 'active'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Games
|
||||
</Link>
|
||||
{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 === '/main/game' &&
|
||||
Number(params.get('id') || 0) == i.id) ||
|
||||
(i.id == 1 &&
|
||||
pathname === '/main/game/berrydash/leaderboards')
|
||||
? 'active'
|
||||
: ''
|
||||
} ml-auto w-50 ${
|
||||
sidebarAlwaysShowGames ||
|
||||
pathname === '/' ||
|
||||
pathname.startsWith('/main/game')
|
||||
? ''
|
||||
: 'hidden'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
router.push('/main/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
Object.entries(i.categoryNames).length > 0
|
||||
? faLayerGroup
|
||||
: faGamepad
|
||||
}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span className='truncate max-w-full'>{i.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
{Object.entries(i.categoryNames)
|
||||
.sort(([a], [b]) => Number(b) - Number(a))
|
||||
.filter(([key]) => {
|
||||
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 && !linuxUseWine)
|
||||
return false
|
||||
|
||||
return info.game === i.id && info.category === Number(key)
|
||||
}).length
|
||||
|
||||
return count >= 1
|
||||
})
|
||||
.map(([key, value]) => (
|
||||
<div
|
||||
key={`${i.id}-${key}`}
|
||||
draggable={false}
|
||||
className={`link ${
|
||||
((pathname === '/main/game' &&
|
||||
Number(params.get('id') || 0) == i.id) ||
|
||||
(i.id == 1 &&
|
||||
pathname === '/main/game/berrydash/leaderboards')) &&
|
||||
category == Number(key)
|
||||
? 'active'
|
||||
: ''
|
||||
} ml-auto w-47.5 ${
|
||||
sidebarAlwaysShowGames ||
|
||||
pathname === '/' ||
|
||||
pathname.startsWith('/main/game')
|
||||
? ''
|
||||
: 'hidden'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCategory(Number(key))
|
||||
router.push('/main/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<FontAwesomeIcon icon={faGamepad} className='mr-1' />
|
||||
<span className='truncate max-w-full'>{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/main/settings'
|
||||
className={`link ${pathname === '/main/settings' ? 'active' : ''}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
|
||||
className='link mr-auto'
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className='mr-1' /> Community
|
||||
</button>
|
||||
</nav>
|
||||
<div
|
||||
className='sidebar-downloads'
|
||||
hidden={downloadProgress.length == 0}
|
||||
onClick={() => {
|
||||
setPopupMode(1)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faDownload} /> Downloads
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const GetIconForUser = (user: number) => {
|
||||
if (user == 1) return -1
|
||||
else if (user == 2) return -2
|
||||
else if (user == 4) return -3
|
||||
else if (user == 3) return -4
|
||||
else return 1
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export const openFolder = async (name: string) => {
|
||||
try {
|
||||
await invoke('open_game_folder', { version: name })
|
||||
} catch (e) {
|
||||
await message(String(e), {
|
||||
title: 'Folder not found',
|
||||
kind: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const formatEtaSmart = (seconds: number) => {
|
||||
if (seconds < 60) return `${Math.floor(seconds)}s`
|
||||
if (seconds < 3600)
|
||||
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
||||
if (seconds < 86400) {
|
||||
const h = Math.floor(seconds / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
return `${h}h ${m}m`
|
||||
}
|
||||
const d = Math.floor(seconds / 86400)
|
||||
const h = Math.floor((seconds % 86400) / 3600)
|
||||
return `${d}d ${h}h`
|
||||
}
|
||||
|
||||
export const verifySignature = async (body: string, signature: string) => {
|
||||
try {
|
||||
const result = await invoke<boolean>('verify_signature', {
|
||||
body: body ?? '',
|
||||
signature: signature ?? '',
|
||||
publicKey: process.env.NEXT_PUBLIC_PUBLIC_SIGNING_KEY ?? ''
|
||||
})
|
||||
return Boolean(result)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import {
|
||||
faCheck,
|
||||
faCode,
|
||||
faDownload,
|
||||
faShieldHalved,
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
export default function GamesDownloadPopup () {
|
||||
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(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'>
|
||||
<p className='text-2xl'>{v.name}</p>
|
||||
<div className='flex gap-2'>
|
||||
<div
|
||||
className='entry-info-item btntheme3'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
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>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme3'
|
||||
hidden={!v.official}
|
||||
title='This game is official.'
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme3'
|
||||
hidden={v.official}
|
||||
title={
|
||||
v.verified
|
||||
? 'This game is verified to be safe'
|
||||
: 'This game is NOT verified to be save. Proceed with caution.'
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={v.verified ? faShieldHalved : faWarning}
|
||||
color={v.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>{v.verified ? 'Verified' : 'Unverified'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme3 mt-2'
|
||||
hidden={v.developer == null}
|
||||
title={`The developer of ${v.name} is ${v.developer}.`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
||||
<p>Developer: {v.developer}</p>
|
||||
</div>
|
||||
<button
|
||||
className='button btntheme3 right-2 bottom-2'
|
||||
onClick={() => setSelectedGame(v.id)}
|
||||
title='Click to download more versions of the game.'
|
||||
>
|
||||
<>
|
||||
<FontAwesomeIcon icon={faDownload} /> Download
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faCheck,
|
||||
faCode,
|
||||
faHardDrive,
|
||||
faShieldHalved,
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useEffect, useState } from 'react'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
import { openFolder } from '@/lib/util'
|
||||
|
||||
export default function ManageVersionPopup () {
|
||||
const {
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
managingVersion,
|
||||
viewingInfoFromDownloads,
|
||||
setManagingVersion,
|
||||
closePopup,
|
||||
setPopupMode,
|
||||
setSelectedVersionList,
|
||||
downloadVersions,
|
||||
versions
|
||||
} = useGlobal()
|
||||
const [versionSize, setVersionSize] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (viewingInfoFromDownloads) return
|
||||
invoke<string>('folder_size', {
|
||||
version: managingVersion
|
||||
}).then(size => {
|
||||
setVersionSize(Number(size))
|
||||
})
|
||||
}, [managingVersion, setVersionSize, viewingInfoFromDownloads])
|
||||
|
||||
if (!managingVersion) return <></>
|
||||
|
||||
const versionInfo = serverVersionList?.versions.find(
|
||||
vf => vf.id == managingVersion
|
||||
)
|
||||
const gameInfo = serverVersionList?.games.find(
|
||||
vf => vf.id == versionInfo?.game
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>Viewing {versionInfo?.displayName}</p>
|
||||
<div className='popup-content flex flex-col items-center justify-center gap-2 h-full'>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={viewingInfoFromDownloads}
|
||||
>
|
||||
<p>
|
||||
Installed{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
versionsList[managingVersion] ?? 0
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={!versionInfo || versionInfo.releaseDate == 0}
|
||||
>
|
||||
<p>
|
||||
Released{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
versionInfo?.releaseDate ? versionInfo.releaseDate * 1000 : 0
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='entry-info-item btntheme2' hidden={!gameInfo?.official}>
|
||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div className='entry-info-item btntheme2' hidden={gameInfo?.official}>
|
||||
<FontAwesomeIcon
|
||||
icon={gameInfo?.verified ? faShieldHalved : faWarning}
|
||||
color={gameInfo?.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>{gameInfo?.verified ? 'Verified' : 'Unverified'}</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={gameInfo?.developer == null}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
||||
<p>Developer: {gameInfo?.developer}</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
hidden={viewingInfoFromDownloads}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
||||
<p>
|
||||
Size on disk:{' '}
|
||||
{versionSize && versionSize > 0
|
||||
? prettyBytes(versionSize, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
: 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
onClick={() => setPopupMode(4)}
|
||||
hidden={!versionInfo?.changelog}
|
||||
>
|
||||
<p>View Changelog</p>
|
||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare} color='lightgray' />
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
onClick={async () => openFolder(managingVersion)}
|
||||
title="Click to browse the game's files."
|
||||
hidden={viewingInfoFromDownloads}
|
||||
>
|
||||
Open Folder
|
||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare} color='lightgray' />
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
onClick={async () => {
|
||||
closePopup()
|
||||
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== managingVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
await exists('game/' + managingVersion, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + managingVersion, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
}}
|
||||
title='Click to uninstall this game. This will NOT remove any progress or any save files.'
|
||||
hidden={viewingInfoFromDownloads}
|
||||
>
|
||||
Uninstall
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item btntheme2'
|
||||
onClick={async () => {
|
||||
//change popup to downloads
|
||||
setManagingVersion(null)
|
||||
setPopupMode(1)
|
||||
|
||||
//uninstall
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== managingVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
await exists('game/' + managingVersion, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + managingVersion, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
|
||||
//reinstall
|
||||
setSelectedVersionList([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}
|
||||
>
|
||||
Reinstall
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
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,
|
||||
selectedGame,
|
||||
setViewingInfoFromDownloads,
|
||||
downloadVersions
|
||||
} = useGlobal()
|
||||
|
||||
const game = serverVersionList?.games.find(g => g.id === selectedGame)
|
||||
|
||||
if (!selectedGame || !game) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-xl text-center'>Select versions to download</p>
|
||||
<div className='popup-content'>
|
||||
{getSpecialVersionsList(selectedGame).map((v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
<div className='flex items-center'>
|
||||
<p
|
||||
className={`text-2xl truncate ${
|
||||
selectedVersionList.includes(v.id)
|
||||
? 'max-w-84.5'
|
||||
: 'max-w-91.5'
|
||||
}`}
|
||||
>
|
||||
{v.displayName}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className='button btntheme3 right-20.75 bottom-1.75'
|
||||
onClick={() => {
|
||||
setSelectedVersionList(prev =>
|
||||
prev.includes(v.id)
|
||||
? prev.filter(i => i !== v.id)
|
||||
: [...prev, v.id]
|
||||
)
|
||||
}}
|
||||
title={
|
||||
selectedVersionList.includes(v.id)
|
||||
? 'This version will be downloaded. Click to remove from the list of versions that will be downloaded.'
|
||||
: 'This version will NOT be downloaded. Click to add from the list of versions that will be downloaded.'
|
||||
}
|
||||
>
|
||||
{selectedVersionList.includes(v.id) ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faRemove} /> Remove
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAdd} /> Add
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme3 right-1.5 bottom-1.75'
|
||||
onClick={() => {
|
||||
setManagingVersion(v.id)
|
||||
setViewingInfoFromDownloads(true)
|
||||
setPopupMode(2)
|
||||
}}
|
||||
title='Click to view version info'
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfo} /> Info
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex justify-center'>
|
||||
<button
|
||||
className='button btntheme1 w-fit mt-2 -mb-4'
|
||||
onClick={() => {
|
||||
downloadVersions(
|
||||
selectedVersionList.map(versionId => ({
|
||||
id: versionId,
|
||||
type: 0
|
||||
}))
|
||||
)
|
||||
setPopupMode(1)
|
||||
}}
|
||||
disabled={selectedVersionList.length == 0}
|
||||
title={`Download ${selectedVersionList.length} version${
|
||||
selectedVersionList.length == 1 ? '' : 's'
|
||||
} of ${game.name}`}
|
||||
>
|
||||
Download {selectedVersionList.length} version
|
||||
{selectedVersionList.length == 1 ? '' : 's'}
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1 w-fit mt-2 ml-2 -mb-4'
|
||||
onClick={() => {
|
||||
const allIds = getSpecialVersionsList(selectedGame).map(v => v.id)
|
||||
setSelectedVersionList(prev =>
|
||||
prev.length === allIds.length ? [] : allIds
|
||||
)
|
||||
}}
|
||||
title={
|
||||
selectedVersionList.length ===
|
||||
getSpecialVersionsList(selectedGame).length
|
||||
? 'Click to remove all selected versions for download.'
|
||||
: 'Click to add all selected versions for download.'
|
||||
}
|
||||
>
|
||||
{selectedVersionList.length ===
|
||||
getSpecialVersionsList(selectedGame).length
|
||||
? 'Deselect All'
|
||||
: 'Select All'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
ReactNode,
|
||||
Dispatch,
|
||||
SetStateAction
|
||||
} from 'react'
|
||||
import { DownloadProgress } from '@/types/DownloadProgress'
|
||||
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
|
||||
import { Mod } from '@/types/Mod'
|
||||
import { Store } from '@tauri-apps/plugin-store'
|
||||
import { GameVersion } from '@/types/GameVersion'
|
||||
|
||||
type GlobalCtxType = {
|
||||
serverVersionList: ServerVersionsResponse | null
|
||||
selectedVersionList: string[]
|
||||
setSelectedVersionList: (value: SetStateAction<string[]>) => void
|
||||
downloadProgress: DownloadProgress[]
|
||||
setDownloadProgress: Dispatch<SetStateAction<DownloadProgress[]>>
|
||||
showPopup: boolean
|
||||
setShowPopup: Dispatch<SetStateAction<boolean>>
|
||||
popupMode: number | null
|
||||
setPopupMode: Dispatch<SetStateAction<number | null>>
|
||||
fadeOut: boolean
|
||||
setFadeOut: Dispatch<SetStateAction<boolean>>
|
||||
managingVersion: string | null
|
||||
setManagingVersion: Dispatch<SetStateAction<string | null>>
|
||||
setSelectedGame: Dispatch<SetStateAction<number | null>>
|
||||
viewingInfoFromDownloads: boolean
|
||||
version: string | null
|
||||
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
|
||||
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)
|
||||
|
||||
export const useGlobal = () => {
|
||||
const ctx = useContext(GlobalCtx)
|
||||
if (!ctx) throw new Error('useGlobal must be inside GlobalProvider')
|
||||
return ctx
|
||||
}
|
||||
|
||||
export const GlobalProvider = ({
|
||||
children,
|
||||
value
|
||||
}: {
|
||||
children: ReactNode
|
||||
value: GlobalCtxType
|
||||
}) => {
|
||||
return <GlobalCtx.Provider value={value}>{children}</GlobalCtx.Provider>
|
||||
}
|
||||
@@ -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,20 +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);
|
||||
}
|
||||
|
||||
.logo {
|
||||
@apply flex items-center pl-2 h-10 w-60 pt-2;
|
||||
@apply bg-(--col3)/50 border-(--col4)/50;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
@@ -26,7 +22,7 @@
|
||||
}
|
||||
|
||||
.link.active {
|
||||
@apply bg-(--col2) border-(--col3);
|
||||
@apply bg-(--col2)/50 border-(--col3)/50;
|
||||
}
|
||||
|
||||
.link.active,
|
||||
@@ -35,7 +31,7 @@
|
||||
}
|
||||
|
||||
.link.active:hover {
|
||||
@apply bg-(--col3) border-(--col4);
|
||||
@apply bg-(--col3)/50 border-(--col4)/50;
|
||||
}
|
||||
|
||||
.sidebar ::-webkit-scrollbar {
|
||||
@@ -0,0 +1 @@
|
||||
export type BirdColor = [number, number, number]
|
||||
@@ -0,0 +1,27 @@
|
||||
export class DownloadProgress {
|
||||
constructor (
|
||||
public version: string,
|
||||
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 speed: number,
|
||||
public size: 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
|
||||
) {}
|
||||
}
|
||||
@@ -3,13 +3,12 @@ export interface GameVersion {
|
||||
displayName: string
|
||||
releaseDate: number
|
||||
game: number
|
||||
downloadUrl: string
|
||||
executable: string
|
||||
sha512sum: string
|
||||
size: number
|
||||
place: number
|
||||
changelog: string
|
||||
wine: number | undefined
|
||||
category: number
|
||||
lastRevision: 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
|
||||
}
|
||||
@@ -4,4 +4,5 @@ export type SettingProps = {
|
||||
onChange: (val: boolean) => void
|
||||
className?: string
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"target": "es2024",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -28,7 +28,9 @@
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
"**/*.mts",
|
||||
"next.config.ts",
|
||||
"eslint.config.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||