Compare commits
41 Commits
3.1.5
..
bde922ab9f
| Author | SHA1 | Date | |
|---|---|---|---|
| bde922ab9f | |||
| a1f96a7de3 | |||
| 3b2c902264 | |||
| 40a32d7a60 | |||
| 52c60acf37 | |||
| 2788cd6e8b | |||
| 2d1941db33 | |||
| 00958d5238 | |||
| b8a0b448c7 | |||
| 09487e945e | |||
| 2532ab0059 | |||
| 16fe91b3a5 | |||
| eca08f16c5 | |||
| e76d67f3a8 | |||
| 9811b36e58 | |||
|
0f10beb3b4
|
|||
|
4326199a56
|
|||
|
7f65384b1c
|
|||
|
aacd0d1d91
|
|||
|
079a076f99
|
|||
| 6d88c39ce2 | |||
| 56608a64fc | |||
| eaf6f5976d | |||
| 93f1f380ec | |||
| c47ea1f976 | |||
| 09af78f1aa | |||
| 3900b5c6cf | |||
| adf607e24c | |||
| 1128b51778 | |||
| d435f0c09a | |||
| b188e2dee8 | |||
| 8fd56ae749 | |||
| b2e7d22d28 | |||
| d871e06172 | |||
| 6a1c5441c3 | |||
| ef36f49a6f | |||
| 7e40536ec8 | |||
| 694d403aeb | |||
| 9515892b2f | |||
| 0a58f0f63b | |||
| 60e3ef4131 |
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"git.enableCommitSigning": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
|
||||
@@ -22,6 +22,7 @@ It enhances the osu! experience with quality-of-life features and integration sp
|
||||
- [x] Displays ping to the EZPPFarm server
|
||||
- [x] Discord Rich Presence
|
||||
- [x] Performance Display Overlay (currently in experimental patcher release stream)
|
||||
- [x] Launcher Theming
|
||||
|
||||
---
|
||||
|
||||
@@ -73,3 +74,9 @@ src-tauri/target/release/ezpplauncher.exe
|
||||
├── package.json # Bun project config
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Launcher Themes
|
||||
|
||||
By default, the launcher includes several downloadable themes. If you want to create your own custom theme, please use the [create-ezpplauncher-theme](https://git.ez-pp.farm/EZPPFarm/create-ezpplauncher-theme) repository for a template and instructions.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 157 KiB |
@@ -15,10 +15,11 @@
|
||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||
"@tauri-apps/plugin-fs": "^2.5.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@types/umami": "^2.10.1",
|
||||
"@types/node": "^25.9.1",
|
||||
"animejs": "^4.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-svelte": "^1.0.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -302,7 +303,7 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
|
||||
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
@@ -312,8 +313,6 @@
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/umami": ["@types/umami@2.10.1", "", {}, "sha512-CvOEMq+BFOUXj8yciq4hPsdOt0o9KBg5jQXnF1FrdOMKZMxJiwryyZNLa9Ud2NVO6nXSwU22YMKR3E+Xhbo+DA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.42.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/types": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg=="],
|
||||
@@ -392,6 +391,8 @@
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
|
||||
@@ -486,12 +487,16 @@
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
|
||||
|
||||
"immutable": ["immutable@5.1.3", "", {}, "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@@ -502,6 +507,8 @@
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
@@ -514,6 +521,8 @@
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
@@ -522,6 +531,8 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||
|
||||
"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.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
@@ -608,6 +619,8 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
@@ -636,6 +649,8 @@
|
||||
|
||||
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
@@ -646,6 +661,8 @@
|
||||
|
||||
"react-to-webcomponent": ["react-to-webcomponent@2.0.1", "", { "dependencies": { "@r2wc/core": "^1.0.0" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-jpVztcA0SbV6TnBRV6EFuU+hmuuSIxgEww3wRcbneT0ytqn57y9ygeRfUmhzz8+bYg53u1h4COKk8iAextHE+Q=="],
|
||||
|
||||
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
@@ -662,6 +679,8 @@
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"sass": ["sass@1.92.1", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ=="],
|
||||
|
||||
"sass-embedded": ["sass-embedded@1.92.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.92.1", "sass-embedded-android-arm": "1.92.1", "sass-embedded-android-arm64": "1.92.1", "sass-embedded-android-riscv64": "1.92.1", "sass-embedded-android-x64": "1.92.1", "sass-embedded-darwin-arm64": "1.92.1", "sass-embedded-darwin-x64": "1.92.1", "sass-embedded-linux-arm": "1.92.1", "sass-embedded-linux-arm64": "1.92.1", "sass-embedded-linux-musl-arm": "1.92.1", "sass-embedded-linux-musl-arm64": "1.92.1", "sass-embedded-linux-musl-riscv64": "1.92.1", "sass-embedded-linux-musl-x64": "1.92.1", "sass-embedded-linux-riscv64": "1.92.1", "sass-embedded-linux-x64": "1.92.1", "sass-embedded-unknown-all": "1.92.1", "sass-embedded-win32-arm64": "1.92.1", "sass-embedded-win32-x64": "1.92.1" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-28YwLnF5atAhogt3E4hXzz/NB9dwKffyw08a7DEasLh94P7+aELkG3ENSHYCWB9QFN14hYNLfwr9ozUsPDhcDQ=="],
|
||||
@@ -708,6 +727,8 @@
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
||||
|
||||
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
@@ -718,6 +739,8 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="],
|
||||
@@ -772,7 +795,7 @@
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.42.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/parser": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/utils": "8.42.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
@@ -832,6 +855,8 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"eslint/@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
|
||||
@@ -884,6 +909,8 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"eslint-plugin-svelte/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "ezpplauncher",
|
||||
"version": "3.1.5",
|
||||
"version": "3.2.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"debug": "bun run tauri dev",
|
||||
"build": "bun ./scripts/sync-version.ts && bun run vite:build && bun run tauri:build",
|
||||
"debug:theme": "bun run tauri dev -- -- \"C:\\Users\\moebius\\Desktop\\dance.ezpplauncher-theme\"",
|
||||
"build": "bun ./scripts/build.ts",
|
||||
"vite:dev": "vite dev",
|
||||
"vite:build": "vite build",
|
||||
"vite:build": "vite build && exit 0",
|
||||
"tauri:build": "tauri build",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
@@ -28,10 +29,11 @@
|
||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||
"@tauri-apps/plugin-fs": "^2.5.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@types/umami": "^2.10.1",
|
||||
"@types/node": "^25.9.1",
|
||||
"animejs": "^4.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-svelte": "^1.0.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
function runVite(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn('bunx', ['--bun', 'vite', 'build'], { stdio: ['inherit', 'pipe', 'pipe'] });
|
||||
|
||||
proc.stdout?.on('data', (data: Buffer) => {
|
||||
process.stdout.write(data);
|
||||
if (data.toString().includes('✔ done')) {
|
||||
setTimeout(() => {
|
||||
proc.kill();
|
||||
resolve();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
proc.stderr?.on('data', (data: Buffer) => process.stderr.write(data));
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0 || code === null) resolve();
|
||||
else reject(new Error(`vite build exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function run(cmd: string, args: string[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(cmd, args, { stdio: 'inherit' });
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`${cmd} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await run('bun', ['./scripts/sync-version.ts']);
|
||||
await runVite();
|
||||
await run('bunx', ['tauri', 'build']);
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["@types/bun", "@types/node"]
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
Generated
+9
-1
@@ -986,7 +986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ezpplauncher"
|
||||
version = "3.1.5"
|
||||
version = "3.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -1010,6 +1010,7 @@ dependencies = [
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tokio",
|
||||
"url",
|
||||
"widestring",
|
||||
"winapi",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -1613,6 +1614,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -4035,6 +4042,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http-range",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
|
||||
+13
-2
@@ -1,12 +1,22 @@
|
||||
[package]
|
||||
name = "ezpplauncher"
|
||||
version = "3.1.5"
|
||||
version = "3.2.0"
|
||||
description = "EZPPLauncher redefined."
|
||||
authors = ["HorizonCode"]
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1 # Allows LLVM to perform better optimization.
|
||||
lto = true # Enables link-time-optimizations.
|
||||
opt-level = "s" # Prioritizes small binary size. Use `3` if you prefer speed.
|
||||
panic = "abort" # Higher performance by disabling panic handlers.
|
||||
strip = true # Ensures debug symbols are removed.
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
@@ -18,7 +28,7 @@ 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 = ["protocol-asset"] }
|
||||
tauri-plugin-shell = "2.3.5"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
@@ -37,6 +47,7 @@ once_cell = "1.21.4"
|
||||
futures-util = "0.3.32"
|
||||
byteorder = "1.5.0"
|
||||
anyhow = "1.0.102"
|
||||
url = "2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.56.0"
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
@@ -19,58 +21,128 @@
|
||||
{
|
||||
"identifier": "fs:allow-read",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-write",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-exists",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-read-file",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-write-file",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-read-text-file",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-mkdir",
|
||||
"allow": [
|
||||
{ "path": "$HOME/.ezpplauncher" },
|
||||
{ "path": "$HOME/.ezpplauncher/*" },
|
||||
{ "path": "$HOME/.ezpplauncher/**/*" }
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-read-dir",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "fs:allow-remove",
|
||||
"allow": [
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/*"
|
||||
},
|
||||
{
|
||||
"path": "$HOME/.ezpplauncher/**/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+53
-15
@@ -12,6 +12,7 @@ use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::OpenedUrls;
|
||||
use crate::osudb::parse_osudb;
|
||||
use crate::presence;
|
||||
use crate::utils::{
|
||||
@@ -20,6 +21,11 @@ use crate::utils::{
|
||||
set_osu_config_vals, set_osu_user_config_vals,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn opened_urls(app: tauri::AppHandle) -> Vec<PathBuf> {
|
||||
app.state::<OpenedUrls>().0.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_launcher_version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
@@ -163,7 +169,12 @@ pub async fn get_beatmapsets_count(folder: String) -> Option<u64> {
|
||||
}
|
||||
let osu_db_bytes = fs::read(osu_db_path).await.ok()?;
|
||||
let osu_db = parse_osudb(osu_db_bytes).ok()?;
|
||||
let beatmap_sets = osu_db.beatmaps.iter().map(|b| b.beatmapset_id).collect::<HashSet<_>>().len();
|
||||
let beatmap_sets = osu_db
|
||||
.beatmaps
|
||||
.iter()
|
||||
.map(|b| b.beatmapset_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.len();
|
||||
Some(beatmap_sets as u64)
|
||||
}
|
||||
|
||||
@@ -242,7 +253,7 @@ pub async fn get_skins_count(folder: String) -> Option<u64> {
|
||||
let skins_folder = path.join("Skins");
|
||||
|
||||
if !skins_folder.exists() {
|
||||
return None;
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
@@ -431,21 +442,48 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
|
||||
}
|
||||
};
|
||||
|
||||
if patch {
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if patch {
|
||||
let patcher_exe_path = PathBuf::from(&folder)
|
||||
.join("EZPPLauncher")
|
||||
.join("patcher")
|
||||
.join("osu!.patcher.exe");
|
||||
|
||||
let patcher_exe_path = PathBuf::from(&folder)
|
||||
.join("EZPPLauncher")
|
||||
.join("patcher")
|
||||
.join("osu!.patcher.exe");
|
||||
if patcher_exe_path.exists() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut sys = System::new_all();
|
||||
|
||||
if patcher_exe_path.exists() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = Command::new(&patcher_exe_path)
|
||||
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to run patcher: {e}"))?;
|
||||
loop {
|
||||
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
|
||||
|
||||
let mut found = false;
|
||||
|
||||
for (_pid, process) in sys.processes() {
|
||||
if process.name() == "osu!.exe" {
|
||||
let pid = process.pid();
|
||||
let title = get_window_title_by_pid(pid);
|
||||
|
||||
if !title.is_empty() && !title.contains("updater") {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
break;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
let _ = Command::new(&patcher_exe_path)
|
||||
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to run patcher: {e}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+56
-5
@@ -1,21 +1,26 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
pub mod commands;
|
||||
pub mod osudb;
|
||||
pub mod presence;
|
||||
pub mod utils;
|
||||
pub mod osudb;
|
||||
use crate::commands::{
|
||||
check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files,
|
||||
encrypt_string, exit, find_osu_installation, get_beatmapsets_count,
|
||||
get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream,
|
||||
get_osu_skin, get_osu_version, get_platform, get_skins, get_skins_count, has_net8,
|
||||
has_osuwinello, has_wmctrl, install_ezpp_launcher_update, is_osu_running, open_url_in_browser,
|
||||
presence_connect, presence_disconnect, presence_is_connected, presence_update_status,
|
||||
presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
|
||||
set_osu_user_config_values, valid_osu_folder,
|
||||
opened_urls, presence_connect, presence_disconnect, presence_is_connected,
|
||||
presence_update_status, presence_update_user, replace_ui_files, run_osu, run_osu_updater,
|
||||
set_osu_config_values, set_osu_user_config_values, valid_osu_folder,
|
||||
};
|
||||
|
||||
struct OpenedUrls(Mutex<Vec<PathBuf>>);
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -26,14 +31,60 @@ pub fn run() {
|
||||
let mut builder = tauri::Builder::default();
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
let app_window = app.get_webview_window("main").expect("no main window");
|
||||
app_window.set_focus().expect("failed to focus");
|
||||
if args.len() > 1 {
|
||||
use tauri::Emitter;
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
for maybe_file in args.iter().skip(1) {
|
||||
if maybe_file.starts_with("-") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = PathBuf::from(maybe_file);
|
||||
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
|
||||
app.fs_scope().allow_file(&path).ok();
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
app.state::<OpenedUrls>().0.lock().unwrap().extend(files.clone());
|
||||
|
||||
app.emit("opened", files.clone()).ok();
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.manage(OpenedUrls(Mutex::new(vec![])))
|
||||
.setup(|app| {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
if args.len() > 1 {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for maybe_file in args.iter().skip(1) {
|
||||
if maybe_file.starts_with("-") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = PathBuf::from(maybe_file);
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
|
||||
app.fs_scope().allow_file(&path).ok();
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
app.state::<OpenedUrls>().0.lock().unwrap().extend(files);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
opened_urls,
|
||||
get_hwid,
|
||||
find_osu_installation,
|
||||
valid_osu_folder,
|
||||
|
||||
+211
-121
@@ -1,15 +1,19 @@
|
||||
use discord_rich_presence::{
|
||||
activity::{Activity, Assets, Button, Timestamps},
|
||||
DiscordIpc, DiscordIpcClient,
|
||||
activity::{Activity, Assets, Button, Timestamps},
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::{Duration, interval};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PresenceButton {
|
||||
pub label: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PresenceData {
|
||||
pub state: String,
|
||||
@@ -18,6 +22,21 @@ pub struct PresenceData {
|
||||
pub large_image_text: String,
|
||||
pub small_image_key: Option<String>,
|
||||
pub small_image_text: Option<String>,
|
||||
pub dynamic_button: Option<PresenceButton>,
|
||||
}
|
||||
|
||||
impl Default for PresenceData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: "Idle in Launcher...".to_string(),
|
||||
details: " ".to_string(),
|
||||
large_image_key: "ezppfarm".to_string(),
|
||||
large_image_text: "EZPPFarm".to_string(),
|
||||
small_image_key: None,
|
||||
small_image_text: None,
|
||||
dynamic_button: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -26,14 +45,21 @@ enum PresenceCommand {
|
||||
Disconnect(oneshot::Sender<()>),
|
||||
UpdateData(PresenceData),
|
||||
IsConnected(oneshot::Sender<bool>),
|
||||
Tick,
|
||||
}
|
||||
|
||||
enum ReconnectResult {
|
||||
Connected(DiscordIpcClient),
|
||||
Failed,
|
||||
}
|
||||
|
||||
struct PresenceActor {
|
||||
receiver: mpsc::Receiver<PresenceCommand>,
|
||||
reconnect_rx: mpsc::Receiver<ReconnectResult>,
|
||||
reconnect_tx: mpsc::Sender<ReconnectResult>,
|
||||
client: Option<DiscordIpcClient>,
|
||||
data: PresenceData,
|
||||
start_timestamp: i64,
|
||||
reconnecting: bool,
|
||||
}
|
||||
|
||||
impl PresenceActor {
|
||||
@@ -43,153 +69,183 @@ impl PresenceActor {
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
let data = PresenceData {
|
||||
state: "Idle in Launcher...".to_string(),
|
||||
details: " ".to_string(),
|
||||
large_image_key: "ezppfarm".to_string(),
|
||||
large_image_text: "EZPPFarm".to_string(),
|
||||
small_image_key: None,
|
||||
small_image_text: None,
|
||||
};
|
||||
let (reconnect_tx, reconnect_rx) = mpsc::channel(1);
|
||||
|
||||
PresenceActor {
|
||||
Self {
|
||||
receiver,
|
||||
reconnect_rx,
|
||||
reconnect_tx,
|
||||
client: None,
|
||||
data,
|
||||
data: PresenceData::default(),
|
||||
start_timestamp: start,
|
||||
reconnecting: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self) {
|
||||
let mut update_interval = interval(Duration::from_secs(5));
|
||||
let mut heartbeat = interval(Duration::from_secs(5));
|
||||
heartbeat.tick().await;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(cmd) = self.receiver.recv() => {
|
||||
match cmd {
|
||||
PresenceCommand::Connect(responder) => self.handle_connect(responder).await,
|
||||
PresenceCommand::Disconnect(responder) => {
|
||||
self.handle_disconnect(responder).await;
|
||||
},
|
||||
PresenceCommand::UpdateData(new_data) => {
|
||||
self.data = new_data;
|
||||
},
|
||||
PresenceCommand::IsConnected(responder) => {
|
||||
let _ = responder.send(self.client.is_some());
|
||||
PresenceCommand::Connect(resp) => self.cmd_connect(resp).await,
|
||||
PresenceCommand::Disconnect(resp) => self.cmd_disconnect(resp).await,
|
||||
PresenceCommand::UpdateData(data) => {
|
||||
self.data = data;
|
||||
self.push_presence().await;
|
||||
}
|
||||
PresenceCommand::Tick => {
|
||||
if self.client.is_some() {
|
||||
self.handle_update().await;
|
||||
}
|
||||
PresenceCommand::IsConnected(resp) => {
|
||||
let _ = resp.send(self.client.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = update_interval.tick() => {
|
||||
let _ = PRESENCE_TX.send(PresenceCommand::Tick).await;
|
||||
|
||||
Some(result) = self.reconnect_rx.recv() => {
|
||||
self.reconnecting = false;
|
||||
match result {
|
||||
ReconnectResult::Connected(client) => {
|
||||
println!("Presence: reconnect succeeded.");
|
||||
self.client = Some(client);
|
||||
self.push_presence().await;
|
||||
}
|
||||
ReconnectResult::Failed => {
|
||||
eprintln!("Presence: all reconnect attempts exhausted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = heartbeat.tick() => {
|
||||
self.push_presence().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, responder: oneshot::Sender<bool>) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
async fn cmd_connect(&mut self, resp: oneshot::Sender<bool>) {
|
||||
if self.client.is_some() {
|
||||
let _ = responder.send(true);
|
||||
let _ = resp.send(true);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Actor: Connecting to Discord...");
|
||||
let mut dc_client = DiscordIpcClient::new("1032772293220384808");
|
||||
match dc_client.connect() {
|
||||
Ok(()) => {
|
||||
self.client = Some(dc_client);
|
||||
println!("Actor: Connected successfully.");
|
||||
self.handle_update().await;
|
||||
let _ = responder.send(true);
|
||||
match try_connect_once() {
|
||||
Some(client) => {
|
||||
self.client = Some(client);
|
||||
self.reconnecting = false;
|
||||
self.push_presence().await;
|
||||
let _ = resp.send(true);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to Discord: {:?}. Retrying in 5s...", e);
|
||||
let tx = PRESENCE_TX.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
let (resp_tx, _) = oneshot::channel();
|
||||
let _ = tx.send(PresenceCommand::Connect(resp_tx)).await;
|
||||
});
|
||||
|
||||
if !responder.is_closed() {
|
||||
let _ = responder.send(false);
|
||||
}
|
||||
None => {
|
||||
eprintln!("Presence: initial connect failed; spawning reconnect task.");
|
||||
let _ = resp.send(false);
|
||||
self.spawn_reconnect_task();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_disconnect(&mut self, responder: oneshot::Sender<()>) {
|
||||
async fn cmd_disconnect(&mut self, resp: oneshot::Sender<()>) {
|
||||
if let Some(mut client) = self.client.take() {
|
||||
println!("Actor: Disconnecting from Discord...");
|
||||
let _ = client.clear_activity();
|
||||
let _ = client.close();
|
||||
println!("Actor: Disconnected successfully.");
|
||||
println!("Presence: disconnected.");
|
||||
}
|
||||
let _ = responder.send(());
|
||||
let _ = resp.send(());
|
||||
}
|
||||
|
||||
async fn handle_update(&mut self) {
|
||||
if let Some(client) = self.client.as_mut() {
|
||||
let mut assets = Assets::new()
|
||||
.large_image(&self.data.large_image_key)
|
||||
.large_text(&self.data.large_image_text);
|
||||
async fn push_presence(&mut self) {
|
||||
let client = match self.client.as_mut() {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if let Some(key) = &self.data.small_image_key {
|
||||
assets = assets.small_image(key);
|
||||
}
|
||||
if let Some(text) = &self.data.small_image_text {
|
||||
assets = assets.small_text(text);
|
||||
let mut assets = Assets::new()
|
||||
.large_image(&self.data.large_image_key)
|
||||
.large_text(&self.data.large_image_text);
|
||||
|
||||
if let Some(key) = &self.data.small_image_key {
|
||||
assets = assets.small_image(key);
|
||||
}
|
||||
if let Some(text) = &self.data.small_image_text {
|
||||
assets = assets.small_text(text);
|
||||
}
|
||||
|
||||
let buttons = if let Some(btn) = &self.data.dynamic_button {
|
||||
vec![
|
||||
Button::new(&btn.label, &btn.url),
|
||||
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Button::new("Download the Launcher", "https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest"),
|
||||
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
|
||||
]
|
||||
};
|
||||
|
||||
let activity = Activity::new()
|
||||
.state(&self.data.state)
|
||||
.details(&self.data.details)
|
||||
.timestamps(Timestamps::new().start(self.start_timestamp))
|
||||
.assets(assets)
|
||||
.buttons(buttons);
|
||||
|
||||
if let Err(e) = client.set_activity(activity) {
|
||||
eprintln!("Presence: set_activity failed ({:?}); will reconnect.", e);
|
||||
self.client = None;
|
||||
self.spawn_reconnect_task();
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_reconnect_task(&mut self) {
|
||||
if self.reconnecting {
|
||||
return;
|
||||
}
|
||||
self.reconnecting = true;
|
||||
|
||||
let tx = self.reconnect_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
const MAX_ATTEMPTS: u32 = 5;
|
||||
for attempt in 1..=MAX_ATTEMPTS {
|
||||
let delay = Duration::from_secs(5 * attempt as u64);
|
||||
println!(
|
||||
"Presence: reconnect attempt {}/{} in {:?}…",
|
||||
attempt, MAX_ATTEMPTS, delay
|
||||
);
|
||||
tokio::time::sleep(delay).await;
|
||||
|
||||
if let Some(client) = try_connect_once() {
|
||||
let _ = tx.send(ReconnectResult::Connected(client)).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = tx.send(ReconnectResult::Failed).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let activity = Activity::new()
|
||||
.state(&self.data.state)
|
||||
.details(&self.data.details)
|
||||
.timestamps(Timestamps::new().start(self.start_timestamp))
|
||||
.assets(assets)
|
||||
.buttons(vec![
|
||||
Button::new(
|
||||
"Download the Launcher",
|
||||
"https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest",
|
||||
),
|
||||
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
|
||||
]);
|
||||
|
||||
if let Err(e) = client.set_activity(activity) {
|
||||
eprintln!("Failed to set activity, disconnecting: {:?}", e);
|
||||
self.client = None;
|
||||
|
||||
let (tx, _) = oneshot::channel();
|
||||
self.handle_connect(tx).await;
|
||||
} else {
|
||||
println!("Actor: Presence updated successfully.");
|
||||
}
|
||||
fn try_connect_once() -> Option<DiscordIpcClient> {
|
||||
let mut client = DiscordIpcClient::new("1032772293220384808");
|
||||
match client.connect() {
|
||||
Ok(()) => {
|
||||
println!("Presence: IPC connected.");
|
||||
Some(client)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Presence: IPC connect error: {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PRESENCE_TX: Lazy<mpsc::Sender<PresenceCommand>> = Lazy::new(|| {
|
||||
let (tx, rx) = mpsc::channel(10);
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let mut actor = PresenceActor::new(rx);
|
||||
tokio::spawn(async move { actor.run().await });
|
||||
tx
|
||||
});
|
||||
|
||||
pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> = Lazy::new(|| {
|
||||
StdMutex::new(PresenceData {
|
||||
state: "Idle in Launcher...".to_string(),
|
||||
details: " ".to_string(),
|
||||
large_image_key: "ezppfarm".to_string(),
|
||||
large_image_text: "EZPPFarm".to_string(),
|
||||
small_image_key: None,
|
||||
small_image_text: None,
|
||||
})
|
||||
});
|
||||
pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> =
|
||||
Lazy::new(|| StdMutex::new(PresenceData::default()));
|
||||
|
||||
pub async fn connect() -> bool {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
@@ -201,8 +257,7 @@ pub async fn connect() -> bool {
|
||||
|
||||
pub async fn disconnect() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
// Use a timeout to ensure we don't hang the exit process if Discord is unresponsive
|
||||
if let Ok(_) = PRESENCE_TX.send(PresenceCommand::Disconnect(tx)).await {
|
||||
if PRESENCE_TX.send(PresenceCommand::Disconnect(tx)).await.is_ok() {
|
||||
let _ = tokio::time::timeout(Duration::from_secs(2), rx).await;
|
||||
}
|
||||
}
|
||||
@@ -220,30 +275,65 @@ pub async fn has_presence() -> bool {
|
||||
}
|
||||
|
||||
pub fn update_status(state: Option<&str>, details: Option<&str>, large_image_key: Option<&str>) {
|
||||
let mut data = PRESENCE_DATA.lock().unwrap();
|
||||
if let Some(s) = state {
|
||||
data.state = s.to_string();
|
||||
}
|
||||
if let Some(d) = details {
|
||||
data.details = d.to_string();
|
||||
}
|
||||
if let Some(img) = large_image_key {
|
||||
data.large_image_key = img.to_string();
|
||||
}
|
||||
let data_clone = data.clone();
|
||||
let data = {
|
||||
let mut guard = PRESENCE_DATA.lock().unwrap();
|
||||
if let Some(s) = state {
|
||||
guard.state = s.to_string();
|
||||
}
|
||||
if let Some(d) = details {
|
||||
guard.details = d.to_string();
|
||||
}
|
||||
if let Some(img) = large_image_key {
|
||||
guard.large_image_key = img.to_string();
|
||||
}
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let tx = PRESENCE_TX.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tx.send(PresenceCommand::UpdateData(data_clone)).await;
|
||||
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_button(label: &str, url: &str) {
|
||||
let data = {
|
||||
let mut guard = PRESENCE_DATA.lock().unwrap();
|
||||
guard.dynamic_button = Some(PresenceButton {
|
||||
label: label.to_string(),
|
||||
url: url.to_string(),
|
||||
});
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let tx = PRESENCE_TX.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_button() {
|
||||
let data = {
|
||||
let mut guard = PRESENCE_DATA.lock().unwrap();
|
||||
guard.dynamic_button = None;
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let tx = PRESENCE_TX.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_user(username: Option<&str>, id: Option<&str>) {
|
||||
let mut data = PRESENCE_DATA.lock().unwrap();
|
||||
data.small_image_key = id.map(|id_str| format!("https://a.ez-pp.farm/{}", id_str));
|
||||
data.small_image_text = username.map(|s| s.to_string());
|
||||
let data_clone = data.clone();
|
||||
let data = {
|
||||
let mut guard = PRESENCE_DATA.lock().unwrap();
|
||||
guard.small_image_key = id.map(|s| format!("https://a.ez-pp.farm/{}", s));
|
||||
guard.small_image_text = username.map(str::to_string);
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let tx = PRESENCE_TX.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tx.send(PresenceCommand::UpdateData(data_clone)).await;
|
||||
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "EZPPLauncher",
|
||||
"version": "3.1.5",
|
||||
"version": "3.2.0",
|
||||
"identifier": "farm.EZPPFarm.Launcher",
|
||||
"mainBinaryName": "ezpplauncher",
|
||||
"build": {
|
||||
@@ -23,7 +23,13 @@
|
||||
],
|
||||
"withGlobalTauri": true,
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": null,
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": [
|
||||
"$HOME/.ezpplauncher/themes/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@@ -36,6 +42,15 @@
|
||||
"nsis",
|
||||
"appimage"
|
||||
],
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": ["ezpplauncher-theme"],
|
||||
"name": "ezppLauncher Theme",
|
||||
"description": "ezppLauncher Theme File",
|
||||
"role": "Viewer",
|
||||
"mimeType": "application/x-ezpplauncher-theme"
|
||||
}
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"silent": true,
|
||||
@@ -44,6 +59,7 @@
|
||||
"nsis": {
|
||||
"installMode": "currentUser",
|
||||
"compression": "lzma",
|
||||
"installerIcon": "icons/icon.ico",
|
||||
"installerHooks": "./nsis/nsis-hooks.nsh"
|
||||
}
|
||||
},
|
||||
|
||||
+56
@@ -145,6 +145,62 @@
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
main[data-theme="execution_clap"] {
|
||||
--primary: #fc112b;
|
||||
--primary-foreground: #fff;
|
||||
--ring: #fc112b;
|
||||
--color-primary-50: #fef2f2;
|
||||
--color-primary-100: #fee2e2;
|
||||
--color-primary-200: #fecaca;
|
||||
--color-primary-300: #fca5a5;
|
||||
--color-primary-400: #f87171;
|
||||
--color-primary-500: #ef4444;
|
||||
--color-primary-600: #dc2626;
|
||||
--color-primary-700: #b91c1c;
|
||||
--color-primary-800: #991b1b;
|
||||
--color-primary-900: #7f1d1d;
|
||||
--color-primary-950: #450a0a;
|
||||
--bg-primary-50: #fef2f2;
|
||||
--bg-primary-100: #fee2e2;
|
||||
--bg-primary-200: #fecaca;
|
||||
--bg-primary-300: #fca5a5;
|
||||
--bg-primary-400: #f87171;
|
||||
--bg-primary-500: #ef4444;
|
||||
--bg-primary-600: #dc2626;
|
||||
--bg-primary-700: #b91c1c;
|
||||
--bg-primary-800: #991b1b;
|
||||
--bg-primary-900: #7f1d1d;
|
||||
--bg-primary-950: #450a0a;
|
||||
}
|
||||
|
||||
main[data-theme="lost_umbrella"] {
|
||||
--primary: #e5e5e5;
|
||||
--primary-foreground: #000;
|
||||
--ring: #e5e5e5;
|
||||
--color-primary-50: #0d0d0d;
|
||||
--color-primary-100: #1a1a1a;
|
||||
--color-primary-200: #333333;
|
||||
--color-primary-300: #4d4d4d;
|
||||
--color-primary-400: #666666;
|
||||
--color-primary-500: #808080;
|
||||
--color-primary-600: #999999;
|
||||
--color-primary-700: #b3b3b3;
|
||||
--color-primary-800: #cccccc;
|
||||
--color-primary-900: #e6e6e6;
|
||||
--color-primary-950: #f2f2f2;
|
||||
--bg-primary-50: #0d0d0d;
|
||||
--bg-primary-100: #1a1a1a;
|
||||
--bg-primary-200: #333333;
|
||||
--bg-primary-300: #4d4d4d;
|
||||
--bg-primary-400: #666666;
|
||||
--bg-primary-500: #808080;
|
||||
--bg-primary-600: #999999;
|
||||
--bg-primary-700: #b3b3b3;
|
||||
--bg-primary-800: #cccccc;
|
||||
--bg-primary-900: #e6e6e6;
|
||||
--bg-primary-950: #f2f2f2;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import NyanCat from '$assets/nyan-cat.gif';
|
||||
import NyanCat from '$assets/themes/nyan_cat/image.gif';
|
||||
import { reduceAnimations } from '@/userSettings';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
//TODO: maybe dynamic background images fetched from ezpp?
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="group flex items-center h-12 rounded-full bg-primary-300 disabled:bg-primary-300/70 shadow-lg shadow-primary/25 transition-all not-disabled:hover:shadow-primary/40 not-disabled:hover:brightness-110 not-disabled:active:scale-[0.98] cursor-pointer w-fit"
|
||||
class="group flex items-center h-12 rounded-full bg-primary disabled:bg-primary/70 shadow-lg shadow-primary/25 transition-all not-disabled:hover:shadow-primary/40 not-disabled:hover:brightness-110 not-disabled:active:scale-[0.98] cursor-pointer w-fit"
|
||||
disabled={prop.disabled}
|
||||
onclick={prop.onClick}
|
||||
>
|
||||
|
||||
@@ -74,12 +74,18 @@
|
||||
|
||||
<div class="wrapper" bind:this={wrapper}>
|
||||
<!-- Scrollable content -->
|
||||
<div class={`content ${className}`} bind:this={container} onscroll={updateScrollbar}>
|
||||
<div
|
||||
class={`content ${className}`}
|
||||
bind:this={container}
|
||||
onscroll={updateScrollbar}
|
||||
id="scrollable-content"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<!-- Custom scrollbar -->
|
||||
<div class="scrollbar" style="top: {topOffset}px;">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="thumb"
|
||||
bind:this={thumb}
|
||||
@@ -87,6 +93,7 @@
|
||||
onpointermove={drag}
|
||||
onpointerup={endDrag}
|
||||
onpointercancel={endDrag}
|
||||
aria-controls="scrollable-content"
|
||||
style="height:{thumbHeight}px; transform:translateY({thumbTop}px);"
|
||||
></div>
|
||||
</div>
|
||||
@@ -117,8 +124,8 @@
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
background: var(--color-theme-950);
|
||||
width: 8px;
|
||||
background: transparent;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@@ -126,7 +133,7 @@
|
||||
.thumb {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: var(--color-theme-800);
|
||||
background: var(--color-theme-700);
|
||||
border-radius: 999px;
|
||||
cursor: grab;
|
||||
transition: background 0.15s;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
bind:ref
|
||||
{value}
|
||||
class={cn(
|
||||
'data-[highlighted]:bg-primary-300 data-[highlighted]:text-primary-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
import Minimize from 'lucide-svelte/icons/minus';
|
||||
import Close from 'lucide-svelte/icons/x';
|
||||
|
||||
import Logo from '$assets/logo.png';
|
||||
import Volume2 from 'lucide-svelte/icons/volume-2';
|
||||
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { onMount } from 'svelte';
|
||||
import { launcherVersion } from '@/global';
|
||||
import Badge from '../badge/badge.svelte';
|
||||
import { reduceAnimations } from '@/userSettings';
|
||||
import { custom_theme_audio_playing, custom_theme_volume } from '@/global';
|
||||
import { reduceAnimations, userSettings } from '@/userSettings';
|
||||
import { Volume, Volume1, VolumeOff } from 'lucide-svelte';
|
||||
|
||||
onMount(() => {
|
||||
const appWindow = getCurrentWindow();
|
||||
@@ -21,14 +20,67 @@
|
||||
</script>
|
||||
|
||||
<div data-tauri-drag-region class="titlebar z-[100]">
|
||||
{#if $custom_theme_audio_playing}
|
||||
{#if $custom_theme_audio_playing}
|
||||
<div class="relative flex items-center justify-center group">
|
||||
<div class="relative flex items-center justify-center p-1 cursor-pointer">
|
||||
<div
|
||||
class="absolute inset-0 bg-background rounded
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}"
|
||||
></div>
|
||||
|
||||
{#if $custom_theme_volume > 0.7}
|
||||
<Volume2 class="z-10 pointer-events-none" size={14} />
|
||||
{:else if $custom_theme_volume >= 0.15}
|
||||
<Volume1 class="z-10 pointer-events-none" size={14} />
|
||||
{:else if $custom_theme_volume > 0}
|
||||
<Volume class="z-10 pointer-events-none" size={14} />
|
||||
{:else}
|
||||
<VolumeOff class="z-10 pointer-events-none" size={14} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute top-full right-0 translate-x-[7px]
|
||||
opacity-0 invisible pointer-events-none -translate-y-2
|
||||
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto group-hover:translate-y-0
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
z-50"
|
||||
>
|
||||
<div class="h-2 w-full"></div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-center h-[120px] w-[35px]
|
||||
bg-black/20 backdrop-blur-sm border border-white/10 rounded-xl"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
bind:value={$custom_theme_volume}
|
||||
on:input={() => {
|
||||
$userSettings.value('volume').set($custom_theme_volume);
|
||||
$userSettings.save();
|
||||
}}
|
||||
class="volume-slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="relative flex items-center justify-center p-1 group" id="titlebar-minimize">
|
||||
<div
|
||||
class="absolute inset-0 bg-background rounded
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
origin-center"
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
origin-center"
|
||||
></div>
|
||||
|
||||
<Minimize class="z-10 pointer-events-none" size={14} />
|
||||
@@ -36,11 +88,11 @@
|
||||
<div class="relative flex items-center justify-center p-1 group" id="titlebar-close">
|
||||
<div
|
||||
class="absolute inset-0 bg-red-500 rounded
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
origin-center"
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
origin-center"
|
||||
></div>
|
||||
|
||||
<Close class="z-10 pointer-events-none" size={14} />
|
||||
@@ -63,4 +115,52 @@
|
||||
padding-right: 5px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
width: 100px; /* becomes vertical */
|
||||
height: 6px;
|
||||
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* track */
|
||||
.volume-slider::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* thumb */
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
|
||||
margin-top: -4px; /* center thumb */
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
.volume-slider::-moz-range-track {
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.volume-slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
type TauriDragDropEvent = {
|
||||
paths: string[];
|
||||
position: { x: number; y: number };
|
||||
};
|
||||
|
||||
export const useDropZone = ({ onDrop }: { onDrop: (paths: string[]) => void }) => {
|
||||
let element = $state<HTMLDivElement | null>(null);
|
||||
let dragIn = $state(false);
|
||||
let isDraggingOverApp = $state(false);
|
||||
|
||||
const isOver = (x: number, y: number) => {
|
||||
if (!element) return false;
|
||||
const el = document.elementFromPoint(x, y);
|
||||
return element === el || element.contains(el); // ← fix
|
||||
};
|
||||
|
||||
const dropUnlisten = listen<TauriDragDropEvent>('tauri://drag-drop', (e) => {
|
||||
const { x, y } = e.payload.position;
|
||||
if (isOver(x, y)) {
|
||||
onDrop(e.payload.paths);
|
||||
}
|
||||
dragIn = false;
|
||||
isDraggingOverApp = false;
|
||||
});
|
||||
|
||||
const overUnlisten = listen<TauriDragDropEvent>('tauri://drag-over', (e) => {
|
||||
const { x, y } = e.payload.position;
|
||||
isDraggingOverApp = true;
|
||||
dragIn = isOver(x, y);
|
||||
});
|
||||
|
||||
const leaveUnlisten = listen('tauri://drag-leave', () => {
|
||||
dragIn = false;
|
||||
isDraggingOverApp = false;
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
(await dropUnlisten)();
|
||||
(await overUnlisten)();
|
||||
(await leaveUnlisten)();
|
||||
});
|
||||
|
||||
return {
|
||||
get ref() {
|
||||
return element;
|
||||
},
|
||||
set ref(el: HTMLDivElement | null) {
|
||||
element = el;
|
||||
},
|
||||
get dragIn() {
|
||||
return dragIn;
|
||||
},
|
||||
get isDraggingOverApp() {
|
||||
return isDraggingOverApp;
|
||||
},
|
||||
};
|
||||
};
|
||||
+13
-2
@@ -3,7 +3,9 @@ import { ezppfarm } from './api/ezpp';
|
||||
import type { Component } from 'svelte';
|
||||
import Loading from '../screens/Loading.svelte';
|
||||
import type { Release } from './types';
|
||||
|
||||
import type { Theme } from './themes';
|
||||
import { browser } from '$app/environment';
|
||||
import { setGlobalVolume } from './utils';
|
||||
export const currentView = writable<Component>(Loading);
|
||||
|
||||
export const platform = writable<string>('');
|
||||
@@ -24,7 +26,16 @@ export const currentLoadingInfo = writable<string>('Initializing...');
|
||||
export const firstStartup = writable<boolean>(false);
|
||||
|
||||
export const launching = writable<boolean>(false);
|
||||
export const nyanCatSong = writable<HTMLAudioElement | undefined>(undefined);
|
||||
export const custom_themes = writable<Theme[]>([]);
|
||||
export const custom_theme_container = writable<HTMLElement | undefined>(undefined);
|
||||
export const active_custom_theme = writable<Theme | undefined>(undefined);
|
||||
export const custom_theme_audio_playing = writable<boolean>(false);
|
||||
export const custom_theme_volume = writable<number>(0.15);
|
||||
custom_theme_volume.subscribe((val) => {
|
||||
if (browser) {
|
||||
setGlobalVolume(val);
|
||||
}
|
||||
});
|
||||
|
||||
export const serverPing = writable<number | undefined>(undefined);
|
||||
export const serverConnectionFails = writable(0);
|
||||
|
||||
+23
-38
@@ -17,20 +17,14 @@ export function normalizeOsuName(name: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
export const getHWID = async () => {
|
||||
const hwid = await invoke('get_hwid');
|
||||
return typeof hwid === 'string' ? hwid : undefined;
|
||||
};
|
||||
export const getHWID = async (): Promise<string | undefined> =>
|
||||
await invoke<string | undefined>('get_hwid');
|
||||
|
||||
export const isValidOsuFolder = async (folder: string): Promise<boolean> => {
|
||||
const result = await invoke('valid_osu_folder', { folder });
|
||||
return typeof result === 'boolean' ? result : false;
|
||||
};
|
||||
export const isValidOsuFolder = async (folder: string): Promise<boolean | undefined> =>
|
||||
await invoke<boolean | undefined>('valid_osu_folder', { folder });
|
||||
|
||||
export const autoDetectOsuInstallFolder = async () => {
|
||||
const result = await invoke('find_osu_installation');
|
||||
return typeof result === 'string' ? result : undefined;
|
||||
};
|
||||
export const autoDetectOsuInstallFolder = async () =>
|
||||
await invoke<string | undefined>('find_osu_installation');
|
||||
|
||||
export const setUserConfigValues = async (
|
||||
osuFolderPath: string,
|
||||
@@ -50,29 +44,23 @@ export const setConfigValues = async (
|
||||
entries,
|
||||
});
|
||||
|
||||
export const getReleaseStream = async (folder: string) => {
|
||||
const result = await invoke('get_osu_release_stream', { folder });
|
||||
return typeof result === 'string' ? result : undefined;
|
||||
};
|
||||
export const getReleaseStream = async (folder: string): Promise<string | undefined> =>
|
||||
await invoke<string | undefined>('get_osu_release_stream', { folder });
|
||||
|
||||
export const getVersion = async (folder: string) => {
|
||||
const result = await invoke('get_osu_version', { folder });
|
||||
return typeof result === 'string' ? result : undefined;
|
||||
};
|
||||
export const getVersion = async (folder: string): Promise<string | undefined> =>
|
||||
await invoke<string | undefined>('get_osu_version', { folder });
|
||||
|
||||
export const getBeatmapSetsCount = async (folder: string) => {
|
||||
const result = await invoke('get_beatmapsets_count', {
|
||||
export const getBeatmapSetsCount = async (folder: string): Promise<number | undefined> =>
|
||||
await invoke<number | undefined>('get_beatmapsets_count', {
|
||||
folder,
|
||||
});
|
||||
return typeof result === 'number' ? result : 0;
|
||||
};
|
||||
|
||||
export const getSkin = async (folder: string) => {
|
||||
const result = await invoke('get_osu_skin', {
|
||||
const result = await invoke<string | undefined>('get_osu_skin', {
|
||||
folder,
|
||||
});
|
||||
|
||||
return typeof result === 'string' ? result : 'Default';
|
||||
return result || 'Default';
|
||||
};
|
||||
|
||||
export const runUpdater = async (folder: string) => await invoke('run_osu_updater', { folder });
|
||||
@@ -113,8 +101,8 @@ export const downloadEZPPLauncherUpdateFiles = async (
|
||||
allFiles: UpdateFile[],
|
||||
progressCallback: (file: UpdateStatus) => void
|
||||
) => {
|
||||
const downloadStatusListen = await listen('download-progress', (event) =>
|
||||
progressCallback(event.payload as UpdateStatus)
|
||||
const downloadStatusListen = await listen<UpdateStatus>('download-progress', (event) =>
|
||||
progressCallback(event.payload)
|
||||
);
|
||||
try {
|
||||
await invoke('download_ezpp_launcher_update_files', { folder, updateFiles, allFiles });
|
||||
@@ -127,20 +115,17 @@ export const replaceUIFiles = async (folder: string, revert: boolean) => {
|
||||
await invoke('replace_ui_files', { folder, revert });
|
||||
};
|
||||
|
||||
export const isOsuRunning = async () => {
|
||||
const result = await invoke('is_osu_running');
|
||||
return typeof result === 'boolean' ? result : false;
|
||||
};
|
||||
export const isOsuRunning = async (): Promise<boolean> => await invoke<boolean>('is_osu_running');
|
||||
|
||||
export const getLauncherVersion = async () => await invoke<string>('get_launcher_version');
|
||||
export const exit = async () => await invoke('exit');
|
||||
export const getPlatform = async () => await invoke<string>('get_platform');
|
||||
export const isOsuCorrupted = async (folder: string) =>
|
||||
export const getPlatform = async (): Promise<string> => await invoke<string>('get_platform');
|
||||
export const isOsuCorrupted = async (folder: string): Promise<boolean> =>
|
||||
await invoke<boolean>('check_for_corruption', { folder });
|
||||
export const hasWMCTRL = async () => await invoke<boolean>('has_wmctrl');
|
||||
export const hasOsuWinello = async () => await invoke<boolean>('has_osuwinello');
|
||||
export const hasNet8 = async () => await invoke<boolean>('has_net8');
|
||||
export const encryptString = async (str: string, entropy: string) =>
|
||||
export const hasWMCTRL = async (): Promise<boolean> => await invoke<boolean>('has_wmctrl');
|
||||
export const hasOsuWinello = async (): Promise<boolean> => await invoke<boolean>('has_osuwinello');
|
||||
export const hasNet8 = async (): Promise<boolean> => await invoke<boolean>('has_net8');
|
||||
export const encryptString = async (str: string, entropy: string): Promise<string> =>
|
||||
await invoke<string>('encrypt_string', { string: str, entropy });
|
||||
export const downloadUpdate = async (
|
||||
url: string,
|
||||
|
||||
+3
-1
@@ -7,7 +7,9 @@ const osuSkinsCollator = new Intl.Collator(undefined, {
|
||||
sensitivity: 'base',
|
||||
});
|
||||
|
||||
export const sortSkins = (skins: { name: string, author: string | undefined, modified: number}[]) =>
|
||||
export const sortSkins = (
|
||||
skins: { name: string; author: string | undefined; modified: number }[]
|
||||
) =>
|
||||
skins.sort((a, b) => {
|
||||
const pa = priority[a.name[0]];
|
||||
const pb = priority[b.name[0]];
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import * as fs from '@tauri-apps/plugin-fs';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { active_custom_theme, custom_themes } from './global';
|
||||
import { calculateGitBlobSha, setGlobalVolume } from './utils';
|
||||
import { betterFetch } from '@better-fetch/fetch';
|
||||
import zip from 'jszip';
|
||||
import { SemVer } from 'semver';
|
||||
|
||||
export type Theme = {
|
||||
folder_name: string;
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
scriptUrl: string;
|
||||
assets: string;
|
||||
preview: string;
|
||||
status: 'installed' | 'downloading' | 'extracting' | 'deleting' | 'not-installed';
|
||||
progress: number;
|
||||
updateAvailable: boolean;
|
||||
};
|
||||
|
||||
export type ThemeInfo = {
|
||||
name: string;
|
||||
version: string;
|
||||
apiVersion: string;
|
||||
author: string;
|
||||
entry: string;
|
||||
style: string;
|
||||
preview: string;
|
||||
};
|
||||
|
||||
export const getDownloadableThemes = async () => {
|
||||
const themes: Theme[] = [];
|
||||
|
||||
const downloadableThemes = await betterFetch<
|
||||
{
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
}[]
|
||||
>('https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes.json', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
if (downloadableThemes.data) {
|
||||
for (const theme of downloadableThemes.data) {
|
||||
themes.push({
|
||||
folder_name: `${toSafeName(theme.name)}-${generateRandomString(16)}`,
|
||||
name: theme.name,
|
||||
version: theme.version,
|
||||
author: theme.author,
|
||||
scriptUrl: '',
|
||||
assets: '',
|
||||
preview: `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/${toSafeName(theme.name)}.png`,
|
||||
status: 'not-installed',
|
||||
progress: 0,
|
||||
updateAvailable: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch downloadable themes');
|
||||
console.error(downloadableThemes.error);
|
||||
}
|
||||
return themes;
|
||||
};
|
||||
|
||||
export const getThemes = async (): Promise<Theme[]> => {
|
||||
const themes: Theme[] = [];
|
||||
themes.push({
|
||||
folder_name: 'default',
|
||||
author: 'EZPPFarm',
|
||||
name: 'Default',
|
||||
version: '1.0.0',
|
||||
scriptUrl: '',
|
||||
assets: '',
|
||||
preview: '',
|
||||
status: 'installed',
|
||||
progress: 0,
|
||||
updateAvailable: false,
|
||||
});
|
||||
const folderPath = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
|
||||
if (await fs.exists(folderPath)) {
|
||||
const themeFolders = await fs.readDir(folderPath);
|
||||
for (const themeFolder of themeFolders) {
|
||||
const themeFolderPath = await path.join(folderPath, themeFolder.name);
|
||||
const themeConfigFilePath = await path.join(folderPath, themeFolder.name, 'theme.json');
|
||||
if (await fs.exists(themeConfigFilePath)) {
|
||||
const themeConfig = await fs.readTextFile(themeConfigFilePath);
|
||||
const theme = JSON.parse(themeConfig) as ThemeInfo;
|
||||
const scriptFile = await path.join(folderPath, themeFolder.name, theme.entry);
|
||||
if (await fs.exists(scriptFile)) {
|
||||
themes.push({
|
||||
folder_name: themeFolder.name,
|
||||
name: theme.name,
|
||||
version: theme.version,
|
||||
author: theme.author,
|
||||
scriptUrl: convertFileSrc(await path.normalize(`${themeFolderPath}/${theme.entry}`)),
|
||||
assets: convertFileSrc(await path.normalize(`${themeFolderPath}/assets`)),
|
||||
preview: convertFileSrc(
|
||||
await path.normalize(`${themeFolderPath}/assets/${theme.preview}`)
|
||||
),
|
||||
status: 'installed',
|
||||
progress: 0,
|
||||
updateAvailable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return themes;
|
||||
};
|
||||
|
||||
function combineChunks(chunks: Uint8Array[], totalLength: number): Uint8Array {
|
||||
const result = new Uint8Array(totalLength);
|
||||
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const downloadTheme = async (
|
||||
theme: Theme,
|
||||
force = false
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
if (theme.status !== 'not-installed' && !force) return { success: false };
|
||||
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'downloading';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
const themeFileName = `${toSafeName(theme.name)}.ezpplauncher-theme`;
|
||||
const themeContents = await betterFetch<{
|
||||
file_contents: {
|
||||
sha: string;
|
||||
download_url: string;
|
||||
};
|
||||
}>(
|
||||
`https://git.ez-pp.farm/api/v1/repos/EZPPFarm/EZPPLauncher-Themes/contents-ext/themes/${themeFileName}?ref=main`
|
||||
);
|
||||
if (themeContents.error) {
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'not-installed';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: themeContents.error.message ?? 'An unknown error occurred',
|
||||
};
|
||||
}
|
||||
const downloadUrl = `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${themeFileName}`;
|
||||
const downloadReq = await fetch(downloadUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
if (!downloadReq.ok || downloadReq.body === null) {
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'not-installed';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to download theme file.',
|
||||
};
|
||||
}
|
||||
const total = Number(downloadReq.headers.get('content-length')) || 0;
|
||||
const reader = downloadReq.body.getReader();
|
||||
let received = 0;
|
||||
const chunks = [];
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
received += value.length;
|
||||
const progress = received / total;
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.progress = progress;
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
}
|
||||
|
||||
const fileBuffer = combineChunks(chunks, total);
|
||||
|
||||
const sha = calculateGitBlobSha(Buffer.from(fileBuffer));
|
||||
if (sha !== themeContents.data.file_contents.sha) {
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'not-installed';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to verify downloaded theme file hash.',
|
||||
};
|
||||
}
|
||||
|
||||
//TODO: check file hash
|
||||
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'extracting';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
const baseThemeFolder = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
|
||||
const themeFolder = await path.join(baseThemeFolder, toSafeName(theme.name));
|
||||
if (!(await fs.exists(baseThemeFolder))) await fs.mkdir(baseThemeFolder, { recursive: true });
|
||||
if (!(await fs.exists(themeFolder))) await fs.mkdir(themeFolder, { recursive: true });
|
||||
|
||||
const zipFile = await zip.loadAsync(fileBuffer);
|
||||
const totalFiles = Object.keys(zipFile.files).length;
|
||||
let extractedFiles = 0;
|
||||
for (const zipEntry of Object.keys(zipFile.files)) {
|
||||
const file = zipFile.file(zipEntry);
|
||||
if (file) {
|
||||
try {
|
||||
const fileData = await file.async('uint8array');
|
||||
const filePath = await path.join(themeFolder, zipEntry);
|
||||
const dirPath = await path.dirname(filePath);
|
||||
if (!(await fs.exists(dirPath))) await fs.mkdir(dirPath, { recursive: true });
|
||||
await new Promise((res) => setTimeout(res, 250));
|
||||
await fs.writeFile(filePath, fileData);
|
||||
extractedFiles++;
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.progress = extractedFiles / totalFiles;
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'not-installed';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to extract theme file.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const themeInfo = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
|
||||
const themeInfoObj = JSON.parse(themeInfo) as ThemeInfo;
|
||||
const themeScriptUrl = convertFileSrc(
|
||||
await path.normalize(`${themeFolder}/${themeInfoObj.entry}`)
|
||||
);
|
||||
const themeAssets = convertFileSrc(await path.normalize(`${themeFolder}/assets`));
|
||||
const themePreview = convertFileSrc(
|
||||
await path.normalize(`${themeFolder}/assets/${themeInfoObj.preview}`)
|
||||
);
|
||||
|
||||
const downloadableThemes = await getDownloadableThemes();
|
||||
custom_themes.update((themes) => {
|
||||
const reloadedThemes = [...themes];
|
||||
|
||||
for (const theme of downloadableThemes) {
|
||||
if (theme.name === 'Default') continue; // never overwrite Default from remote
|
||||
|
||||
if (!reloadedThemes.find((t) => t.name === theme.name)) {
|
||||
reloadedThemes.push(theme);
|
||||
}
|
||||
|
||||
const installedTheme = reloadedThemes.find((t) => t.name === theme.name);
|
||||
if (installedTheme) {
|
||||
const installedThemeVersion = new SemVer(installedTheme.version);
|
||||
const downloadableThemeVersion = new SemVer(theme.version);
|
||||
if (downloadableThemeVersion.compare(installedThemeVersion) > 0) {
|
||||
installedTheme.updateAvailable = true;
|
||||
const index = reloadedThemes.findIndex((t) => t.name === theme.name);
|
||||
reloadedThemes[index] = installedTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const installedTheme of reloadedThemes) {
|
||||
if (installedTheme.name === 'Default') continue; // never touch Default
|
||||
|
||||
if (installedTheme.name === theme.name) {
|
||||
installedTheme.name = themeInfoObj.name;
|
||||
installedTheme.author = themeInfoObj.author;
|
||||
installedTheme.version = themeInfoObj.version;
|
||||
installedTheme.scriptUrl = themeScriptUrl;
|
||||
installedTheme.assets = themeAssets;
|
||||
installedTheme.preview = themePreview;
|
||||
installedTheme.status = 'installed';
|
||||
}
|
||||
}
|
||||
|
||||
return reloadedThemes.sort((a, b) => {
|
||||
if (a.name === 'Default') return -1;
|
||||
if (b.name === 'Default') return 1;
|
||||
if (a.status === 'installed' && b.status !== 'installed') return -1;
|
||||
if (a.status !== 'installed' && b.status === 'installed') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volume = 0.15) => {
|
||||
active_custom_theme.set(theme);
|
||||
themeContainer.innerHTML = '';
|
||||
if (theme.scriptUrl.length > 0) {
|
||||
/* @vite-ignore */
|
||||
const themeScript = await import(theme.scriptUrl);
|
||||
themeScript.mountTheme(themeContainer, { assets: theme.assets });
|
||||
}
|
||||
setGlobalVolume(volume);
|
||||
};
|
||||
|
||||
export const deleteTheme = async (themeToUninstall: Theme) => {
|
||||
if (themeToUninstall.status !== 'installed') return false;
|
||||
const baseThemeFolder = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
|
||||
if (!(await fs.exists(baseThemeFolder))) return false;
|
||||
const themeFolder = await path.join(baseThemeFolder, themeToUninstall.folder_name);
|
||||
if (!(await fs.exists(themeFolder))) return false;
|
||||
custom_themes.update((themes) => {
|
||||
const matchedTheme = themes.find((t) => t.name === themeToUninstall.name);
|
||||
if (matchedTheme) {
|
||||
matchedTheme.status = 'deleting';
|
||||
}
|
||||
return themes;
|
||||
});
|
||||
await fs.remove(themeFolder, { recursive: true });
|
||||
const downloadableThemes = await getDownloadableThemes();
|
||||
custom_themes.update((themes) => {
|
||||
const reloadedThemes = [...themes];
|
||||
for (const theme of downloadableThemes) {
|
||||
if (theme.name === themeToUninstall.name) {
|
||||
const themeIndex = reloadedThemes.findIndex((t) => t.name === theme.name);
|
||||
if (themeIndex) {
|
||||
reloadedThemes.splice(themeIndex, 1);
|
||||
reloadedThemes.push(theme);
|
||||
}
|
||||
}
|
||||
|
||||
const installedTheme = reloadedThemes.find((t) => t.name === theme.name);
|
||||
if (installedTheme) {
|
||||
const installedThemeVersion = new SemVer(installedTheme.version);
|
||||
const downloadableThemeVersion = new SemVer(theme.version);
|
||||
if (downloadableThemeVersion.compare(installedThemeVersion) > 0) {
|
||||
installedTheme.updateAvailable = true;
|
||||
const index = reloadedThemes.findIndex((t) => t.name === theme.name);
|
||||
reloadedThemes[index] = installedTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reloadedThemes.sort((a, b) => {
|
||||
if (a.name === 'Default') return -1;
|
||||
if (b.name === 'Default') return 1;
|
||||
if (a.status === 'installed' && b.status !== 'installed') return -1;
|
||||
if (a.status !== 'installed' && b.status === 'installed') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
export const checkThemeFromFile = async (filePath: string) => {
|
||||
const normalizedFilePath = await path.normalize(filePath);
|
||||
if (!(await fs.exists(normalizedFilePath))) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'File not found',
|
||||
};
|
||||
}
|
||||
const fileBuffer = await fs.readFile(normalizedFilePath);
|
||||
|
||||
let themeInfo: ThemeInfo | undefined = undefined;
|
||||
|
||||
try {
|
||||
const zipFile = await zip.loadAsync(fileBuffer);
|
||||
|
||||
const themeConfig = zipFile.file('theme.json');
|
||||
if (!themeConfig) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Theme config file not found',
|
||||
};
|
||||
}
|
||||
const themeConfigData = await themeConfig.async('string');
|
||||
themeInfo = JSON.parse(themeConfigData) as {
|
||||
name: string;
|
||||
version: string;
|
||||
apiVersion: string;
|
||||
author: string;
|
||||
entry: string;
|
||||
style: string;
|
||||
preview: string;
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not read theme file',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: !!themeInfo,
|
||||
themeInfo,
|
||||
};
|
||||
};
|
||||
|
||||
export const importThemeFromFile = async (themeName: string, filePath: string) => {
|
||||
const baseThemeFolder = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
|
||||
const themeFolder = await path.join(baseThemeFolder, toSafeName(themeName));
|
||||
if (!(await fs.exists(baseThemeFolder))) await fs.mkdir(baseThemeFolder, { recursive: true });
|
||||
if (!(await fs.exists(themeFolder))) await fs.mkdir(themeFolder, { recursive: true });
|
||||
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
const zipFile = await zip.loadAsync(fileBuffer);
|
||||
for (const zipEntry of Object.keys(zipFile.files)) {
|
||||
const file = zipFile.file(zipEntry);
|
||||
if (file) {
|
||||
try {
|
||||
const fileData = await file.async('uint8array');
|
||||
const filePath = await path.join(themeFolder, zipEntry);
|
||||
const dirPath = await path.dirname(filePath);
|
||||
if (!(await fs.exists(dirPath))) await fs.mkdir(dirPath, { recursive: true });
|
||||
await new Promise((res) => setTimeout(res, 250));
|
||||
await fs.writeFile(filePath, fileData);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to extract theme file.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const themeInfo = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
|
||||
const themeInfoObj = JSON.parse(themeInfo) as ThemeInfo;
|
||||
const themeScriptUrl = convertFileSrc(
|
||||
await path.normalize(`${themeFolder}/${themeInfoObj.entry}`)
|
||||
);
|
||||
const themeAssets = convertFileSrc(await path.normalize(`${themeFolder}/assets`));
|
||||
const themePreview = convertFileSrc(
|
||||
await path.normalize(`${themeFolder}/assets/${themeInfoObj.preview}`)
|
||||
);
|
||||
|
||||
const downloadableThemes = await getDownloadableThemes();
|
||||
custom_themes.update((themes) => {
|
||||
const reloadedThemes = [...themes];
|
||||
|
||||
for (const theme of downloadableThemes) {
|
||||
if (theme.name === 'Default') continue; // never overwrite Default from remote
|
||||
|
||||
if (!reloadedThemes.find((t) => t.name === theme.name)) {
|
||||
reloadedThemes.push(theme);
|
||||
}
|
||||
|
||||
const installedTheme = reloadedThemes.find((t) => t.name === theme.name);
|
||||
if (installedTheme) {
|
||||
const installedThemeVersion = new SemVer(installedTheme.version);
|
||||
const downloadableThemeVersion = new SemVer(theme.version);
|
||||
if (downloadableThemeVersion.compare(installedThemeVersion) > 0) {
|
||||
installedTheme.updateAvailable = true;
|
||||
const index = reloadedThemes.findIndex((t) => t.name === theme.name);
|
||||
reloadedThemes[index] = installedTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const installedTheme of reloadedThemes) {
|
||||
if (installedTheme.name === 'Default') continue; // never touch Default
|
||||
|
||||
if (installedTheme.name === themeName) {
|
||||
installedTheme.name = themeInfoObj.name;
|
||||
installedTheme.author = themeInfoObj.author;
|
||||
installedTheme.version = themeInfoObj.version;
|
||||
installedTheme.scriptUrl = themeScriptUrl;
|
||||
installedTheme.assets = themeAssets;
|
||||
installedTheme.preview = themePreview;
|
||||
installedTheme.status = 'installed';
|
||||
}
|
||||
}
|
||||
|
||||
return reloadedThemes.sort((a, b) => {
|
||||
if (a.name === 'Default') return -1;
|
||||
if (b.name === 'Default') return 1;
|
||||
if (a.status === 'installed' && b.status !== 'installed') return -1;
|
||||
if (a.status !== 'installed' && b.status === 'installed') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
export const toSafeName = (name: string) => {
|
||||
return (
|
||||
name
|
||||
.toLowerCase()
|
||||
.replace(/[<>:"/\\|?*]/g, '_')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/[\x00-\x1F\x7F]/g, '_')
|
||||
.replace(/[\s-]+/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/[. ]+$/g, '_')
|
||||
);
|
||||
};
|
||||
|
||||
const generateRandomString = (length: number) => {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
return Array.from({ length }, () =>
|
||||
characters.charAt(Math.floor(Math.random() * characters.length))
|
||||
).join('');
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
declare global {
|
||||
const umami: Umami;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link https://umami.is/docs/tracker-functions|Umami Docs}
|
||||
*/
|
||||
interface Umami {
|
||||
track(): Promise<string> | undefined;
|
||||
track(event_name: string, event_data?: Record<string, unknown>): Promise<string> | undefined;
|
||||
track(custom_payload: { website: string; [key: string]: unknown }): Promise<string> | undefined;
|
||||
track(
|
||||
callback: (props: {
|
||||
hostname: string;
|
||||
language: string;
|
||||
referrer: string;
|
||||
screen: string;
|
||||
title: string;
|
||||
url: string;
|
||||
website: string;
|
||||
}) => { website: string; [key: string]: unknown }
|
||||
): Promise<string> | undefined;
|
||||
|
||||
/** Pass in your own ID to identify a user. */
|
||||
identify(identity_id: string): Promise<void>;
|
||||
|
||||
/** Save data about the current session. */
|
||||
identify(identity_id: string, data: Record<string, unknown>): Promise<void>;
|
||||
|
||||
/** To save data without a unique ID, pass in only a JSON object. */
|
||||
identify(data: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,6 +1,8 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { custom_theme_audio_playing } from './global';
|
||||
import * as crypto from 'crypto-js';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -99,3 +101,111 @@ export const urlIsValidImage = async (url: string) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export function toURL(data: Buffer, type: string) {
|
||||
return URL.createObjectURL(new Blob([new Uint8Array(data)], { type }));
|
||||
}
|
||||
|
||||
export function setGlobalVolume(volume: number) {
|
||||
const mediaElements: NodeListOf<HTMLMediaElement> = document.querySelectorAll('audio, video');
|
||||
mediaElements.forEach((element) => {
|
||||
element.volume = volume;
|
||||
});
|
||||
}
|
||||
|
||||
export function fadeGlobalVolume(
|
||||
volumeFrom: number,
|
||||
volumeTo: number,
|
||||
duration: number,
|
||||
onDone?: () => void
|
||||
) {
|
||||
const mediaElements: NodeListOf<HTMLMediaElement> = document.querySelectorAll('audio, video');
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
function animate() {
|
||||
const currentTime = performance.now();
|
||||
const elapsedTime = currentTime - startTime;
|
||||
const progress = Math.min(elapsedTime / duration, 1);
|
||||
|
||||
const interpolatedVolume = volumeFrom + (volumeTo - volumeFrom) * progress;
|
||||
|
||||
mediaElements.forEach((element) => {
|
||||
element.volume = interpolatedVolume;
|
||||
});
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
if (onDone) onDone();
|
||||
}
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
export const pauseGlobalMedia = () => {
|
||||
const mediaElements: NodeListOf<HTMLMediaElement> = document.querySelectorAll('audio, video');
|
||||
mediaElements.forEach((element) => element.pause());
|
||||
};
|
||||
|
||||
export const resumeGlobalMedia = () => {
|
||||
const mediaElements: NodeListOf<HTMLMediaElement> = document.querySelectorAll('audio, video');
|
||||
mediaElements.forEach((element) => element.play());
|
||||
};
|
||||
|
||||
export const setCurrentTimeGlobalMedia = (time: number) => {
|
||||
const mediaElements: NodeListOf<HTMLMediaElement> = document.querySelectorAll('audio, video');
|
||||
mediaElements.forEach((element) => (element.currentTime = time));
|
||||
};
|
||||
|
||||
export function trackMediaContainer(node: HTMLElement) {
|
||||
const media = new Set<HTMLMediaElement>();
|
||||
|
||||
function update() {
|
||||
custom_theme_audio_playing.set(Array.from(media).some((el) => !el.paused));
|
||||
}
|
||||
|
||||
function register(el: HTMLMediaElement) {
|
||||
media.add(el);
|
||||
|
||||
const handler = () => update();
|
||||
el.addEventListener('play', handler);
|
||||
el.addEventListener('pause', handler);
|
||||
|
||||
return () => {
|
||||
media.delete(el);
|
||||
el.removeEventListener('play', handler);
|
||||
el.removeEventListener('pause', handler);
|
||||
};
|
||||
}
|
||||
|
||||
// find existing media inside the div
|
||||
node.querySelectorAll<HTMLMediaElement>('video, audio').forEach(register);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
m.addedNodes.forEach((n) => {
|
||||
if (n instanceof HTMLMediaElement) register(n);
|
||||
if (n instanceof HTMLElement) {
|
||||
n.querySelectorAll<HTMLMediaElement>('video, audio').forEach(register);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(node, { childList: true, subtree: true });
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateGitBlobSha(fileBuffer: Buffer): string {
|
||||
const header = Buffer.from(`blob ${fileBuffer.length}\0`);
|
||||
const store = Buffer.concat([header, fileBuffer]);
|
||||
|
||||
return crypto.SHA1(crypto.lib.WordArray.create(store)).toString(crypto.enc.Hex);
|
||||
}
|
||||
|
||||
+57
-124
@@ -1,16 +1,18 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$assets/logo.png';
|
||||
import NyanCatSong from '$assets/nyan-cat.mp3';
|
||||
import '../app.css';
|
||||
|
||||
import Titlebar from '@/components/ui/titlebar/titlebar.svelte';
|
||||
import * as AlertDialog from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
active_custom_theme,
|
||||
currentLoadingInfo,
|
||||
custom_theme_container,
|
||||
custom_theme_volume,
|
||||
custom_themes,
|
||||
discordPresence,
|
||||
firstStartup,
|
||||
launcherVersion,
|
||||
nyanCatSong,
|
||||
platform,
|
||||
presenceLoading,
|
||||
setupValues,
|
||||
@@ -36,14 +38,15 @@
|
||||
|
||||
import '@fontsource/sora';
|
||||
import '@fontsource/space-mono';
|
||||
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
|
||||
import NyanCatBg from '@/components/ui/animated-bg/NyanCatBg.svelte';
|
||||
import { getDownloadableThemes, getThemes, loadTheme } from '@/themes';
|
||||
import { sileo } from 'sileo';
|
||||
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
|
||||
import { SemVer } from 'semver';
|
||||
import { trackMediaContainer } from '@/utils';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let unsupported_platform = $state<boolean>(false);
|
||||
let secret_enabled = $state<boolean>(false);
|
||||
|
||||
function disableReload() {
|
||||
if (window.location.hostname !== 'tauri.localhost') {
|
||||
@@ -93,19 +96,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
let konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
window.Buffer = Buffer;
|
||||
|
||||
@@ -118,107 +108,47 @@
|
||||
firstStartup.set(isFirstStartup);
|
||||
$userAuth.init();
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === konami_code[0].toLowerCase()) {
|
||||
konami_code.shift();
|
||||
if (konami_code.length === 0) {
|
||||
if (secret_enabled) {
|
||||
sileo.success({
|
||||
title: 'Cheatcode deactivated!',
|
||||
duration: 3000,
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
const audio = $nyanCatSong;
|
||||
if (audio) {
|
||||
const fadeOutDuration = 2000; // 2 seconds
|
||||
const fadeOutSteps = 20;
|
||||
const fadeOutStepTime = fadeOutDuration / fadeOutSteps;
|
||||
const volumeStep = audio.volume / fadeOutSteps;
|
||||
|
||||
const fadeOutInterval = setInterval(() => {
|
||||
if (audio.volume > 0) {
|
||||
audio.volume = Math.max(0, audio.volume - volumeStep);
|
||||
} else {
|
||||
audio.pause();
|
||||
clearInterval(fadeOutInterval);
|
||||
}
|
||||
}, fadeOutStepTime);
|
||||
}
|
||||
nyanCatSong.set(undefined);
|
||||
secret_enabled = false;
|
||||
konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
return;
|
||||
try {
|
||||
const themeVolume = $userSettings.value('volume').get(0.15);
|
||||
custom_theme_volume.set(themeVolume);
|
||||
currentLoadingInfo.set('Loading themes...');
|
||||
const downloadableThemes = await getDownloadableThemes();
|
||||
const themes = await getThemes();
|
||||
const combinedThemes = [...themes];
|
||||
for (const theme of downloadableThemes) {
|
||||
if (!combinedThemes.find((t) => t.name === theme.name)) combinedThemes.push(theme);
|
||||
const installedTheme = themes.find((t) => t.name === theme.name);
|
||||
if (installedTheme) {
|
||||
const installedThemeVersion = new SemVer(installedTheme.version);
|
||||
const downloadableThemeVersion = new SemVer(theme.version);
|
||||
if (downloadableThemeVersion.compare(installedThemeVersion) > 0) {
|
||||
installedTheme.updateAvailable = true;
|
||||
const index = combinedThemes.findIndex((t) => t.name === theme.name);
|
||||
combinedThemes[index] = installedTheme;
|
||||
}
|
||||
sileo.success({
|
||||
title: 'Cheatcode activated!',
|
||||
duration: 3000,
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
const audio = new Audio(NyanCatSong);
|
||||
audio.loop = true;
|
||||
audio.volume = 0;
|
||||
audio.play();
|
||||
const fadeInVolume = 0.15;
|
||||
const fadeInDuration = 2000; // 2 seconds
|
||||
const fadeInSteps = 20;
|
||||
const fadeInStepTime = fadeInDuration / fadeInSteps;
|
||||
const volumeStep = (fadeInVolume - audio.volume) / fadeInSteps;
|
||||
|
||||
const fadeInInterval = setInterval(() => {
|
||||
if (audio.volume < fadeInVolume) {
|
||||
audio.volume = Math.min(fadeInVolume, audio.volume + volumeStep);
|
||||
} else {
|
||||
clearInterval(fadeInInterval);
|
||||
}
|
||||
}, fadeInStepTime);
|
||||
nyanCatSong.set(audio);
|
||||
|
||||
secret_enabled = true;
|
||||
konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
konami_code = [
|
||||
'ArrowUp',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'b',
|
||||
'a',
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
custom_themes.set(combinedThemes);
|
||||
const activeTheme = $userSettings.value('theme').get('Default');
|
||||
const theme = themes.find((theme) => theme.name === activeTheme);
|
||||
if (!theme) {
|
||||
$userSettings.value('theme').set('Default');
|
||||
await $userSettings.save();
|
||||
loadTheme(themes[0], $custom_theme_container!, themeVolume);
|
||||
} else {
|
||||
loadTheme(theme, $custom_theme_container!, themeVolume);
|
||||
}
|
||||
} catch {
|
||||
sileo.error({
|
||||
title: 'An error occured!',
|
||||
description: 'Failed to load theme',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
currentLoadingInfo.set('Loading config...');
|
||||
const config_patching = $userSettings.value('patching');
|
||||
@@ -300,15 +230,18 @@
|
||||
|
||||
<main>
|
||||
<div class="opacity-30">
|
||||
{#if secret_enabled}
|
||||
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
|
||||
<NyanCatBg />
|
||||
</div>
|
||||
{:else}
|
||||
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
|
||||
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
|
||||
<div
|
||||
class={$active_custom_theme && $active_custom_theme.name === 'Default'
|
||||
? 'hidden'
|
||||
: 'absolute z-0 top-0 left-0 w-full h-full object-cover object-center aspect-video'}
|
||||
bind:this={$custom_theme_container}
|
||||
use:trackMediaContainer
|
||||
></div>
|
||||
{#if ($active_custom_theme && $active_custom_theme.name === 'Default') || !$active_custom_theme}
|
||||
<AnimatedBg />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
+426
-46
@@ -1,19 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$assets/logo.png';
|
||||
import DefaultThemePreview from '$assets/default_preview.png';
|
||||
import * as Avatar from '@/components/ui/avatar';
|
||||
import Badge from '@/components/ui/badge/badge.svelte';
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import * as Select from '@/components/ui/select';
|
||||
import {
|
||||
active_custom_theme,
|
||||
beatmapSets,
|
||||
currentSkin,
|
||||
custom_theme_container,
|
||||
custom_theme_volume,
|
||||
custom_themes,
|
||||
discordPresence,
|
||||
launcherStream,
|
||||
launcherStreams,
|
||||
launcherVersion,
|
||||
launching,
|
||||
newVersion,
|
||||
nyanCatSong,
|
||||
osuBuild,
|
||||
osuStream,
|
||||
platform,
|
||||
@@ -39,19 +43,27 @@
|
||||
ArrowRight,
|
||||
Settings,
|
||||
House,
|
||||
Paintbrush,
|
||||
Trash,
|
||||
CloudDownload,
|
||||
Import,
|
||||
} from 'lucide-svelte';
|
||||
import NumberFlow from '@number-flow/svelte';
|
||||
import * as AlertDialog from '@/components/ui/alert-dialog';
|
||||
import Progress from '@/components/ui/progress/progress.svelte';
|
||||
import {
|
||||
compareBuildNumbers,
|
||||
fadeGlobalVolume,
|
||||
formatBytes,
|
||||
numberHumanReadable,
|
||||
openURL,
|
||||
pauseGlobalMedia,
|
||||
releaseStreamToReadable,
|
||||
resumeGlobalMedia,
|
||||
setCurrentTimeGlobalMedia,
|
||||
urlIsValidImage,
|
||||
} from '@/utils';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { fade, fly, scale } from 'svelte/transition';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import Label from '@/components/ui/label/label.svelte';
|
||||
import {
|
||||
@@ -110,6 +122,18 @@
|
||||
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
|
||||
import { animate } from 'animejs';
|
||||
import { sileo } from 'sileo';
|
||||
import ScrollContainer from '@/components/ui/scroll-container/ScrollContainer.svelte';
|
||||
import {
|
||||
checkThemeFromFile,
|
||||
deleteTheme,
|
||||
downloadTheme,
|
||||
importThemeFromFile,
|
||||
loadTheme,
|
||||
type ThemeInfo,
|
||||
} from '@/themes';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useDropZone } from '@/dropZone.svelte';
|
||||
|
||||
let selectedView = $state('home');
|
||||
let progress = $state(-1);
|
||||
@@ -121,6 +145,47 @@
|
||||
|
||||
let downloadingUpdate = $state(false);
|
||||
|
||||
let dragAndDrop = useDropZone({
|
||||
onDrop: async (file) => {
|
||||
const firstFile = file[0];
|
||||
if (!firstFile.endsWith('.ezpplauncher-theme')) {
|
||||
sileo.error({
|
||||
title: 'Uhh...',
|
||||
description: 'Dropped file is not a valid theme file.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const importResult = await checkThemeFromFile(firstFile);
|
||||
if (!importResult.success || !importResult.themeInfo) {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
description: importResult.error || 'An unknown error occurred.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
droppedTheme = {
|
||||
filePath: firstFile,
|
||||
themeInfo: importResult.themeInfo,
|
||||
};
|
||||
},
|
||||
});
|
||||
let droppedTheme = $state<
|
||||
| {
|
||||
filePath: string;
|
||||
themeInfo: ThemeInfo;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
let themeInstalling = $state(false);
|
||||
|
||||
let downloadingEZPPFiles = $state(false);
|
||||
let cleanup = $state(false);
|
||||
|
||||
@@ -170,7 +235,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
const beatmapSetCount: number | null = await getBeatmapSetsCount(selectedPath);
|
||||
const beatmapSetCount = await getBeatmapSetsCount(selectedPath);
|
||||
if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
|
||||
|
||||
const skins_list = await getSkins(selectedPath);
|
||||
@@ -435,23 +500,7 @@
|
||||
await new Promise((res) => setTimeout(res, 1500));
|
||||
launchInfo = 'Launching osu!...';
|
||||
|
||||
if ($nyanCatSong) {
|
||||
const audio = $nyanCatSong;
|
||||
const fadeOutDuration = 2000; // 2 seconds
|
||||
const fadeOutSteps = 20;
|
||||
const fadeOutStepTime = fadeOutDuration / fadeOutSteps;
|
||||
const volumeStep = audio.volume / fadeOutSteps;
|
||||
|
||||
const fadeOutInterval = setInterval(() => {
|
||||
if (audio.volume > 0) {
|
||||
audio.volume = Math.max(0, audio.volume - volumeStep);
|
||||
} else {
|
||||
clearInterval(fadeOutInterval);
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
}, fadeOutStepTime);
|
||||
}
|
||||
fadeGlobalVolume($custom_theme_volume, 0, 2000, pauseGlobalMedia);
|
||||
|
||||
await replaceUIFiles(osuPath, false);
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
@@ -563,25 +612,9 @@
|
||||
cleanup = true;
|
||||
launchInfo = 'Cleaning up...';
|
||||
|
||||
if ($nyanCatSong) {
|
||||
const audio = $nyanCatSong;
|
||||
audio.play();
|
||||
audio.volume = 0;
|
||||
|
||||
const fadeInVolume = 0.15;
|
||||
const fadeInDuration = 2000; // 2 seconds
|
||||
const fadeInSteps = 20;
|
||||
const fadeInStepTime = fadeInDuration / fadeInSteps;
|
||||
const volumeStep = (fadeInVolume - audio.volume) / fadeInSteps;
|
||||
|
||||
const fadeInInterval = setInterval(() => {
|
||||
if (audio.volume < fadeInVolume) {
|
||||
audio.volume = Math.min(fadeInVolume, audio.volume + volumeStep);
|
||||
} else {
|
||||
clearInterval(fadeInInterval);
|
||||
}
|
||||
}, fadeInStepTime);
|
||||
}
|
||||
setCurrentTimeGlobalMedia(0);
|
||||
resumeGlobalMedia();
|
||||
fadeGlobalVolume(0, $custom_theme_volume, 2000);
|
||||
|
||||
await getCurrentWindow().show();
|
||||
if (presenceUpdater) {
|
||||
@@ -739,6 +772,74 @@
|
||||
}, 450);
|
||||
};
|
||||
|
||||
const setupThemeImport = async () => {
|
||||
const initialUrls = await invoke<string[]>('opened_urls');
|
||||
if (initialUrls.length > 0) {
|
||||
const firstFile = initialUrls[0];
|
||||
if (!firstFile.endsWith('.ezpplauncher-theme')) {
|
||||
sileo.error({
|
||||
title: 'Uhh...',
|
||||
description: 'Dropped file is not a valid theme file.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const importResult = await checkThemeFromFile(firstFile);
|
||||
if (!importResult.success || !importResult.themeInfo) {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
description: importResult.error || 'An unknown error occurred.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
droppedTheme = {
|
||||
filePath: firstFile,
|
||||
themeInfo: importResult.themeInfo,
|
||||
};
|
||||
}
|
||||
|
||||
await listen<string[]>('opened', async (event) => {
|
||||
const files = event.payload;
|
||||
if (files.length > 0) {
|
||||
const firstFile = files[0];
|
||||
if (!firstFile.endsWith('.ezpplauncher-theme')) {
|
||||
sileo.error({
|
||||
title: 'Uhh...',
|
||||
description: 'Dropped file is not a valid theme file.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const importResult = await checkThemeFromFile(firstFile);
|
||||
if (!importResult.success || !importResult.themeInfo) {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
description: importResult.error || 'An unknown error occurred.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
droppedTheme = {
|
||||
filePath: firstFile,
|
||||
themeInfo: importResult.themeInfo,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const config = $userSettings;
|
||||
const trackingConsent = config.value('tracking_consent');
|
||||
@@ -755,6 +856,8 @@
|
||||
ease: (t: number) => (t - 1) ** 7 + 1,
|
||||
onComplete: doBPMAnimation,
|
||||
});
|
||||
|
||||
setupThemeImport();
|
||||
return () => {
|
||||
window.clearInterval(animateInterval);
|
||||
};
|
||||
@@ -921,19 +1024,139 @@
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<div class="grid grid-cols-[0.085fr_1fr] h-[100vh] relative">
|
||||
<AlertDialog.Root open={$newVersion === undefined && !!droppedTheme}>
|
||||
<AlertDialog.Content
|
||||
class="bg-theme-950 border-theme-800 p-0 max-w-2xl"
|
||||
escapeKeydownBehavior="ignore"
|
||||
interactOutsideBehavior="ignore"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center border-b border-theme-800 bg-black/40 rounded-t-lg p-3"
|
||||
>
|
||||
<Import size={40} />
|
||||
<span class="font-semibold text-xl">Import Theme</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center text-sm text-center bg-theme-900 border border-theme-800 rounded-lg mx-3 p-3"
|
||||
>
|
||||
<p class="mb-4">
|
||||
You are about to import the theme <strong>"{droppedTheme?.themeInfo.name}"</strong> by
|
||||
<strong>{droppedTheme?.themeInfo.author}</strong>.
|
||||
</p>
|
||||
<div
|
||||
class="mb-4 text-red-400 bg-red-800/20 border border-red-700/20 rounded-lg p-2 flex flex-col items-center justify-center"
|
||||
>
|
||||
<div class="text-base font-semibold">Warning</div>
|
||||
<div>Themes from external sources may contain malicious code or assets.</div>
|
||||
<div>Only import themes from creators you trust.</div>
|
||||
</div>
|
||||
<p>Are you sure you want to proceed with the installation?</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center mb-3 gap-4 mt-4">
|
||||
<Button
|
||||
class="min-w-28"
|
||||
onclick={async () => {
|
||||
if (!droppedTheme) {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
description: 'Failed to install theme.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
themeInstalling = true;
|
||||
try {
|
||||
const importResult = await importThemeFromFile(
|
||||
droppedTheme.themeInfo.name,
|
||||
droppedTheme.filePath
|
||||
);
|
||||
if (!importResult.success) {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
description:
|
||||
importResult.error || 'An unknown error occurred while importing your theme.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
sileo.success({
|
||||
title: 'Yay!',
|
||||
description: 'Theme imported successfully',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
sileo.error({
|
||||
title: 'Hmmm...',
|
||||
description: 'An unknown error occurred while importing your theme.',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
droppedTheme = undefined;
|
||||
themeInstalling = false;
|
||||
}
|
||||
}}
|
||||
disabled={themeInstalling}
|
||||
>
|
||||
{#if themeInstalling}
|
||||
<LoaderCircle class="animate-spin" size={18} />
|
||||
{:else}
|
||||
Yes, import
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={async () => {
|
||||
themeInstalling = false;
|
||||
droppedTheme = undefined;
|
||||
}}
|
||||
disabled={themeInstalling}
|
||||
>
|
||||
No, don't import
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<div class="grid grid-cols-[0.085fr_1fr] h-[100vh] relative" bind:this={dragAndDrop.ref}>
|
||||
{#if dragAndDrop.isDraggingOverApp}
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full z-50 flex items-center justify-center bg-black/10 backdrop-blur-sm"
|
||||
transition:fade={{ duration: 300 }}
|
||||
>
|
||||
<div
|
||||
class="w-[90vw] h-[90vh] flex flex-col items-center justify-center bg-theme-900 border border-theme-800 rounded-lg"
|
||||
transition:scale={{ start: 0.7, duration: 300 }}
|
||||
>
|
||||
<Import size={64} />
|
||||
<p>Drag and Drop a .ezpplauncher-theme file here to import</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="p-3 border-r border-r-theme-900 flex flex-col items-center gap-2 z-10 bg-black/40 backdrop-blur-sm"
|
||||
>
|
||||
<div class="bg-primary-800/80 ring-1 ring-inset ring-white/15 rounded-[1.1rem] p-1.5">
|
||||
<div class="bg-primary/30 ring-1 ring-inset ring-white/15 rounded-[1.1rem] p-1.5">
|
||||
<img class="pointer-events-none" src={Logo} alt="logo" bind:this={ezppLogo} />
|
||||
</div>
|
||||
<Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge>
|
||||
<Button
|
||||
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||
'home'
|
||||
? 'bg-primary-300/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||
? 'bg-primary/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary/50 rounded-[0.85rem] p-3 mt-3"
|
||||
disabled={$launching}
|
||||
onclick={() => {
|
||||
if (!$launching) selectedView = 'home';
|
||||
@@ -941,11 +1164,23 @@
|
||||
>
|
||||
<House class="text-theme-200 !size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||
'themes'
|
||||
? 'bg-primary/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary/50 rounded-[0.85rem] p-3 mt-3"
|
||||
disabled={$launching}
|
||||
onclick={() => {
|
||||
if (!$launching) selectedView = 'themes';
|
||||
}}
|
||||
>
|
||||
<Paintbrush class="text-theme-200 !size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
|
||||
'settings'
|
||||
? 'bg-primary-300/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
|
||||
? 'bg-primary/50'
|
||||
: 'bg-black/20 border-black/20'} hover:bg-primary/50 rounded-[0.85rem] p-3 mt-3"
|
||||
disabled={$launching}
|
||||
onclick={() => {
|
||||
if (!$launching) selectedView = 'settings';
|
||||
@@ -1009,7 +1244,9 @@
|
||||
}}
|
||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||
>
|
||||
<div class="relative z-10 px-8 py-4 flex items-center justify-between">
|
||||
<div
|
||||
class="relative z-10 px-8 py-4 flex items-center justify-between bg-black/40 backdrop-blur-sm border-t border-t-theme-900"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
{#if $osuInstallationPath !== ''}
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -1532,6 +1769,149 @@
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
{:else if selectedView === 'themes'}
|
||||
<div
|
||||
class="h-[100vh] w-full flex flex-col items-center bg-black/20 backdrop-blur-sm"
|
||||
in:fly={{
|
||||
duration: $reduceAnimations ? 0 : 400,
|
||||
delay: $reduceAnimations ? 0 : 400,
|
||||
y: 5,
|
||||
opacity: 0,
|
||||
}}
|
||||
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
|
||||
>
|
||||
<ScrollContainer class="pt-8" topOffset={45}>
|
||||
<div class="grid w-full gap-1 grid-cols-3 p-3 pr-5">
|
||||
{#each $custom_themes as theme (theme.folder_name)}
|
||||
<div
|
||||
class="group overflow-hidden rounded-3xl border {$active_custom_theme &&
|
||||
$active_custom_theme.folder_name === theme.folder_name
|
||||
? 'border-primary'
|
||||
: 'border-theme-800'} bg-theme-950 transition hover:border-white/20 h-[295px]"
|
||||
>
|
||||
<img
|
||||
src={theme.preview || DefaultThemePreview}
|
||||
alt="Preview of {theme.name}"
|
||||
class="mb-2 h-40 w-full rounded-t-3xl object-cover object-center pointer-events-none select-none"
|
||||
/>
|
||||
<div class="flex flex-col gap-1 p-3">
|
||||
<div class="mb-4 flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-sm font-semibold">{theme.name}</span>
|
||||
<span class="text-xs text-muted-foreground">by {theme.author}</span>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">v{theme.version}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
class="w-full"
|
||||
disabled={($active_custom_theme &&
|
||||
$active_custom_theme.folder_name === theme.folder_name) ||
|
||||
theme.status === 'downloading' ||
|
||||
theme.status === 'extracting' ||
|
||||
theme.status === 'deleting'}
|
||||
onclick={async () => {
|
||||
if (theme.status === 'installed') {
|
||||
if ($custom_theme_container) {
|
||||
loadTheme(theme, $custom_theme_container, $custom_theme_volume);
|
||||
$userSettings.value('theme').set(theme.name);
|
||||
$userSettings.save();
|
||||
} else {
|
||||
sileo.error({
|
||||
title: 'Uhhm..',
|
||||
description: 'Failed to apply theme.',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const downloadThemeResult = await downloadTheme(theme);
|
||||
if (!downloadThemeResult.success) {
|
||||
if (downloadThemeResult.error && downloadThemeResult.error.length > 0) {
|
||||
sileo.error({
|
||||
title: 'Uhhm..',
|
||||
description: downloadThemeResult.error,
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
sileo.success({
|
||||
title: 'Yaay!',
|
||||
description: 'Theme installed successfully!',
|
||||
fill: '#181825',
|
||||
styles: {
|
||||
description: 'text-center!',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $active_custom_theme && $active_custom_theme.folder_name === theme.folder_name}
|
||||
Theme in use
|
||||
{:else if theme.status !== 'installed'}
|
||||
{#if theme.status === 'downloading'}
|
||||
{theme.updateAvailable ? 'Updating' : 'Downloading'} Theme... ({Math.round(
|
||||
theme.progress * 100
|
||||
)}%)
|
||||
{:else if theme.status === 'extracting'}
|
||||
Extracting Theme... ({Math.round(theme.progress * 100)}%)
|
||||
{:else if theme.status === 'deleting'}
|
||||
Uninstalling Theme...
|
||||
{:else}
|
||||
Download Theme
|
||||
{/if}
|
||||
{:else}
|
||||
Use Theme
|
||||
{/if}
|
||||
</Button>
|
||||
{#if theme.status === 'installed' && theme.updateAvailable}
|
||||
<Button
|
||||
class="min-w-[40px]"
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onclick={async () => {
|
||||
const defaultTheme = $custom_themes.find((t) => t.name === 'Default');
|
||||
if (defaultTheme) {
|
||||
loadTheme(defaultTheme, $custom_theme_container!, $custom_theme_volume);
|
||||
$userSettings.value('theme').set(defaultTheme.name);
|
||||
$userSettings.save();
|
||||
|
||||
if (!(await downloadTheme(theme, true))) {
|
||||
sileo.error({
|
||||
title: 'Uhhm..',
|
||||
description: 'Failed to update theme.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloudDownload />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if (theme.status === 'installed' || theme.status === 'deleting') && $active_custom_theme && $active_custom_theme.folder_name !== theme.folder_name && theme.name !== 'Default'}
|
||||
<Button
|
||||
class="min-w-[40px]"
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
disabled={theme.status !== 'installed'}
|
||||
onclick={() => deleteTheme(theme)}
|
||||
>
|
||||
{#if theme.status === 'deleting'}
|
||||
<LoaderCircle class="animate-spin" />
|
||||
{:else}
|
||||
<Trash />
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,6 +131,9 @@
|
||||
}
|
||||
|
||||
if (!$firstStartup) {
|
||||
/* const launcherTheme = $userSettings.value('launcherTheme').get('default');
|
||||
const them = THEMES.find((theme) => theme.name === launcherTheme);
|
||||
if (them) theme.set(them); */
|
||||
currentLoadingInfo.set('Checking osu installation path...');
|
||||
const validFolder = await isValidOsuFolder($osuInstallationPath);
|
||||
if (!validFolder) {
|
||||
@@ -158,7 +161,6 @@
|
||||
|
||||
currentLoadingInfo.set('Counting skins...');
|
||||
const skins_list = await getSkins($osuInstallationPath);
|
||||
console.log(skins_list);
|
||||
if (skins_list) {
|
||||
skins.set(skins_list);
|
||||
skinsCount.set(skins_list.length);
|
||||
@@ -252,19 +254,19 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-[100vh] w-full">
|
||||
<div class="relative w-80 h-80 flex items-center justify-center">
|
||||
<div class="flex flex-col items-center justify-center h-[100vh] w-full z-10">
|
||||
<div class="relative w-80 h-80 flex items-center justify-center z-10">
|
||||
<svg
|
||||
class="absolute top-0 left-0 w-full h-full animate-spin"
|
||||
style="animation-duration: 5s;"
|
||||
viewBox="0 0 208 208"
|
||||
>
|
||||
<circle
|
||||
class="stroke-primary"
|
||||
cx="104"
|
||||
cy="104"
|
||||
r="90"
|
||||
fill="none"
|
||||
stroke="#ff0098"
|
||||
stroke-width="8"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="180 385"
|
||||
@@ -279,5 +281,5 @@
|
||||
bind:this={ezppLogo}
|
||||
/>
|
||||
</div>
|
||||
<span class="text-theme-200 text-sm mt-5">{$currentLoadingInfo}</span>
|
||||
<span class="text-theme-200 text-sm mt-5 z-10">{$currentLoadingInfo}</span>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
getVersion,
|
||||
isValidOsuFolder,
|
||||
} from '@/osuUtil';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
let selectedStep = $state(1);
|
||||
const steps = ['Welcome', 'Locate your osu! Installation', 'Appearance Settings'];
|
||||
@@ -124,7 +125,6 @@
|
||||
|
||||
onMount(async () => {
|
||||
const osuPath = await autoDetectOsuInstallFolder();
|
||||
console.log(osuPath);
|
||||
if (osuPath) {
|
||||
osuInstallPath = osuPath;
|
||||
autoDetectedOsuPath = true;
|
||||
@@ -372,7 +372,7 @@
|
||||
else selectedStep = Math.min(selectedStep + 1, steps.length);
|
||||
}}
|
||||
disabled={selectedStep > steps.length ||
|
||||
(selectedStep === 2 && osuInstallPath.length <= 0)}
|
||||
(selectedStep === 2 && !dev && osuInstallPath.length <= 0)}
|
||||
>{selectedStep >= steps.length ? 'Finish' : 'Next'}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["@types/node"]
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||
|
||||
Reference in New Issue
Block a user