Compare commits

..
66 Commits
Author SHA1 Message Date
Lncvrt 066f4aae8c 1.9.1 2026-04-13 17:33:24 -07:00
Lncvrt 2ebe127def Set app category & update dependencies 2026-04-13 17:33:04 -07:00
Lncvrt e0300acb6d Dropdown opening animation
This part is vibecoded
2026-04-13 15:12:15 -07:00
Lncvrt d18dc5afb7 Move away from native dropdowns, it looks inconsistent across systems 2026-04-13 15:01:54 -07:00
Lncvrt d20c99651c No point in this 2026-04-13 14:41:23 -07:00
Lncvrt 86a5a7b744 Re-add this back 2026-04-13 14:40:49 -07:00
Lncvrt 18f8c2316f Improvements 2026-04-13 14:11:46 -07:00
Lncvrt 12914e42dd More fixes 2026-04-13 13:28:58 -07:00
Lncvrt 91ff7caaed Fix using bash forcefully even if it doesn't exist for launching with wine 2026-04-13 13:20:36 -07:00
Lncvrt fad7073607 oop 2026-04-13 13:11:44 -07:00
Lncvrt beb5ce175e FIx issue with wine command input box showing even if wine is not enabled on linux & fix a notification issue 2026-04-13 13:08:43 -07:00
Lncvrt b8498d36e4 Add dashboard button to XPS game 2026-04-06 17:38:13 -07:00
Lncvrt 141d17fea0 Fix issues with macOS app launching on non .app executables 2026-04-06 13:05:21 -07:00
Lncvrt 138275d752 Fix for windows 2026-04-03 13:42:03 -07:00
Lncvrt 1f035882f7 1.9.0 2026-04-03 11:38:28 -07:00
Lncvrt 68cd3c891d Update typescript & node type 2026-04-03 11:37:25 -07:00
Lncvrt 65bd6cfe82 Fix dev mode issues on Windows 2026-04-03 11:32:50 -07:00
Lncvrt d91314112d Should be ready for release now. Will do some testing before. 2026-04-03 10:51:23 -07:00
Lncvrt da27bcb32c Smaller filesize & Close on issue 2026-04-03 10:35:18 -07:00
Lncvrt 6c8dfd1200 Update tokio & remove redundant Windows config 2026-04-03 10:13:45 -07:00
Lncvrt e09d125980 Should fix issues on linux 2026-04-03 07:12:24 -07:00
Lncvrt 35dd842576 I was lied to 2026-04-02 21:10:37 -07:00
Lncvrt 55714c8edb Stupid git 2026-04-02 17:27:05 -07:00
Lncvrt 04d1f2822b Add auto updater to the launcher 2026-04-02 17:11:25 -07:00
Lncvrt dfd99ab12e Use a popup for changelog again 2026-04-01 19:09:24 -07:00
Lncvrt 837c4a8863 Heavily improve codebase 2026-04-01 18:33:06 -07:00
Lncvrt c69060a360 1.8.1 2026-03-27 12:47:27 -07:00
Lncvrt f2b0358605 Big smart 2026-03-27 12:45:58 -07:00
Lncvrt 57441a8872 Close popup & notify on download finish 2026-03-27 12:45:29 -07:00
Lncvrt df522122ac Remove debug logging and fix hashing 2026-03-27 12:40:25 -07:00
Lncvrt f4e16c12e5 Fix overscroll 2026-03-27 12:28:33 -07:00
Lncvrt e2d98307c0 Optimize builds 2026-03-27 12:27:05 -07:00
Lncvrt 387bd54ce2 Rename "unix wine" to "linux wine" 2026-03-24 22:50:09 -07:00
Lncvrt e7a61872b7 Switch to Tauris json store management for settings 2026-03-24 22:45:28 -07:00
Lncvrt ac66c78dbf Fix this not using Rust for sending requests 2026-03-24 21:31:52 -07:00
Lncvrt d9302319fa Fix sorting issue 2026-03-24 20:15:41 -07:00
Lncvrt 5206a7f25d Fix downloads 2026-03-24 19:55:00 -07:00
Lncvrt 3ee371998a Should fix linux/macos BIE support? 2026-03-24 19:01:44 -07:00
Lncvrt 5733c21fbb 1.8.0 2026-03-24 18:38:28 -07:00
Lncvrt 61b9d07fc6 Fix number formatting 2026-03-24 18:34:56 -07:00
Lncvrt 59cd625238 Finish mod manager & Count downloads for mods 2026-03-24 18:33:06 -07:00
Lncvrt 6bf2733cec Show that no mods are available to download/installed 2026-03-24 18:08:08 -07:00
Lncvrt 60f9f95be4 Finish mod installing and uninstalling 2026-03-24 18:01:24 -07:00
Lncvrt ea1c05d2d9 Add view mod info mode 2026-03-24 17:11:12 -07:00
Lncvrt 49c03e82b1 Pre-work on adding view mod info popup 2026-03-24 16:29:15 -07:00
Lncvrt 0c52169533 Add a refresh button 2026-03-24 16:12:44 -07:00
Lncvrt bf6da20fd8 Improvements here 2026-03-24 16:08:24 -07:00
Lncvrt c1f87580c7 Early WIP version of the mod manager 2026-03-24 15:57:33 -07:00
Lncvrt dd7d2b96e3 Fix potential issues here 2026-03-13 18:34:58 -07:00
Lncvrt 874f0627f7 Add downloading mod manager 2026-03-13 18:31:52 -07:00
Lncvrt e8dec1a1b2 Add types to downloads, will be for modding stuff 2026-03-13 18:11:58 -07:00
Lncvrt e1f0ce1290 wtf 2026-03-13 17:46:16 -07:00
Lncvrt bc5ea81170 Make remove button how it should be 2026-03-13 17:40:15 -07:00
Lncvrt 71b998f4bc Fix outdated page 2026-03-13 17:31:09 -07:00
Lncvrt cbac50753f Built in modding support wip 2026-03-13 17:10:11 -07:00
Lncvrt fb65348ba3 Fix this logic broken in 1.6.0 2026-03-12 23:20:29 -07:00
Lncvrt f23d1025d2 1.7.0 2026-03-10 21:43:24 -07:00
Lncvrt a8573c4b6a Improve that 2026-03-10 21:39:23 -07:00
Lncvrt 4054fdd06b Add download pausing, canceling and resuming 2026-03-10 21:22:24 -07:00
Lncvrt 0b4e639630 Add unzip status 2026-03-10 20:35:26 -07:00
Lncvrt c65fb5e1a5 Optimize crates & a import 2026-03-10 20:33:01 -07:00
Lncvrt 6486498c74 Readd progress bar status, but optimized 2026-03-10 19:56:55 -07:00
Lncvrt cf72d143f3 oop 2026-03-10 15:44:33 -07:00
Lncvrt ee7c50c6f5 Reset berry state on back 2026-03-10 15:44:01 -07:00
Lncvrt f7317c8232 Improve berry select popup 2026-03-10 15:22:49 -07:00
Lncvrt 9856cf2426 Remove progress bar status, causes so much issues 2026-03-10 15:17:06 -07:00
52 changed files with 3334 additions and 1552 deletions

No files matched your search

+125 -107
View File
@@ -8,31 +8,37 @@
"@fortawesome/fontawesome-svg-core": "7.2.0",
"@fortawesome/free-brands-svg-icons": "7.2.0",
"@fortawesome/free-solid-svg-icons": "7.2.0",
"@fortawesome/react-fontawesome": "3.2.0",
"@fortawesome/react-fontawesome": "3.3.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "2.6.0",
"@tauri-apps/plugin-fs": "2.4.5",
"@tauri-apps/plugin-http": "2.5.7",
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
"@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",
"next": "16.1.6",
"@tauri-apps/plugin-store": "2.4.2",
"@tauri-apps/plugin-updater": "2.10.1",
"@tauri-apps/plugin-window-state": "2.4.1",
"dompurify": "3.3.3",
"next": "16.2.3",
"pretty-bytes": "7.1.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react": "19.2.5",
"react-dom": "19.2.5",
},
"devDependencies": {
"@eslint/eslintrc": "3.3.5",
"@tailwindcss/postcss": "4.2.1",
"@tailwindcss/postcss": "4.2.2",
"@tauri-apps/cli": "2.10.1",
"@types/crypto-js": "4.2.2",
"@types/node": "25.3.5",
"@types/node": "25.6.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"crypto-js": "4.2.0",
"eslint": "9.39.4",
"eslint-config-next": "16.1.6",
"tailwindcss": "4.2.1",
"typescript": "5.9.3",
"eslint-config-next": "16.2.3",
"tailwindcss": "4.2.2",
"typescript": "6.0.2",
},
},
},
@@ -61,9 +67,9 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
@@ -71,11 +77,11 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
@@ -103,7 +109,7 @@
"@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w=="],
"@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.2.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw=="],
"@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.3.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-EHmHeTf8WgO29sdY3iX/7ekE3gNUdlc2RW6mm/FzELlHFKfTrA9S4MlyquRR+RRCRCn8+jXfLFpLGB2l7wCWyw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -175,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=="],
@@ -207,35 +213,35 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
"@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
@@ -263,16 +269,26 @@
"@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-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
"@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-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="],
"@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-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="],
"@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=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="],
"@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=="],
"@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="],
@@ -283,31 +299,33 @@
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
"@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.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
"@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.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
"@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.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
"@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.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="],
"@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.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
"@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.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
"@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.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
"@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=="],
@@ -381,21 +399,21 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="],
"axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@@ -403,7 +421,7 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -443,21 +461,23 @@
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
"electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="],
"es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
@@ -473,9 +493,9 @@
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
"eslint-config-next": ["eslint-config-next@16.1.6", "", { "dependencies": { "@next/eslint-plugin-next": "16.1.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA=="],
"eslint-config-next": ["eslint-config-next@16.2.3", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
@@ -523,7 +543,7 @@
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="],
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
@@ -543,7 +563,7 @@
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -665,29 +685,29 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@@ -717,11 +737,11 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
"next": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="],
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -757,11 +777,11 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@@ -773,9 +793,9 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
@@ -783,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=="],
@@ -817,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=="],
@@ -851,15 +871,15 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
@@ -875,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.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="],
"typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
@@ -913,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=="],
@@ -927,7 +947,7 @@
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -941,19 +961,17 @@
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-react/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
}
+19 -13
View File
@@ -1,7 +1,7 @@
{
"name": "lncvrt-games-launcher",
"private": true,
"version": "1.6.5",
"version": "1.9.1",
"type": "module",
"scripts": {
"dev": "next dev",
@@ -14,30 +14,36 @@
"@fortawesome/fontawesome-svg-core": "7.2.0",
"@fortawesome/free-brands-svg-icons": "7.2.0",
"@fortawesome/free-solid-svg-icons": "7.2.0",
"@fortawesome/react-fontawesome": "3.2.0",
"@fortawesome/react-fontawesome": "3.3.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "2.6.0",
"@tauri-apps/plugin-fs": "2.4.5",
"@tauri-apps/plugin-http": "2.5.7",
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
"@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",
"next": "16.1.6",
"@tauri-apps/plugin-store": "2.4.2",
"@tauri-apps/plugin-updater": "2.10.1",
"@tauri-apps/plugin-window-state": "2.4.1",
"dompurify": "3.3.3",
"next": "16.2.3",
"pretty-bytes": "7.1.0",
"react": "19.2.4",
"react-dom": "19.2.4"
"react": "19.2.5",
"react-dom": "19.2.5"
},
"devDependencies": {
"@eslint/eslintrc": "3.3.5",
"@tailwindcss/postcss": "4.2.1",
"@tailwindcss/postcss": "4.2.2",
"@tauri-apps/cli": "2.10.1",
"@types/crypto-js": "4.2.2",
"@types/node": "25.3.5",
"@types/node": "25.6.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"crypto-js": "4.2.0",
"eslint": "9.39.4",
"eslint-config-next": "16.1.6",
"tailwindcss": "4.2.1",
"typescript": "5.9.3"
"eslint-config-next": "16.2.3",
"tailwindcss": "4.2.2",
"typescript": "6.0.2"
}
}
-8
View File
@@ -1,8 +0,0 @@
[build]
target = "x86_64-win7-windows-msvc"
[unstable]
build-std = ["std", "panic_abort"]
[toolchain]
channel = "nightly"
+34 -12
View File
@@ -1,6 +1,6 @@
[package]
name = "lncvrt-games-launcher"
version = "1.6.5"
version = "1.9.1"
authors = ["Lncvrt"]
edition = "2024"
@@ -12,24 +12,46 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.5.6", features = [] }
[dependencies]
tauri = { version = "2.10.3", features = [] }
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.2", default-features = false, features = ["stream", "rustls"] }
tokio = "1.50.0"
reqwest = { version = "0.13.2", default-features = false, features = [
"stream",
"rustls",
] }
tokio = "1.51.1"
futures-util = { version = "0.3.32", features = ["io"] }
tauri-plugin-fs = "2.4.5"
zip = "8.2.0"
libc = "0.2.183"
tauri-plugin-dialog = "2.6.0"
tauri-plugin-http = "2.5.7"
tauri-plugin-decorum = "1.1.1"
tauri-plugin-fs = "2.5.0"
zip = "8.5.1"
tauri-plugin-dialog = "2.7.0"
tauri-plugin-notification = "2.3.3"
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"
[target.'cfg(target_os = "linux")'.dependencies]
shlex = "1.3.0"
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
sysinfo = "0.38.4"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.4.0"
tauri-plugin-single-instance = "2.4.1"
tauri-plugin-updater = "2.10.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"
+24 -11
View File
@@ -2,28 +2,40 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"os:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-center",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-set-size",
"core:window:allow-set-focus",
"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",
"notification:default",
"clipboard-manager:default",
"clipboard-manager:allow-write-text",
"store:default",
"store:allow-load",
"store:allow-save",
"fs:allow-applocaldata-read",
"fs:allow-applocaldata-write",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APPLOCALDATA/game/**" }
]
"allow": [{ "path": "$APPLOCALDATA/game/**" }, { "path": "$APPCONFIG/versions.json" }]
},
{
"identifier": "opener:allow-open-path",
"allow": [
{ "path": "$APPLOCALDATA/game/**" }
]
"allow": [{ "path": "$APPLOCALDATA/game/**" }]
},
{
"identifier": "http:default",
@@ -36,8 +48,9 @@
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/berry" },
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/coin" },
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/legacy" },
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total" }
{ "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total" },
{ "url": "https://games.lncvrt.xyz/api/launcher/mods" }
]
}
]
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"window-state:default",
"updater:default"
]
}
+553 -38
View File
@@ -1,4 +1,13 @@
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;
use std::time::Instant;
use std::{
fs::{File, create_dir_all},
io::{BufReader, copy},
@@ -6,9 +15,47 @@ use std::{
process::Command,
time::Duration,
};
use tauri::window::{ProgressBarState, ProgressBarStatus};
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_os::platform;
use tauri_plugin_prevent_default::Flags;
use tokio::io::AsyncReadExt;
use tokio::{io::AsyncWriteExt, time::timeout};
use zip::ZipArchive;
use zip::result::ZipError;
#[cfg(any(target_os = "windows", target_os = "linux"))]
use sysinfo::System;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(not(debug_assertions))]
use tauri_plugin_updater::UpdaterExt;
static CANCEL_MAP: OnceLock<DashMap<String, bool>> = OnceLock::new();
fn cancel_map() -> &'static DashMap<String, bool> {
CANCEL_MAP.get_or_init(DashMap::new)
}
fn is_cancelled(name: &str) -> bool {
cancel_map().get(name).map(|v| *v).unwrap_or(false)
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn is_running_by_path(path: &Path) -> bool {
let sys = System::new_all();
let target = match path.canonicalize() {
Ok(t) => t,
Err(_) => return false,
};
sys.processes().values().any(|proc| {
proc.exe()
.and_then(|exe| exe.canonicalize().ok())
.map_or(false, |exe| exe == target)
})
}
fn should_skip(name: &str) -> bool {
name.starts_with("__MACOSX/")
@@ -17,23 +64,33 @@ fn should_skip(name: &str) -> bool {
|| name.ends_with(".DS_Store")
}
async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
async fn unzip_to_dir(app: AppHandle, zip_path: PathBuf, out_dir: PathBuf, name: String) -> String {
let _ = app.emit("unzip-start", format!("{}", name));
let res = tauri::async_runtime::spawn_blocking(move || {
let file = File::open(zip_path)?;
let mut archive = ZipArchive::new(BufReader::new(file))?;
let total = archive.len();
for i in 0..archive.len() {
for i in 0..total {
let mut entry = archive.by_index(i)?;
let name = entry.name();
let entry_name = entry.name();
if should_skip(name) {
if should_skip(entry_name) {
continue;
}
let outpath = out_dir.join(name);
let outpath = out_dir.join(entry_name);
if !outpath.starts_with(&out_dir) {
return Err(ZipError::InvalidArchive("Path traversal detected".into()));
}
if entry.is_dir() {
create_dir_all(&outpath)?;
#[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)?;
@@ -41,7 +98,14 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
let mut outfile = File::create(&outpath)?;
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>(())
@@ -55,79 +119,289 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
}
#[tauri::command]
async fn download(app: AppHandle, url: String, name: String) -> String {
let client = reqwest::Client::new();
let resp = match client.get(&url).send().await {
Ok(r) => r,
fn folder_size(app: AppHandle, version: String) -> String {
let path = match app.path().app_local_data_dir() {
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) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
size += metadata.len();
} else if metadata.is_dir() {
size += inner(&entry.path());
}
}
}
}
size
}
let total_size = resp.content_length().unwrap_or(0);
let p = Path::new(&path);
if p.exists() && p.is_dir() {
inner(p).to_string()
} else {
"-1".to_string()
}
}
let mut downloaded: u64 = 0;
let mut stream = resp.bytes_stream();
#[tauri::command]
async fn download(
app: AppHandle,
url: String,
name: String,
hash: String,
download_type: i8,
mod_id: String,
) -> String {
let _ = app.emit("download-start", format!("{}", name));
let window = app.get_webview_window("main").expect("main window missing");
let downloads_path = match app.path().app_local_data_dir() {
Ok(p) => p.join("downloads"),
Err(_) => return "-1".to_string(),
Err(_) => {
let _ = &app.emit("download-stop", format!("{}", name));
return "-1".to_string();
}
};
let game_path = match app.path().app_local_data_dir() {
Ok(p) => p.join("game"),
Err(_) => return "-1".to_string(),
Err(_) => {
let _ = &app.emit("download-stop", format!("{}", name));
return "-1".to_string();
}
};
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 _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
let mut file = match tokio::fs::File::create(download_part_path.clone()).await {
Ok(f) => f,
Err(_) => return "-1".to_string(),
let resume_from: u64 = match tokio::fs::metadata(&download_part_path).await {
Ok(meta) => {
let size = meta.len();
size
}
Err(_) => 0,
};
let client = reqwest::Client::new();
let mut request = client.get(&url);
if resume_from > 0 {
request = request.header("Range", format!("bytes={}-", resume_from));
}
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(_) => return "-1".to_string(),
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();
}
downloaded += chunk.len() as u64;
let progress = if total_size > 0 {
((downloaded as f64) / (total_size as f64)) * 100.0
} else {
0.0
};
let session_bytes = downloaded - actual_resume_from;
let elapsed_secs = start.elapsed().as_secs_f64();
let speed = if elapsed_secs > 0.0 {
session_bytes as f64 / elapsed_secs
} else {
0.0
};
let eta_secs = if total_size > downloaded && speed > 0.0 {
(total_size - downloaded) as f64 / speed
} else {
0.0
};
if chunk_count % 50 == 0 || downloaded == total_size {
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
),
);
}
if total_size > 0 && downloaded < total_size {
let _ = window.set_progress_bar(ProgressBarState {
status: Some(ProgressBarStatus::None),
progress: Some(0),
});
return "-1".to_string();
}
let _ = app.emit("download-finishing", format!("{}", &name));
let _ = app.emit("download-hash-checking", format!("{}", &name));
if let Err(_) = tokio::fs::rename(download_part_path.clone(), download_zip_path.clone()).await {
let download_hash = {
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 = [0u8; 8192];
loop {
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);
hex::encode(hasher.finalize())
};
if hash != download_hash {
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();
}
let unzip_res = unzip_to_dir(download_zip_path.clone(), game_path.join(&name)).await;
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 _ = tokio::fs::remove_file(download_zip_path.clone()).await;
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("BepInEx")
.join("plugins")
.join(&mod_id)
} else {
game_path.join(&name)
},
name.clone(),
)
.await;
let _ = tokio::fs::remove_file(&download_zip_path).await;
if unzip_res == "-1" {
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)]
@@ -148,19 +422,165 @@ fn launch_game(
return;
}
if let Err(_) = Command::new(game_folder.join(&executable))
.current_dir(&game_folder)
.spawn()
let exe_path = game_folder.join(&executable);
//if already running on macos, it'll auto take the user to that proccess
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
eprintln!("Failed to launch game");
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
if !use_wine && is_running_by_path(&exe_path) {
app.dialog()
.message(format!(
"{} is already running, if this doesn't seem true, try to kill the proccess.",
display_name
))
.kind(MessageDialogKind::Error)
.title("Game already running")
.show(|_| {});
return;
}
}
#[cfg(target_os = "linux")]
{
if use_wine {
let quoted_path = format!("\"{}\"", exe_path.to_string_lossy());
let cmd = wine_command.replace("%path%", &quoted_path);
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()
{
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;
}
}
let bepinex_folder = match app.path().app_local_data_dir() {
Ok(p) => p.join("game").join(&name).join("BepInEx"),
Err(_) => return,
};
if platform() == "macos" && !bepinex_folder.exists() && executable.ends_with(".app") {
if let Err(_) = Command::new("open")
.arg(&executable)
.current_dir(&game_folder)
.spawn()
{
eprintln!("Failed to launch game on macOS");
}
} else {
if (platform() == "macos" || platform() == "linux") && bepinex_folder.exists() {
if let Err(_) = Command::new("./run_bepinex.sh")
.arg(&executable)
.current_dir(&game_folder)
.spawn()
{
eprintln!("Failed to launch game");
}
} else {
if let Err(_) = Command::new(&exe_path).current_dir(&game_folder).spawn() {
eprintln!("Failed to launch game");
}
}
}
}
#[tauri::command]
fn verify_signature(body: String, signature: String, public_key: String) -> bool {
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);
}
#[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_updater::Builder::new().build())
.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| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
@@ -168,9 +588,104 @@ pub fn run() {
}))
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![download, launch_game])
.invoke_handler(tauri::generate_handler![
download,
launch_game,
folder_size,
verify_signature,
cancel_download,
open_new_window
])
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "windows")]
{
use tauri_plugin_decorum::WebviewWindowExt;
if let Err(e) = window.create_overlay_titlebar() {
eprintln!("Failed to create overlay titlebar: {:?}", e);
}
}
#[cfg(not(debug_assertions))]
{
let handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
update(handle, window.clone()).await.unwrap();
});
}
#[cfg(debug_assertions)]
{
let mut new_url = app.config().build.dev_url.clone().unwrap();
new_url.set_path("/main");
window.navigate(new_url).unwrap();
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(not(debug_assertions))]
async fn update(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
) -> tauri_plugin_updater::Result<()> {
let target = {
#[cfg(target_os = "linux")]
{
if std::path::Path::new("/etc/debian_version").exists() {
"linux-debian"
} else if std::path::Path::new("/etc/redhat-release").exists() {
"linux-fedora"
} else {
let mut new_url = window.url().unwrap();
new_url.set_path("/update/outdated");
window.navigate(new_url).unwrap();
return Ok(());
}
}
#[cfg(not(target_os = "linux"))]
{
tauri_plugin_updater::target().unwrap_or("unknown".to_string())
}
};
if let Some(update) = app
.updater_builder()
.target(target)
.build()?
.check()
.await?
{
let mut new_url = window.url().unwrap();
new_url.set_path("/update/updating");
window.navigate(new_url).unwrap();
let mut downloaded = 0;
update
.download_and_install(
|chunk_length, content_length| {
downloaded += chunk_length;
println!("downloaded {downloaded} from {content_length:?}");
},
|| {
println!("download finished");
},
)
.await?;
println!("update installed");
app.restart();
}
let mut new_url = window.url().unwrap();
new_url.set_path("/main");
window.navigate(new_url).unwrap();
Ok(())
}
+5 -21
View File
@@ -1,37 +1,21 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.6.5",
"version": "1.9.1",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "bun next dev --webpack",
"beforeDevCommand": "next dev --webpack",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "bun next build",
"beforeBuildCommand": "next build",
"frontendDist": "../out"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Lncvrt Games Launcher",
"width": 1000,
"height": 600,
"minWidth": 875,
"minHeight": 500
}
],
"windows": [],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"icon": ["icons/icon.ico"],
"targets": "nsis",
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
}
"active": false
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.9.1",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev --webpack",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "next build",
"frontendDist": "../out"
},
"app": {
"windows": [
{
"title": "Lncvrt Games Launcher",
"url": "/update/checking",
"width": 1000,
"height": 600,
"minWidth": 1000,
"minHeight": 500
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"category": "Game",
"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}}&currentVersion={{current_version}}"
]
}
}
}
+48
View File
@@ -0,0 +1,48 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.9.1",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev --webpack",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "next build",
"frontendDist": "../out"
},
"app": {
"macOSPrivateApi": true,
"windows": [
{
"title": "Lncvrt Games Launcher",
"url": "/update/checking",
"width": 1000,
"height": 600,
"minWidth": 1000,
"minHeight": 500,
"titleBarStyle": "Overlay",
"hiddenTitle": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"category": "Game",
"targets": ["dmg", "app"],
"icon": ["icons/icon.icns"],
"createUpdaterArtifacts": true,
"macOS": {
"minimumSystemVersion": "13.0"
}
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
"endpoints": [
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}&arch={{arch}}&currentVersion={{current_version}}"
]
}
}
}
+45
View File
@@ -0,0 +1,45 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.9.1",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev --webpack",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "next build",
"frontendDist": "../out"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Lncvrt Games Launcher",
"url": "/update/checking",
"width": 1000,
"height": 632,
"minWidth": 1000,
"minHeight": 532,
"decorations": false,
"shadow": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"category": "Game",
"targets": ["msi"],
"icon": ["icons/icon.ico"],
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
"endpoints": [
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}&arch={{arch}}&currentVersion={{current_version}}"
]
}
}
}
-510
View File
@@ -1,510 +0,0 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import Sidebar from '@/componets/Sidebar'
import './Globals.css'
import { DownloadProgress } from '@/types/DownloadProgress'
import { invoke } from '@tauri-apps/api/core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronLeft, faXmark } from '@fortawesome/free-solid-svg-icons'
import {
readNormalConfig,
readVersionsConfig,
writeVersionsConfig
} from '@/lib/BazookaManager'
import { VersionsConfig } from '@/types/VersionsConfig'
import { NormalConfig } from '@/types/NormalConfig'
import { app } from '@tauri-apps/api'
import { openUrl } from '@tauri-apps/plugin-opener'
import { GlobalProvider } from './GlobalProvider'
import { Roboto } from 'next/font/google'
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
import { GameVersion } from '@/types/GameVersion'
import { Game } from '@/types/Game'
import { listen } from '@tauri-apps/api/event'
import { usePathname } from 'next/navigation'
import { arch, platform } from '@tauri-apps/plugin-os'
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
import VersionsDownloadPopup from '@/componets/popups/VersionsDownload'
import GamesDownloadPopup from '@/componets/popups/GamesDownload'
import DownloadsPopup from '@/componets/popups/Downloads'
import VersionVersionPopup from '@/componets/popups/VersionVersion'
import { fetch } from '@tauri-apps/plugin-http'
const roboto = Roboto({
subsets: ['latin']
})
export default function RootLayout ({
children
}: {
children: React.ReactNode
}) {
const [loading, setLoading] = useState(true)
const [loadingText, setLoadingText] = useState('Loading...')
const [outdated, setOutdated] = useState(false)
const [version, setVersion] = useState<string | null>(null)
const [platformName, setPlatformName] = useState<string | null>(null)
const [serverVersionList, setServerVersionList] =
useState<null | ServerVersionsResponse>(null)
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
const [downloadedVersionsConfig, setDownloadedVersionsConfig] =
useState<VersionsConfig | null>(null)
const [normalConfig, setNormalConfig] = useState<NormalConfig | null>(null)
const [showPopup, setShowPopup] = useState(false)
const [popupMode, setPopupMode] = useState<null | number>(null)
const [fadeOut, setFadeOut] = useState(false)
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
[]
)
const [downloadQueue, setDownloadQueue] = useState<string[]>([])
const [isProcessingQueue, setIsProcessingQueue] = useState<boolean>(false)
const [managingVersion, setManagingVersion] = useState<string | null>(null)
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
useState<boolean>(false)
const [selectedGame, setSelectedGame] = useState<number | null>(null)
const [category, setCategory] = useState<number>(-1)
const pathname = usePathname()
const revisionCheck = useRef(false)
function getSpecialVersionsList (game?: number): GameVersion[] {
if (!normalConfig || !serverVersionList) return []
return serverVersionList.versions
.filter(
v => !Object.keys(downloadedVersionsConfig?.list ?? []).includes(v.id)
)
.filter(v => {
if (
platformName == 'linux' &&
v.wine &&
!normalConfig.settings.useWineOnUnixWhenNeeded
)
return false
if (game && v.game != game) return false
if (category != -1 && v.category != category) return false
if (downloadProgress.length != 0) {
return !downloadProgress.some(d => d.version === v.id)
}
return true
})
.sort((a, b) => {
if (b.game !== a.game) return a.game - b.game
return 0
})
}
const getVersionInfo = useCallback(
(id: string | undefined): GameVersion | undefined => {
if (!id) return undefined
return serverVersionList?.versions.find(v => v.id === id)
},
[serverVersionList]
)
const getGameInfo = useCallback(
(game: number | undefined): Game | undefined => {
if (!game) return undefined
return serverVersionList?.games.find(g => g.id === game)
},
[serverVersionList]
)
function getListOfGames (): Game[] {
if (!downloadedVersionsConfig?.list) return []
const gamesMap = new Map<number, Game>()
Object.keys(downloadedVersionsConfig.list).forEach(i => {
const version = getVersionInfo(i)
if (!version) return
const game = getGameInfo(version.game)
if (!game) return
gamesMap.set(game.id, game)
})
return Array.from(gamesMap.values())
}
function getVersionsAmountData (gameId: number): {
installed: number
total: number
} | null {
if (!downloadedVersionsConfig || !serverVersionList) return null
const allowWine =
platformName !== 'linux' || normalConfig?.settings.useWineOnUnixWhenNeeded
const installed = Object.keys(downloadedVersionsConfig.list).filter(v => {
const info = getVersionInfo(v)
if (!info) return false
if (info.wine && !allowWine) return false
return getGameInfo(info.game)?.id === gameId
}).length
const total = serverVersionList.versions.filter(v => {
if (!v) return false
if (v.wine && !allowWine) return false
return getGameInfo(v.game)?.id === gameId
}).length
return { installed, total }
}
const closePopup = useCallback(() => {
if (popupMode == 0 && selectedGame && pathname === '/') {
setSelectedGame(null)
setSelectedVersionList([])
} else if (viewingInfoFromDownloads) {
setViewingInfoFromDownloads(false)
setPopupMode(0)
} else {
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
}
}, [popupMode, selectedGame, pathname, viewingInfoFromDownloads])
useEffect(() => {
let unlistenProgress: (() => void) | null = null
listen<string>('download-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], finishing: true }
return copy
})
}).then(f => (unlistenProgress = f))
return () => {
unlistenProgress?.()
}
}, [downloadProgress])
useEffect(() => {
;(async () => {
setPlatformName(platform())
const client = await app.getVersion()
setVersion(client)
if (process.env.NODE_ENV === 'production') {
try {
const response = await fetch(
'https://games.lncvrt.xyz/api/launcher/latest'
)
const data = await response.text()
if (data !== client) {
setOutdated(true)
return
}
} catch {
setLoadingText('Failed to check latest version.')
return
}
}
try {
const response = await fetch(
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
)
const data = await response.json()
setServerVersionList(data)
} catch {
setLoadingText('Failed to download versions list.')
return
}
const normalConfig = await readNormalConfig()
const versionsConfig = await readVersionsConfig()
setDownloadedVersionsConfig(versionsConfig)
setNormalConfig(normalConfig)
setLoading(false)
})()
}, [])
const downloadVersions = useCallback(
async (list: string[]): Promise<void> => {
if (list.length === 0) return
setSelectedVersionList([])
const newVersions = list.filter(
version =>
!downloadQueue.includes(version) &&
!downloadProgress.some(d => d.version === version)
)
if (newVersions.length === 0) return
const newDownloads = newVersions.map(
version => new DownloadProgress(version, false, true, false)
)
setDownloadProgress(prev => [...prev, ...newDownloads])
setDownloadQueue(prev => [...prev, ...newVersions])
},
[downloadQueue, downloadProgress]
)
useEffect(() => {
if (isProcessingQueue || downloadQueue.length === 0) return
const processNextDownload = async () => {
setIsProcessingQueue(true)
const versionId = downloadQueue[0]
const info = getVersionInfo(versionId)
if (!info) {
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
setDownloadQueue(prev => prev.slice(1))
setIsProcessingQueue(false)
return
}
const gameInfo = getGameInfo(info.game)
if (!gameInfo) {
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
setDownloadQueue(prev => prev.slice(1))
setIsProcessingQueue(false)
return
}
setDownloadProgress(prev =>
prev.map(d => (d.version === versionId ? { ...d, queued: false } : d))
)
const downloadInfoRequest = await fetch(
'https://games.lncvrt.xyz/api/launcher/download?gameId=' +
info.id +
'&downloadId=' +
info.download
)
const data = await downloadInfoRequest.json()
if (!data.success) {
setDownloadProgress(prev =>
prev.map(d =>
d.version === versionId
? { ...d, queued: false, failed: true, progress: 0 }
: d
)
)
return
}
const res = await invoke<string>('download', {
url: data.data.url,
name: info.id
})
if (res === '1') {
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
setDownloadedVersionsConfig(prev => {
if (!prev) return prev
const updated = {
...prev,
list: {
...prev.list,
[versionId]: Date.now()
}
}
writeVersionsConfig(updated)
return updated
})
} else {
setDownloadProgress(prev =>
prev.map(d =>
d.version === versionId
? { ...d, queued: false, failed: true, progress: 0 }
: d
)
)
}
setDownloadQueue(prev => prev.slice(1))
setIsProcessingQueue(false)
}
processNextDownload()
}, [
downloadQueue,
isProcessingQueue,
getVersionInfo,
getGameInfo,
normalConfig
])
useEffect(() => {
if (revisionCheck.current) return
if (!serverVersionList || !downloadedVersionsConfig) return
revisionCheck.current = true
;(async () => {
for (const [key, value] of Object.entries(
downloadedVersionsConfig.list
)) {
const verInfo = serverVersionList.versions.find(item => item.id === key)
if (
!verInfo ||
(verInfo.lastRevision > 0 && value / 1000 <= verInfo.lastRevision)
) {
if (
await exists('game/' + key + '/' + verInfo?.executable, {
baseDir: BaseDirectory.AppLocalData
})
)
await remove('game/' + key + '/' + verInfo?.executable, {
baseDir: BaseDirectory.AppLocalData,
recursive: true
})
}
}
})()
}, [serverVersionList, downloadedVersionsConfig, downloadVersions])
return (
<>
<html lang='en' className={roboto.className}>
<body
className={
normalConfig?.settings.theme === 1
? 'red-theme'
: normalConfig?.settings.theme === 2
? 'blue-theme'
: normalConfig?.settings.theme === 3
? 'purple-theme'
: 'dark-theme'
}
>
{loading ? (
<>
<div className='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,
downloadVersions,
category,
setCategory,
downloadQueue,
setDownloadQueue,
closePopup,
getSpecialVersionsList,
selectedGame,
setViewingInfoFromDownloads
}}
>
<div
tabIndex={0}
onKeyDown={e => {
if (showPopup && e.key === 'Escape') closePopup()
}}
>
<Sidebar />
<div className='relative z-0'>
<main className='ml-60'>{children}</main>
</div>
{showPopup && (
<div
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
if (viewingInfoFromDownloads) {
setPopupMode(0)
setViewingInfoFromDownloads(false)
setManagingVersion(null)
setSelectedGame(null)
}
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
}
}}
>
<div className='popup-box'>
<button
className='close-button btntheme1'
onClick={() => closePopup()}
>
<FontAwesomeIcon
icon={
(popupMode == 0 &&
selectedGame &&
pathname === '/') ||
viewingInfoFromDownloads ||
popupMode == 4
? faChevronLeft
: faXmark
}
/>
</button>
{popupMode === 0 && selectedGame ? (
<VersionsDownloadPopup />
) : popupMode === 0 && !selectedGame ? (
<GamesDownloadPopup />
) : popupMode === 1 ? (
<DownloadsPopup />
) : popupMode === 2 ? (
<VersionVersionPopup />
) : null}
</div>
</div>
)}
</div>
</GlobalProvider>
)}
</body>
</html>
</>
)
}
File renamed without changes.
@@ -1,12 +1,15 @@
'use client'
import './styles.css'
import { useCallback, useEffect, useState } from 'react'
import { BirdColor } from '@/types/BerryDash/BirdColor'
import { GetIconForUser } from '@/lib/BerryDash'
import { GetIconForUser } from '@/lib/berrydashutil'
import Image from 'next/image'
import './styles.css'
import { useRouter } from 'next/navigation'
import { platform } from '@tauri-apps/plugin-os'
import { fetch } from '@tauri-apps/plugin-http'
import { verifySignature } from '@/lib/util'
import Dropdown from '@/components/Dropdown'
interface BaseEntry {
id: number
@@ -83,17 +86,23 @@ 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 (data.success) {
if (
(await verifySignature(JSON.stringify(data), signature)) &&
data.success
) {
let accounts = data.data as Account[]
accounts = accounts.map(acc => {
@@ -128,12 +137,18 @@ export default function BerryDashLeaderboards () {
? 'legacy'
: 'total')
)
const signature = response.headers.get('x-signature') ?? ''
const data = await response.json()
if (data.success) setEntries(data.data as LeaderboardEntry[])
if (
(await verifySignature(JSON.stringify(data), signature)) &&
data.success
)
setEntries(data.data as LeaderboardEntry[])
}
} catch {
setEntries([])
}
setLoading(false)
}, [selected, selectedBerryOption])
useEffect(() => {
@@ -149,6 +164,25 @@ export default function BerryDashLeaderboards () {
<div className='flex justify-between items-center mb-4'>
<p className='text-3xl'>Berry Dash Leaderboards</p>
<div className='flex gap-2'>
{selected == 1 && (
<div className='flex justify-center'>
<Dropdown
value={selectedBerryOption}
options={[
{ label: 'Normal Berry', value: 0 },
{ label: 'Poison Berry', value: 1 },
{ label: 'Slow Berry', value: 2 },
{ label: 'Ultra Berry', value: 3 },
{ label: 'Speedy Berry', value: 4 },
{ label: 'Coin Berry', value: 5 },
{ label: 'Random Berry', value: 6 },
{ label: 'Anti Berry', value: 7 },
{ label: 'Golden Berry', value: 8 }
]}
onChange={e => setSelectedBerryOption(Number(e))}
/>
</div>
)}
<button
className='button btntheme1'
onClick={() => {
@@ -163,9 +197,10 @@ export default function BerryDashLeaderboards () {
<button
className='button btntheme1'
onClick={() => {
if (selected == -1) router.push('/game?id=1')
if (selected == -1) router.push('/main/game?id=1')
else {
setSelected(-1)
setSelectedBerryOption(0)
setEntries([])
}
}}
@@ -175,7 +210,13 @@ export default function BerryDashLeaderboards () {
</button>
</div>
</div>
<div className='box h-[calc(100vh-84px)]'>
<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>
@@ -224,11 +265,17 @@ export default function BerryDashLeaderboards () {
</button>
</div>
</>
) : loading ? (
<p className='flex justify-center items-center text-6xl h-full'>
Loading
</p>
) : (
<>
<div
className={`flex flex-col gap-2 overflow-y-auto ${
selected == 1 ? 'h-[calc(100vh-136px)]' : 'h-[calc(100vh-96px)]'
platform() == 'windows'
? 'h-[calc(100vh-128px)]'
: 'h-[calc(100vh-96px)]'
} px-1`}
>
{entries.map((item, index) => {
@@ -274,39 +321,20 @@ export default function BerryDashLeaderboards () {
: selected === 2
? 'Coins'
: 'Score'}
: {item.value.toLocaleString('en-US')}
: {item.value.toLocaleString()}
</p>
) : isAccount ? (
<p>
{selected === 3 ? 'Level' : 'XP'}:{' '}
{selected === 3
? calculateLevel(item.xp).toLocaleString('en-US')
: item.xp.toLocaleString('en-US')}
? calculateLevel(item.xp).toLocaleString()
: item.xp.toLocaleString()}
</p>
) : null}
</div>
)
})}
</div>
{selected == 1 && (
<div className='flex justify-center'>
<select
value={selectedBerryOption}
onChange={e => setSelectedBerryOption(Number(e.target.value))}
className='leaderboard-select mt-0.75'
>
<option value='0'>Normal Berry</option>
<option value='1'>Poison Berry</option>
<option value='2'>Slow Berry</option>
<option value='3'>Ultra Berry</option>
<option value='4'>Speedy Berry</option>
<option value='5'>Coin Berry</option>
<option value='6'>Random Berry</option>
<option value='7'>Anti Berry</option>
<option value='8'>Golden Berry</option>
</select>
</div>
)}
</>
)}
</div>
@@ -4,8 +4,7 @@
@apply bg-(--col1) border border-(--col3) rounded-lg w-auto p-1;
}
.leaderboard-button,
.leaderboard-select {
.leaderboard-button {
@apply bg-(--col2) hover:bg-(--col4) border border-(--col4) hover:border-(--col6) rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
}
@@ -1,36 +1,35 @@
'use client'
import '../Installs.css'
import { useEffect } from 'react'
import '@/app/Installs.css'
import { invoke } from '@tauri-apps/api/core'
import { useGlobal } from '@/app/GlobalProvider'
import { useGlobal } from '@/providers/GlobalProvider'
import { useRouter, useSearchParams } from 'next/navigation'
import { platform } from '@tauri-apps/plugin-os'
import { faWarning } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { ask } from '@tauri-apps/plugin-dialog'
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
import { writeVersionsConfig } from '@/lib/BazookaManager'
import { openFolder } from '@/lib/Util'
import { openFolder } from '@/lib/util'
export default function Installs () {
const {
versionsList,
serverVersionList,
showPopup,
setShowPopup,
setPopupMode,
setFadeOut,
setSelectedVersionList,
downloadedVersionsConfig,
normalConfig,
setManagingVersion,
getVersionInfo,
getGameInfo,
setSelectedGame,
serverVersionList,
category,
setCategory,
setDownloadedVersionsConfig,
downloadVersions
downloadVersions,
downloadProgress,
linuxUseWine,
linuxWineCommand,
versions
} = useGlobal()
const params = useSearchParams()
@@ -42,7 +41,7 @@ export default function Installs () {
useEffect(() => {
if (!showPopup) return
setSelectedVersionList([])
}, [normalConfig, setSelectedVersionList, showPopup])
}, [setSelectedVersionList, showPopup])
if (!id || !game) return <p>Invalid game</p>
@@ -53,14 +52,21 @@ export default function Installs () {
if (!lastRevision) return false
return (
lastRevision > 0 &&
(downloadedVersionsConfig == undefined
? 0
: downloadedVersionsConfig?.list[version]) /
1000 <=
(versionsList == undefined ? 0 : versionsList[version]) / 1000 <=
lastRevision
)
}
const filteredVersions = Object.keys(versionsList).filter(v => {
const info = serverVersionList?.versions.find(vf => vf.id == v)
if (!info) return false
if (platform() == 'linux' && info.wine && !linuxUseWine) return false
return (
info.game === id &&
(category == -1 ? info.category == -1 : info.category == category)
)
})
return (
<div className='mx-4 mt-4'>
<div className='flex justify-between items-center mb-4'>
@@ -68,7 +74,7 @@ export default function Installs () {
className={`text-3xl truncate ${
category != -1
? 'w-[calc(100vw-495px)]'
: game.id == 1
: game.id == 1 || game.id == 9
? 'w-[calc(100vw-560px)]'
: 'w-[calc(100vw-440px)]'
}`}
@@ -79,13 +85,29 @@ export default function Installs () {
<button
className='button btntheme1'
onClick={() => {
router.push('/game/berrydash/leaderboards')
router.push('/main/game/berrydash/leaderboards')
}}
title='View the leaderboards for this game.'
hidden={game.id != 1}
>
Leaderboards
</button>
<button
className='button btntheme1'
onClick={() =>
invoke('open_new_window', {
title: 'XPS Dashboard',
name: 'xpsdashboard',
url: 'https://xps.lncvrt.xyz/dashboard/',
width: 1280,
height: 720
})
}
title='Open the GDPS Dashboard!'
hidden={game.id != 9 || category != -1}
>
Dashboard
</button>
<button
className='button btntheme1'
onClick={() => {
@@ -111,22 +133,23 @@ export default function Installs () {
</div>
</div>
<div className='downloads-container'>
<div className='downloads-scroll h-[calc(100vh-84px)]'>
<div
className={`downloads-scroll ${
platform() == 'windows'
? 'h-[calc(100vh-116px)]'
: 'h-[calc(100vh-84px)]'
}`}
>
{category == -1 &&
Object.entries(game.categoryNames)
.sort(([a], [b]) => Number(b) - Number(a))
.filter(([key]) => {
const count = Object.keys(
downloadedVersionsConfig?.list ?? {}
).filter(v => {
const info = getVersionInfo(v)
const count = Object.keys(versionsList).filter(v => {
const info = serverVersionList?.versions.find(
vf => vf.id == v
)
if (!info) return false
if (
platform() == 'linux' &&
info.wine &&
!normalConfig?.settings.useWineOnUnixWhenNeeded
)
if (platform() == 'linux' && info.wine && !linuxUseWine)
return false
return info.game === id && info.category === Number(key)
@@ -134,6 +157,7 @@ export default function Installs () {
return count >= 1
})
.sort(([a], [b]) => Number(b) - Number(a))
.map(([key, value]) => {
return (
<div
@@ -153,16 +177,15 @@ export default function Installs () {
<p>
{(() => {
const count =
Object.keys(
downloadedVersionsConfig?.list ?? []
).filter(v => {
const info = getVersionInfo(v)
Object.keys(versionsList).filter(v => {
const info = serverVersionList?.versions.find(
vf => vf.id == v
)
if (!info) return false
if (
platform() == 'linux' &&
info.wine &&
!normalConfig?.settings
.useWineOnUnixWhenNeeded
!linuxUseWine
)
return false
return (
@@ -178,48 +201,43 @@ export default function Installs () {
</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)
{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 &&
!normalConfig?.settings.useWineOnUnixWhenNeeded
)
if (platform() == 'linux' && info.wine && !linuxUseWine)
return false
return (
info.game === id &&
(category == -1
? info.category == -1
: info.category == category)
)
})
.map(entry => (
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={entry}
key={v}
className={'downloads-entry'}
title={
'Click to launch game. Right-click to manage this version install'
}
onClick={async () => {
if (
needsRevisionUpdate(
getVersionInfo(entry)?.lastRevision,
entry
)
) {
if (needsRevisionUpdate(versionInfo.lastRevision, v)) {
const answer = await ask(
'Before proceeding, if you do not want your installation directory wiped just yet, please backup the files to another directory. When you click "Yes", it will be wiped. Click "No" if you want to open the installation folder instead.',
{
@@ -243,78 +261,70 @@ export default function Installs () {
setFadeOut(false)
//uninstall
setDownloadedVersionsConfig(prev => {
if (!prev) return prev
const updatedList = Object.fromEntries(
Object.entries(prev.list).filter(
([k]) => k !== entry
versions?.set(
'list',
Object.fromEntries(
Object.entries(versionsList).filter(
([k]) => k !== v
)
)
const updatedConfig = {
...prev,
list: updatedList
}
writeVersionsConfig(updatedConfig)
return updatedConfig
})
)
if (
await exists('game/' + entry, {
await exists('game/' + v, {
baseDir: BaseDirectory.AppLocalData
})
)
await remove('game/' + entry, {
await remove('game/' + v, {
baseDir: BaseDirectory.AppLocalData,
recursive: true
})
//reinstall
setSelectedVersionList([entry])
downloadVersions([entry])
setSelectedVersionList([v])
downloadVersions([
{
id: v,
type: 0
}
])
} else {
openFolder(entry)
openFolder(v)
}
return
}
const verInfo = getVersionInfo(entry)
if (verInfo == undefined) return
const gameInfo = getGameInfo(verInfo.game)
if (gameInfo == undefined) return
invoke('launch_game', {
name: verInfo.id,
executable: verInfo.executable,
displayName: verInfo.displayName,
name: versionInfo.id,
executable: versionInfo.executable,
displayName: versionInfo.displayName,
useWine: !!(
platform() == 'linux' &&
verInfo.wine &&
normalConfig?.settings.useWineOnUnixWhenNeeded
versionInfo.wine &&
linuxUseWine
),
wineCommand: normalConfig?.settings.wineOnUnixCommand
wineCommand: linuxWineCommand
})
}}
onContextMenu={e => {
e.preventDefault()
setManagingVersion(entry)
setManagingVersion(v)
setPopupMode(2)
setShowPopup(true)
setFadeOut(false)
}}
>
<div className='h-18 w-screen relative'>
<p className='text-2xl'>
{getVersionInfo(entry)?.displayName}{' '}
</p>
<p className='text-2xl'>{versionInfo.displayName}</p>
<div className='flex gap-2 absolute left-0 bottom-0'>
<div
className='entry-info-item'
title='The date the game was installed.'
onClick={e => e.stopPropagation()}
>
<p>
Installed{' '}
{new Intl.DateTimeFormat(undefined).format(
downloadedVersionsConfig?.list[entry]
versionsList[v]
)}
</p>
</div>
@@ -322,41 +332,65 @@ export default function Installs () {
className='entry-info-item'
title='This version is using wine. It cannot be guarenteed to work fully and might not work at all.'
hidden={
!(
platform() == 'linux' && getVersionInfo(entry)?.wine
) ||
needsRevisionUpdate(
getVersionInfo(entry)?.lastRevision,
entry
)
!(platform() == 'linux' && versionInfo.wine) ||
needsRevisionUpdate(versionInfo.lastRevision, v)
}
onClick={e => e.stopPropagation()}
>
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
<p>Uses wine</p>
</div>
<div
className='entry-info-item'
onClick={e => e.stopPropagation()}
hidden={
!needsRevisionUpdate(
getVersionInfo(entry)?.lastRevision,
entry
)
!needsRevisionUpdate(versionInfo.lastRevision, v)
}
>
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
<p>Needs revision update!</p>
</div>
</div>
<button
className='absolute right-0 bottom-0 button'
title='Click to manage mods for this game!'
onClick={async e => {
e.stopPropagation()
if (
!(await exists('game/' + v + '/BepInEx', {
baseDir: BaseDirectory.AppLocalData
}))
) {
if (
(await ask(
"You don't have BepInEx (the mod loader for Unity Games), would you like to install it now? It is about 1MB in size. If you choose yes, it will download the recommended BepInEx version for " +
versionInfo.displayName +
'.',
{ title: 'BepInEx not found!', kind: 'error' }
)) &&
!downloadProgress.find(d => d.version == v)
) {
downloadVersions([
{
id: v,
type: 1
}
])
}
return
}
setManagingVersion(v)
setPopupMode(3)
setShowPopup(true)
setFadeOut(false)
}}
hidden={!versionInfo.modSupportDownload}
>
Mod Manager
</button>
</div>
</div>
))
) : (
<div className='flex justify-center items-center h-full'>
<p className='text-3xl'>No versions installed</p>
</div>
)}
)
})}
</div>
</div>
</div>
+835
View File
@@ -0,0 +1,835 @@
'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 [theme, setTheme] = useState<string>('dark')
const [versionsList, setVersionsList] = useState<Record<string, number>>({})
const [modsList, setModsList] = useState<
Record<string, Record<string, number>>
>({})
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 === '/') {
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'
}
})
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
) {
if (
await exists('game/' + key + '/' + verInfo.executable, {
baseDir: BaseDirectory.AppLocalData
})
)
await remove('game/' + key + '/' + verInfo.executable, {
baseDir: BaseDirectory.AppLocalData,
recursive: true
})
}
}
})()
}, [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<string>('theme', v => setTheme(v ?? 'dark'))
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%')
)
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 className={theme + '-theme'}>
{loading ? (
<>
<div
className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)'
hidden={platformName != 'windows'}
/>
<div
className={`w-screen ${
platformName == 'windows'
? 'h-[calc(100vh-64px)]'
: 'h-screen'
} flex items-center justify-center`}
>
<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,
theme,
versionsList,
modsList
}}
>
<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>
)}
</div>
</GlobalProvider>
)}
</body>
</html>
</>
)
}
+140
View File
@@ -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>
)
}
+93
View File
@@ -0,0 +1,93 @@
'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 Dropdown from '@/components/Dropdown'
export default function Settings () {
const {
settings,
version,
notificationsAllowed,
sidebarAlwaysShowGames,
linuxUseWine,
linuxWineCommand,
theme
} = useGlobal()
return (
<>
<p className='text-3xl ml-4 mt-4'>Settings</p>
<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={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>
<div className='flex flex-row gap-2 items-center'>
<p
title='The theme you want the launcher to use.'
className='text-lg'
>
Theme:
</p>
<Dropdown
value={theme}
options={[
{ label: 'Dark', value: 'dark' },
{ label: 'Red', value: 'red' },
{ label: 'Blue', value: 'blue' },
{ label: 'Purple', value: 'purple' }
]}
onChange={async val => {
await settings?.set('theme', val)
}}
/>
</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}`, notificationsAllowed)
}
title='The current launcher version.'
>
v{version}
</p>
</>
)
}
-127
View File
@@ -1,127 +0,0 @@
'use client'
import { useEffect } from 'react'
import './Installs.css'
import { useGlobal } from './GlobalProvider'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faCheck,
faShieldHalved,
faWarning
} from '@fortawesome/free-solid-svg-icons'
import { useRouter } from 'next/navigation'
export default function Installs () {
const {
showPopup,
setShowPopup,
setPopupMode,
setFadeOut,
setSelectedVersionList,
downloadedVersionsConfig,
normalConfig,
setSelectedGame,
getListOfGames,
getVersionsAmountData,
setCategory
} = useGlobal()
const router = useRouter()
useEffect(() => {
if (!showPopup) return
setSelectedVersionList([])
}, [normalConfig, setSelectedVersionList, showPopup])
return (
<div className='mx-4 mt-4'>
<div className='flex justify-between items-center mb-4'>
<p className='text-3xl'>Games</p>
<button
className='button btntheme1'
onClick={() => {
setSelectedGame(null)
setPopupMode(0)
setShowPopup(true)
setFadeOut(false)
}}
title='Click to download more games.'
>
Download game
</button>
</div>
<div className='downloads-container'>
<div className='downloads-scroll h-[calc(100vh-84px)]'>
{downloadedVersionsConfig &&
Object.keys(downloadedVersionsConfig.list).length ? (
getListOfGames()
.sort((a, b) => {
return a.id - b.id
})
.map(i => (
<div
key={i.id}
className={'downloads-entry'}
title={'Click to view game installs'}
onClick={() => {
setCategory(-1)
router.push('/game?id=' + i.id)
}}
>
<div className='h-18 w-screen relative'>
<p className='text-2xl'>{i.name}</p>
<div className='flex gap-2 absolute left-0 bottom-0'>
<div
className='entry-info-item'
title='The amount of versions installed of this game in installed/installable format.'
onClick={e => e.stopPropagation()}
>
<p>
{(() => {
const data = getVersionsAmountData(i.id)
if (!data) return 'N/A'
return `${data.installed}/${data.total}`
})()}{' '}
versions installed
</p>
</div>
<div
className='entry-info-item'
hidden={!i.official}
title='This game is official.'
onClick={e => e.stopPropagation()}
>
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
<p>Official</p>
</div>
<div
className='entry-info-item'
hidden={i.official}
title={
i.verified
? 'This game is verified to be safe'
: 'This game is not verified to be save. Proceed with caution.'
}
onClick={e => e.stopPropagation()}
>
<FontAwesomeIcon
icon={i.verified ? faShieldHalved : faWarning}
color={i.verified ? '#19c84b' : '#ffc800'}
/>
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
</div>
</div>
</div>
</div>
))
) : (
<div className='flex justify-center items-center h-full'>
<p className='text-3xl'>No games installed</p>
</div>
)}
</div>
</div>
</div>
)
}
-156
View File
@@ -1,156 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { Setting } from '@/componets/Setting'
import { writeNormalConfig } from '@/lib/BazookaManager'
import { useGlobal } from '@/app/GlobalProvider'
import { platform } from '@tauri-apps/plugin-os'
export default function Settings () {
const [alwaysShowGamesInSidebar, setAlwaysShowGamesInSidebar] =
useState(false)
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
const [wineOnUnixCommand, setWineOnUnixCommand] = useState('wine %path%')
const [theme, setTheme] = useState(0)
const [loaded, setLoaded] = useState(false)
const { normalConfig, setNormalConfig, version } = useGlobal()
useEffect(() => {
;(async () => {
while (normalConfig != null) {
setAlwaysShowGamesInSidebar(
normalConfig.settings.alwaysShowGamesInSidebar
)
setUseWineOnUnixWhenNeeded(
normalConfig.settings.useWineOnUnixWhenNeeded
)
setWineOnUnixCommand(normalConfig.settings.wineOnUnixCommand)
setTheme(normalConfig.settings.theme)
setLoaded(true)
break
}
})()
}, [normalConfig])
return (
<>
<p className='text-3xl ml-4 mt-4'>Settings</p>
{loaded && (
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
<Setting
label='Always show games in sidebar'
value={alwaysShowGamesInSidebar}
onChange={async () => {
if (!normalConfig) return
setAlwaysShowGamesInSidebar(!alwaysShowGamesInSidebar)
setNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
}
})
await writeNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
}
})
}}
title="This setting will make it so when you are on a page like this, the games won't disappear."
/>
<Setting
label='Use wine when needed to launch games'
value={useWineOnUnixWhenNeeded}
onChange={async () => {
if (!normalConfig) return
setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded)
setNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
useWineOnUnixWhenNeeded: !useWineOnUnixWhenNeeded
}
})
await writeNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
useWineOnUnixWhenNeeded: !useWineOnUnixWhenNeeded
}
})
}}
className={platform() == 'linux' ? '' : 'hidden'}
/>
<p hidden={!(platform() == 'linux' && useWineOnUnixWhenNeeded)}>
Wine Command:
</p>
<input
type='text'
value={wineOnUnixCommand}
onChange={async e => {
if (!normalConfig) return
setWineOnUnixCommand(e.target.value)
setNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
wineOnUnixCommand: e.target.value
}
})
await writeNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
wineOnUnixCommand: e.target.value
}
})
}}
className={`input-field my-1 ${
platform() == 'linux' && useWineOnUnixWhenNeeded ? '' : 'hidden'
}`}
></input>
<div title='The theme you want the launcher to use.'>
<label className='text-lg'>Theme:</label>
<select
className='ml-2 bg-(--col2) border border-(--col4) rounded-md'
value={theme}
onChange={async e => {
if (!normalConfig) return
const newTheme = parseInt(e.target.value)
setTheme(newTheme)
setNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
theme: newTheme
}
})
await writeNormalConfig({
...normalConfig,
settings: {
...normalConfig.settings,
theme: newTheme
}
})
}}
>
<option value={0}>Dark (default)</option>
<option value={1}>Red</option>
<option value={2}>Blue</option>
<option value={3}>Purple</option>
</select>
</div>
</div>
)}
<p
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
title='The current launcher version.'
>
v{version}
</p>
</>
)
}
+5
View File
@@ -0,0 +1,5 @@
'use client'
export default function UpdateCheckingPage () {
return <p className='text-5xl text-center'>Checking for updates</p>
}
+42
View File
@@ -0,0 +1,42 @@
'use client'
import '@/styles/globals.css'
import { platform } from '@tauri-apps/plugin-os'
import { Roboto } from 'next/font/google'
import { useEffect, useState } from 'react'
const roboto = Roboto({
subsets: ['latin']
})
export default function RootLayout ({
children
}: {
children: React.ReactNode
}) {
const [platformName, setPlatformName] = useState<string | null>(null)
useEffect(() => {
setTimeout(() => setPlatformName(platform()), 0)
}, [])
return (
<>
<html lang='en' className={roboto.className}>
<body className='dark-theme'>
<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`}
>
{children}
</div>
</body>
</html>
</>
)
}
+20
View File
@@ -0,0 +1,20 @@
'use client'
import { openUrl } from '@tauri-apps/plugin-opener'
export default function UpdateCheckingPage () {
return (
<div className='text-center'>
<p className='text-6xl mb-4'>Outdated Launcher!</p>
<p className='text-2xl mb-4'>
Please update to the latest version to continue.
</p>
<button
className='button btntheme1'
onClick={() => openUrl('https://games.lncvrt.xyz/download')}
>
Download latest version!
</button>
</div>
)
}
+5
View File
@@ -0,0 +1,5 @@
'use client'
export default function UpdateCheckingPage () {
return <p className='text-5xl text-center'>Updating launcher</p>
}
+61
View File
@@ -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>
)
}
+17
View File
@@ -0,0 +1,17 @@
type ProgressBarProps = {
progress: number
className?: string
}
export default function ProgressBar ({ progress, className }: ProgressBarProps) {
return (
<div
className={`w-full bg-(--col1) border border-(--col5) rounded-full h-4 overflow-hidden ${className}`}
>
<div
className='bg-(--col8) border-r border-r-(--col5) h-full'
style={{ width: `${progress}%` }}
/>
</div>
)
}
@@ -1,4 +1,4 @@
import './Setting.css'
import '@/styles/setting.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { SettingProps } from '@/types/SettingProps'
@@ -1,6 +1,6 @@
'use client'
import './Sidebar.css'
import '@/styles/sidebar.css'
import Icon from '@/assets/Icon.png'
import { openUrl } from '@tauri-apps/plugin-opener'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -13,10 +13,11 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
import { platform } from '@tauri-apps/plugin-os'
import { useGlobal } from '@/app/GlobalProvider'
import { useGlobal } from '@/providers/GlobalProvider'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { Lexend } from 'next/font/google'
import React from 'react'
@@ -26,16 +27,16 @@ const lexend = Lexend({
export default function Sidebar () {
const {
normalConfig,
getListOfGames,
serverVersionList,
versionsList,
setShowPopup,
setPopupMode,
setFadeOut,
downloadProgress,
downloadedVersionsConfig,
getVersionInfo,
category,
setCategory
setCategory,
sidebarAlwaysShowGames,
linuxUseWine
} = useGlobal()
const pathname = usePathname()
@@ -44,7 +45,36 @@ export default function Sidebar () {
return (
<aside className='sidebar'>
<div className='flex items-center h-10 w-60'>
<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
@@ -53,37 +83,42 @@ export default function Sidebar () {
<nav className='nav-links overflow-auto pt-2'>
<Link
draggable={false}
href='/'
href='/main'
className={`link relative flex items-center ${
pathname === '/' || pathname.startsWith('/game') ? 'active' : ''
pathname === '/main' || pathname.startsWith('/main/game')
? 'active'
: ''
}`}
>
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Games
</Link>
{getListOfGames()
.sort((a, b) => {
return a.id - b.id
})
{serverVersionList?.games
.filter(g =>
serverVersionList.versions
.filter(v => v.game === g.id)
.some(v => Object.keys(versionsList).includes(v.id))
)
.map(i => (
<React.Fragment key={i.id}>
<div
draggable={false}
className={`link ${
(pathname === '/game' &&
(pathname === '/main/game' &&
Number(params.get('id') || 0) == i.id) ||
(i.id == 1 && pathname === '/game/berrydash/leaderboards')
(i.id == 1 &&
pathname === '/main/game/berrydash/leaderboards')
? 'active'
: ''
} ml-auto w-50 ${
normalConfig?.settings.alwaysShowGamesInSidebar ||
sidebarAlwaysShowGames ||
pathname === '/' ||
pathname.startsWith('/game')
pathname.startsWith('/main/game')
? ''
: 'hidden'
}`}
onClick={() => {
setCategory(-1)
router.push('/game?id=' + i.id)
router.push('/main/game?id=' + i.id)
}}
>
<div className='flex items-center'>
@@ -101,17 +136,13 @@ export default function Sidebar () {
{Object.entries(i.categoryNames)
.sort(([a], [b]) => Number(b) - Number(a))
.filter(([key]) => {
const count = Object.keys(
downloadedVersionsConfig?.list ?? {}
).filter(v => {
const info = getVersionInfo(v)
const count = Object.keys(versionsList).filter(v => {
const info = serverVersionList.versions.find(
vf => vf.id == v
)
if (!info) return false
if (
platform() == 'linux' &&
info.wine &&
!normalConfig?.settings.useWineOnUnixWhenNeeded
)
if (platform() == 'linux' && info.wine && !linuxUseWine)
return false
return info.game === i.id && info.category === Number(key)
@@ -124,23 +155,23 @@ export default function Sidebar () {
key={`${i.id}-${key}`}
draggable={false}
className={`link ${
(pathname === '/game' &&
((pathname === '/main/game' &&
Number(params.get('id') || 0) == i.id) ||
(i.id == 1 &&
pathname === '/game/berrydash/leaderboards' &&
category == Number(key))
(i.id == 1 &&
pathname === '/main/game/berrydash/leaderboards')) &&
category == Number(key)
? 'active'
: ''
} ml-auto w-47.5 ${
normalConfig?.settings.alwaysShowGamesInSidebar ||
sidebarAlwaysShowGames ||
pathname === '/' ||
pathname.startsWith('/game')
pathname.startsWith('/main/game')
? ''
: 'hidden'
}`}
onClick={() => {
setCategory(Number(key))
router.push('/game?id=' + i.id)
router.push('/main/game?id=' + i.id)
}}
>
<div className='flex items-center'>
@@ -153,8 +184,8 @@ export default function Sidebar () {
))}
<Link
draggable={false}
href='/settings'
className={`link ${pathname === '/settings' ? 'active' : ''}`}
href='/main/settings'
className={`link ${pathname === '/main/settings' ? 'active' : ''}`}
>
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
</Link>
-68
View File
@@ -1,68 +0,0 @@
import { useGlobal } from '@/app/GlobalProvider'
export default function DownloadsPopup () {
const {
downloadProgress,
getVersionInfo,
setDownloadProgress,
downloadQueue,
setDownloadQueue
} = useGlobal()
return (
<>
<p className='text-xl text-center'>Downloads</p>
<div className='popup-content'>
{downloadProgress.map((v, i) => {
const queuePosition = downloadQueue.indexOf(v.version)
return (
<div key={i} className='popup-entry flex flex-col justify-between'>
<p className='text-2xl text-center'>
{getVersionInfo(v.version)?.displayName}
</p>
<div className='mt-6.25 flex items-center justify-between'>
{v.failed || v.queued ? (
<div className='flex items-center justify-between w-full'>
<span
className={`${
v.failed ? 'text-red-500' : 'text-yellow-300'
} inline-block text-center flex-1`}
>
{v.failed
? 'Download failed'
: queuePosition === 0
? 'Starting soon...'
: `Queued (Position ${queuePosition + 1})`}
</span>
<button
className='button btntheme3 -ml-1.25'
onClick={() => {
setDownloadQueue(prev =>
prev.filter(id => id !== v.version)
)
setDownloadProgress(prev =>
prev.filter(d => d.version !== v.version)
)
}}
title='Click to remove this version from the download queue.'
>
Remove
</button>
</div>
) : v.finishing ? (
<span className='text-green-300 inline-block w-full text-center'>
Finishing...
</span>
) : (
<div className='flex flex-col gap-1 w-full'>
<span className='text-center'>Downloading...</span>
</div>
)}
</div>
</div>
)
})}
</div>
</>
)
}
-148
View File
@@ -1,148 +0,0 @@
import { app } from '@tauri-apps/api'
import { NormalConfig } from '@/types/NormalConfig'
import {
BaseDirectory,
create,
exists,
mkdir,
readTextFile,
writeFile
} from '@tauri-apps/plugin-fs'
import { VersionsConfig } from '@/types/VersionsConfig'
export async function readNormalConfig (): Promise<NormalConfig> {
const version = await app.getVersion()
try {
const options = {
baseDir: BaseDirectory.AppLocalData
}
const doesFolderExist = await exists('', options)
const doesConfigExist = await exists('config.json', options)
if (!doesFolderExist || !doesConfigExist) {
if (!doesFolderExist) {
await mkdir('', options)
}
const file = await create('config.json', options)
await file.write(
new TextEncoder().encode(
JSON.stringify(new NormalConfig(version), null, 2)
)
)
await file.close()
return new NormalConfig(version)
}
const config = await readTextFile('config.json', options)
const raw = JSON.parse(config)
if (
raw.settings &&
raw.settings.theme &&
(raw.version == '1.0.0' ||
raw.version == '1.1.0' ||
raw.version == '1.1.1' ||
raw.version == '1.2.0' ||
raw.version == '1.3.0' ||
raw.version == '1.3.1' ||
raw.version == '1.4.0' ||
raw.version == '1.5.0' ||
raw.version == '1.5.1' ||
raw.version == '1.5.2' ||
raw.version == '1.5.3' ||
raw.version == '1.5.4')
) {
const parsed = Number(raw.settings.theme)
if (parsed == 3) raw.settings.theme = 2
if (parsed == 4) raw.settings.theme = 3
else if (parsed != 0 && parsed != 1) raw.settings.theme = 0
}
raw.version = version
writeNormalConfig(raw)
return NormalConfig.import(raw)
} catch {
return new NormalConfig(version)
}
}
export async function writeNormalConfig (data: NormalConfig) {
const options = {
baseDir: BaseDirectory.AppLocalData
}
const doesFolderExist = await exists('', options)
const doesConfigExist = await exists('config.json', options)
if (!doesFolderExist || !doesConfigExist) {
if (!doesFolderExist) {
await mkdir('', options)
}
const file = await create('config.json', options)
await file.write(new TextEncoder().encode(JSON.stringify(data, null, 2)))
await file.close()
} else {
await writeFile(
'config.json',
new TextEncoder().encode(JSON.stringify(data, null, 2)),
options
)
}
}
export async function readVersionsConfig (): Promise<VersionsConfig> {
const version = await app.getVersion()
try {
const options = {
baseDir: BaseDirectory.AppLocalData
}
const doesFolderExist = await exists('', options)
const doesConfigExist = await exists('versions.json', options)
if (!doesFolderExist || !doesConfigExist) {
if (!doesFolderExist) {
await mkdir('', options)
}
const file = await create('versions.json', options)
await file.write(
new TextEncoder().encode(
JSON.stringify(new VersionsConfig(version), null, 2)
)
)
await file.close()
return new VersionsConfig(version)
}
const config = await readTextFile('versions.json', options)
const raw = JSON.parse(config)
if (raw.list && raw.timestamps) {
raw.list = raw.timestamps
delete raw.timestamps
await writeFile(
'versions.json',
new TextEncoder().encode(JSON.stringify(raw, null, 2)),
options
)
}
raw.version = version
writeVersionsConfig(raw)
return VersionsConfig.import(raw)
} catch {
return new VersionsConfig(version)
}
}
export async function writeVersionsConfig (data: VersionsConfig) {
const options = {
baseDir: BaseDirectory.AppLocalData
}
const doesFolderExist = await exists('', options)
const doesConfigExist = await exists('versions.json', options)
if (!doesFolderExist || !doesConfigExist) {
if (!doesFolderExist) {
await mkdir('', options)
}
const file = await create('versions.json', options)
await file.write(new TextEncoder().encode(JSON.stringify(data, null, 2)))
await file.close()
} else {
await writeFile(
'versions.json',
new TextEncoder().encode(JSON.stringify(data, null, 2)),
options
)
}
}
File renamed without changes.
+11
View File
@@ -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')
}
+17
View File
@@ -0,0 +1,17 @@
import {
isPermissionGranted,
requestPermission,
sendNotification
} from '@tauri-apps/plugin-notification'
export async function notifyUser (title: string, body: string) {
let permissionGranted = await isPermissionGranted()
if (!permissionGranted) {
const permission = await requestPermission()
permissionGranted = permission === 'granted'
}
if (permissionGranted) {
sendNotification({ title, body })
}
}
+14
View File
@@ -2,6 +2,7 @@ import { message } from '@tauri-apps/plugin-dialog'
import { BaseDirectory, exists, stat } from '@tauri-apps/plugin-fs'
import { openPath } from '@tauri-apps/plugin-opener'
import { join, appLocalDataDir } from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/core'
export const openFolder = async (name: string) => {
const relativePath = await join('game', name)
@@ -48,3 +49,16 @@ export const formatEtaSmart = (seconds: number) => {
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
}
}
+248
View File
@@ -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)} &bull; Speed:{' '}
{prettyBytes(v.speed, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})}
/s)
</span>
<ProgressBar progress={v.progress} className='w-full' />
</div>
)}
</div>
</div>
)
})}
</div>
</>
)
}
@@ -1,4 +1,4 @@
import { useGlobal } from '@/app/GlobalProvider'
import { useGlobal } from '@/providers/GlobalProvider'
import {
faCheck,
faCode,
@@ -9,18 +9,18 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export default function GamesDownloadPopup () {
const { serverVersionList, getVersionsAmountData, setSelectedGame } =
useGlobal()
const { serverVersionList, setSelectedGame, versionsList } = useGlobal()
return (
<>
<p className='text-xl text-center'>Select a game to download</p>
<div className='popup-content'>
{serverVersionList?.games
.filter(v => {
const data = getVersionsAmountData(v.id)
if (!data) return false
if (data.total > 0) return true
.filter(i => {
const gameVersions = serverVersionList.versions.filter(
vf => vf.game === i.id
)
if (gameVersions.length > 0) return true
})
.map((v, i) => (
<div key={i} className='popup-entry'>
@@ -32,9 +32,15 @@ export default function GamesDownloadPopup () {
>
<p>
{(() => {
const data = getVersionsAmountData(v.id)
if (!data) return 'N/A'
return `${data.installed}/${data.total}`
const gameVersions = serverVersionList.versions.filter(
vf => vf.game === v.id
)
const installed = gameVersions.filter(v =>
Object.keys(versionsList).includes(v.id)
).length
return gameVersions.length
? `${installed}/${gameVersions.length}`
: 'N/A'
})()}{' '}
versions installed
</p>
@@ -74,11 +80,7 @@ export default function GamesDownloadPopup () {
<button
className='button btntheme3 right-2 bottom-2'
onClick={() => setSelectedGame(v.id)}
title={`Click to download specific versions of the game. You have ${(() => {
const data = getVersionsAmountData(v.id)
if (!data) return 'N/A'
return `${data.installed} of ${data.total}`
})()} versions downloaded.`}
title='Click to download more versions of the game.'
>
<>
<FontAwesomeIcon icon={faDownload} /> Download
@@ -4,35 +4,50 @@ import {
faArrowUpRightFromSquare,
faCheck,
faCode,
faHardDrive,
faShieldHalved,
faWarning
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useGlobal } from '@/app/GlobalProvider'
import { message } from '@tauri-apps/plugin-dialog'
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 { writeVersionsConfig } from '@/lib/BazookaManager'
import { openFolder } from '@/lib/Util'
import { openFolder } from '@/lib/util'
export default function VersionVersionPopup () {
export default function ManageVersionPopup () {
const {
getGameInfo,
getVersionInfo,
versionsList,
serverVersionList,
managingVersion,
downloadedVersionsConfig,
viewingInfoFromDownloads,
setManagingVersion,
closePopup,
setDownloadedVersionsConfig,
setPopupMode,
setSelectedVersionList,
downloadVersions
downloadVersions,
versions
} = useGlobal()
const [versionSize, setVersionSize] = useState<number>(0)
if (!managingVersion || !downloadedVersionsConfig) return <></>
useEffect(() => {
if (viewingInfoFromDownloads) return
invoke<string>('folder_size', {
version: managingVersion
}).then(size => {
setVersionSize(Number(size))
})
}, [managingVersion, setVersionSize, viewingInfoFromDownloads])
const versionInfo = getVersionInfo(managingVersion)
const gameInfo = getGameInfo(versionInfo?.game)
if (!managingVersion) return <></>
const versionInfo = serverVersionList?.versions.find(
vf => vf.id == managingVersion
)
const gameInfo = serverVersionList?.games.find(
vf => vf.id == versionInfo?.game
)
return (
<>
@@ -45,7 +60,7 @@ export default function VersionVersionPopup () {
<p>
Installed{' '}
{new Intl.DateTimeFormat(undefined).format(
downloadedVersionsConfig.list[managingVersion] ?? 0
versionsList[managingVersion] ?? 0
)}
</p>
</div>
@@ -80,13 +95,22 @@ export default function VersionVersionPopup () {
</div>
<div
className='entry-info-item btntheme2'
onClick={async () => {
if (!versionInfo) return
await message(atob(versionInfo.changelog), {
title: 'Changelog for ' + versionInfo.displayName,
kind: 'info'
})
}}
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>
@@ -106,18 +130,14 @@ export default function VersionVersionPopup () {
onClick={async () => {
closePopup()
setDownloadedVersionsConfig(prev => {
if (!prev) return prev
const updatedList = Object.fromEntries(
Object.entries(prev.list).filter(([k]) => k !== managingVersion)
versions?.set(
'list',
Object.fromEntries(
Object.entries(versionsList).filter(
([k]) => k !== managingVersion
)
)
const updatedConfig = {
...prev,
list: updatedList
}
writeVersionsConfig(updatedConfig)
return updatedConfig
})
)
if (
await exists('game/' + managingVersion, {
@@ -142,18 +162,14 @@ export default function VersionVersionPopup () {
setPopupMode(1)
//uninstall
setDownloadedVersionsConfig(prev => {
if (!prev) return prev
const updatedList = Object.fromEntries(
Object.entries(prev.list).filter(([k]) => k !== managingVersion)
versions?.set(
'list',
Object.fromEntries(
Object.entries(versionsList).filter(
([k]) => k !== managingVersion
)
)
const updatedConfig = {
...prev,
list: updatedList
}
writeVersionsConfig(updatedConfig)
return updatedConfig
})
)
if (
await exists('game/' + managingVersion, {
@@ -167,7 +183,12 @@ export default function VersionVersionPopup () {
//reinstall
setSelectedVersionList([managingVersion])
downloadVersions([managingVersion])
downloadVersions([
{
id: managingVersion,
type: 0
}
])
}}
title="Click to reinstall this game. This will NOT remove any progress or any save files. This WILL uninstall any modifications to the game's executable files."
hidden={viewingInfoFromDownloads}
+368
View File
@@ -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>
</>
)
}
+34
View File
@@ -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, '&bull; ')
.replaceAll(/`([^`]+)`/g, '<code>$1</code>')
)
}}
></p>
</div>
</>
)
}
@@ -1,21 +1,23 @@
import { useGlobal } from '@/app/GlobalProvider'
import { useGlobal } from '@/providers/GlobalProvider'
import { faAdd, faInfo, faRemove } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
export default function VersionsDownloadPopup () {
const {
serverVersionList,
getSpecialVersionsList,
selectedVersionList,
setSelectedVersionList,
setManagingVersion,
setPopupMode,
getSpecialVersionsList,
selectedGame,
setViewingInfoFromDownloads,
downloadedVersionsConfig,
downloadVersions,
getGameInfo
downloadVersions
} = useGlobal()
if (!selectedGame) return <></>
const game = serverVersionList?.games.find(g => g.id === selectedGame)
if (!selectedGame || !game) return <></>
return (
<>
@@ -77,19 +79,18 @@ export default function VersionsDownloadPopup () {
<button
className='button btntheme1 w-fit mt-2 -mb-4'
onClick={() => {
if (downloadedVersionsConfig) {
downloadVersions(selectedVersionList)
setPopupMode(1)
}
downloadVersions(
selectedVersionList.map(versionId => ({
id: versionId,
type: 0
}))
)
setPopupMode(1)
}}
disabled={selectedVersionList.length == 0}
title={
selectedVersionList.length == 0
? 'Select at least one version to download'
: `Download ${selectedVersionList.length} version${
selectedVersionList.length == 1 ? '' : 's'
} of ${getGameInfo(selectedGame)?.name}`
}
title={`Download ${selectedVersionList.length} version${
selectedVersionList.length == 1 ? '' : 's'
} of ${game.name}`}
>
Download {selectedVersionList.length} version
{selectedVersionList.length == 1 ? '' : 's'}
@@ -8,11 +8,10 @@ import {
SetStateAction
} from 'react'
import { DownloadProgress } from '@/types/DownloadProgress'
import { VersionsConfig } from '@/types/VersionsConfig'
import { NormalConfig } from '@/types/NormalConfig'
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
import { Mod } from '@/types/Mod'
import { Store } from '@tauri-apps/plugin-store'
import { GameVersion } from '@/types/GameVersion'
import { Game } from '@/types/Game'
type GlobalCtxType = {
serverVersionList: ServerVersionsResponse | null
@@ -26,31 +25,40 @@ type GlobalCtxType = {
setPopupMode: Dispatch<SetStateAction<number | null>>
fadeOut: boolean
setFadeOut: Dispatch<SetStateAction<boolean>>
downloadedVersionsConfig: VersionsConfig | null
setDownloadedVersionsConfig: Dispatch<SetStateAction<VersionsConfig | null>>
normalConfig: NormalConfig | null
setNormalConfig: Dispatch<SetStateAction<NormalConfig | null>>
managingVersion: string | null
setManagingVersion: Dispatch<SetStateAction<string | null>>
setSelectedGame: Dispatch<SetStateAction<number | null>>
getVersionInfo: (id: string | undefined) => GameVersion | undefined
getGameInfo: (game: number | undefined) => Game | undefined
getListOfGames(): Game[]
getVersionsAmountData: (gameId: number) => {
installed: number
total: number
} | null
viewingInfoFromDownloads: boolean
version: string | null
downloadVersions: (list: string[]) => Promise<void>
downloadVersions: (
list: Array<{
id: string
type: 0 | 1 | 2
modDownload?: number
gameId?: number
modId?: number
modVersion?: string
}>
) => Promise<void>
category: number
setCategory: Dispatch<SetStateAction<number>>
downloadQueue: string[]
setDownloadQueue: Dispatch<SetStateAction<string[]>>
closePopup: () => void
getSpecialVersionsList(game?: number | undefined): GameVersion[]
selectedGame: number | null
setViewingInfoFromDownloads: Dispatch<SetStateAction<boolean>>
showModInfo: Mod | null
setShowModInfo: Dispatch<SetStateAction<Mod | null>>
getSpecialVersionsList: (game?: number) => GameVersion[]
settings: Store | null
versions: Store | null
notificationsAllowed: boolean
sidebarAlwaysShowGames: boolean
linuxUseWine: boolean
linuxWineCommand: string
theme: string
versionsList: Record<string, number>
modsList: Record<string, Record<string, number>>
}
const GlobalCtx = createContext<GlobalCtxType | null>(null)
+60
View File
@@ -0,0 +1,60 @@
@import "tailwindcss";
.dropdown {
@apply relative w-40;
}
.dropdown-btn {
@apply w-full flex items-center justify-between px-3 py-1.5 rounded-md cursor-pointer
bg-(--col2) hover:bg-(--col3)
border border-(--col4) hover:border-(--col5)
text-white;
}
.dropdown-arrow {
@apply w-4 h-4 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-lg 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-3 py-1.5 cursor-pointer text-white transition-colors;
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);
}
}
+14 -2
View File
@@ -1,5 +1,9 @@
@import "tailwindcss";
html {
overscroll-behavior: none;
}
body {
@apply bg-(--col0) text-white select-none;
}
@@ -14,6 +18,7 @@ body {
--col6: rgb(96, 96, 96);
--col7: rgb(112, 112, 112);
--col8: rgb(128, 128, 128);
--col9: rgb(144, 144, 144);
}
.red-theme {
@@ -26,6 +31,7 @@ body {
--col6: rgb(120, 96, 96);
--col7: rgb(136, 112, 112);
--col8: rgb(152, 128, 128);
--col9: rgb(168, 144, 144);
}
.blue-theme {
@@ -38,6 +44,7 @@ body {
--col6: rgb(96, 96, 120);
--col7: rgb(112, 112, 136);
--col8: rgb(128, 128, 152);
--col9: rgb(144, 144, 168);
}
.purple-theme {
@@ -50,6 +57,7 @@ body {
--col6: rgb(108, 96, 120);
--col7: rgb(124, 112, 136);
--col8: rgb(140, 128, 152);
--col9: rgb(156, 144, 168);
}
.button {
@@ -126,8 +134,8 @@ body {
@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;
.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 {
@@ -141,3 +149,7 @@ body {
.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;
}
File renamed without changes.
File renamed without changes.
+20 -1
View File
@@ -1,8 +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 finishing: 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
) {}
}
+2 -1
View File
@@ -8,6 +8,7 @@ export interface GameVersion {
wine: number | undefined
category: number
lastRevision: number
download: number
download: number | undefined
executable: string
modSupportDownload: number | undefined
}
+12
View File
@@ -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
}
-21
View File
@@ -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
}
}
-9
View File
@@ -1,9 +0,0 @@
export class SettingsType {
constructor (
public allowNotifications: boolean = true,
public alwaysShowGamesInSidebar: boolean = true,
public useWineOnUnixWhenNeeded: boolean = false,
public wineOnUnixCommand: string = 'wine %path%',
public theme: number = 0
) {}
}
-17
View File
@@ -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
}
}