Compare commits

..

20 Commits

Author SHA1 Message Date
HorizonCode 1fc0391113 style: format code for consistency and readability in sileo-toast component 2026-02-22 20:18:37 +01:00
HorizonCode 62f03cf7c0 fix: update ESLint configuration to disable no-empty rule and adjust unused vars handling 2026-02-22 20:18:21 +01:00
HorizonCode 94f6e850cf feat: update icons and launcher files 2026-02-22 20:14:38 +01:00
HorizonCode d6fdb2a152 chore: update Tauri dependencies to latest versions 2026-02-22 20:14:11 +01:00
HorizonCode 9cd990cf96 chore: update dependencies in Cargo.toml 2026-02-22 20:10:15 +01:00
HorizonCode 2e0ef81bcb feat: implement skin management 2026-02-22 17:05:52 +01:00
HorizonCode 96680ff8b5 fix: update button styles to use ring for better accessibility 2026-02-22 14:12:32 +01:00
HorizonCode 0b9d6373bf fix: add fade to bg and audio 2026-02-22 14:03:44 +01:00
HorizonCode 295c1424c9 chore: add audio 2026-02-22 12:35:08 +01:00
HorizonCode 0e12a801d2 chore: add secret :^) 2026-02-22 00:21:42 +01:00
HorizonCode cf86dba5c4 chore: rework titlebar buttons 2026-02-21 23:31:43 +01:00
HorizonCode 540c653399 fix: reconnect presence after pipe was closed 2026-02-21 23:18:01 +01:00
HorizonCode c9b8d9f599 fix: respect reducedAnimations 2026-02-21 22:59:19 +01:00
HorizonCode 49665d94bc fix: Setup WIzard styling 2026-02-19 19:38:21 +01:00
HorizonCode 1a19c19399 chore: bump version 2026-02-19 15:48:26 +01:00
HorizonCode cbfe5ec868 fix: Setup Wizard not working 2026-02-19 15:41:29 +01:00
HorizonCode c5b18dacbb chore: update lucide-svelte 2026-02-19 15:41:10 +01:00
HorizonCode ef01984875 fix: osu installation detection 2026-02-19 15:40:56 +01:00
HorizonCode 46ee36ce81 fix: disable all buttons and dropdowns on launch 2026-02-17 20:34:57 +01:00
HorizonCode c4658ae4cc fix: sort streaks by depth 2026-02-17 20:13:43 +01:00
74 changed files with 1507 additions and 859 deletions
+25 -25
View File
@@ -10,15 +10,15 @@
"@number-flow/svelte": "^0.3.9", "@number-flow/svelte": "^0.3.9",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.4", "@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-shell": "^2.3.3", "@tauri-apps/plugin-shell": "^2.3.5",
"@types/umami": "^2.10.1", "@types/umami": "^2.10.1",
"animejs": "^4.1.3", "animejs": "^4.1.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lucide-svelte": "^0.562.0", "lucide-svelte": "^0.575.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-to-webcomponent": "^2.0.1", "react-to-webcomponent": "^2.0.1",
@@ -31,11 +31,11 @@
"@eslint/compat": "^2.0.0", "@eslint/compat": "^2.0.0",
"@eslint/js": "^9.35.0", "@eslint/js": "^9.35.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@lucide/svelte": "^0.562.0", "@lucide/svelte": "^0.575.0",
"@sveltejs/adapter-static": "^3.0.9", "@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.37.1", "@sveltejs/kit": "^2.37.1",
"@sveltejs/vite-plugin-svelte": "^6.1.4", "@sveltejs/vite-plugin-svelte": "^6.1.4",
"@tauri-apps/cli": "^2.9.6", "@tauri-apps/cli": "^2.10.0",
"@types/bun": "^1.2.21", "@types/bun": "^1.2.21",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -179,7 +179,7 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lucide/svelte": ["@lucide/svelte@0.562.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-wDMULwtTFN2Sc/TFBm6gfuVCNb4Y5P9LDrwxNnUbV52+IEU7NXZmvxwXoz+vrrpad6Xupq+Hw5eUlqIHEGhouw=="], "@lucide/svelte": ["@lucide/svelte@0.575.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-FEFp/0McZwsjBqh1Dn8H+UBm1yHFQYk+utuVMFDw57155+wz2XMoc1pw027ylCPzs+bi14UEXYKbekFhuJKtnw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -307,37 +307,37 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
"@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.10.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.0", "@tauri-apps/cli-darwin-x64": "2.10.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", "@tauri-apps/cli-linux-arm64-musl": "2.10.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", "@tauri-apps/cli-linux-x64-gnu": "2.10.0", "@tauri-apps/cli-linux-x64-musl": "2.10.0", "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", "@tauri-apps/cli-win32-x64-msvc": "2.10.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ=="], "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw=="], "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.6", "", { "os": "linux", "cpu": "arm" }, "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg=="], "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.0", "", { "os": "linux", "cpu": "arm" }, "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg=="], "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw=="], "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.6", "", { "os": "linux", "cpu": "none" }, "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ=="], "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.0", "", { "os": "linux", "cpu": "none" }, "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA=="], "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ=="], "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ=="], "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg=="], "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="], "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="], "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ=="], "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="],
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="], "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
@@ -603,7 +603,7 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"lucide-svelte": ["lucide-svelte@0.562.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA=="], "lucide-svelte": ["lucide-svelte@0.575.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-Tu15tJfbmRNPaU61yeNFf3jfRHs8ABA+NwTt7TWmwVbhlSA3H7sW65tX6RttcP7HGV4aHUlYhXixZOlntoFBdw=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+9
View File
@@ -36,5 +36,14 @@ export default ts.config(
svelteConfig, svelteConfig,
}, },
}, },
},
{
rules: {
'no-empty': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^\\$\\$(Props|Events|Slots|Generic)$' },
],
},
} }
); );
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "ezpplauncher", "name": "ezpplauncher",
"version": "3.1.1", "version": "3.1.2",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -24,15 +24,15 @@
"@number-flow/svelte": "^0.3.9", "@number-flow/svelte": "^0.3.9",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.4", "@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-shell": "^2.3.3", "@tauri-apps/plugin-shell": "^2.3.5",
"@types/umami": "^2.10.1", "@types/umami": "^2.10.1",
"animejs": "^4.1.3", "animejs": "^4.1.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lucide-svelte": "^0.562.0", "lucide-svelte": "^0.575.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-to-webcomponent": "^2.0.1", "react-to-webcomponent": "^2.0.1",
@@ -45,11 +45,11 @@
"@eslint/compat": "^2.0.0", "@eslint/compat": "^2.0.0",
"@eslint/js": "^9.35.0", "@eslint/js": "^9.35.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@lucide/svelte": "^0.562.0", "@lucide/svelte": "^0.575.0",
"@sveltejs/adapter-static": "^3.0.9", "@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.37.1", "@sveltejs/kit": "^2.37.1",
"@sveltejs/vite-plugin-svelte": "^6.1.4", "@sveltejs/vite-plugin-svelte": "^6.1.4",
"@tauri-apps/cli": "^2.9.6", "@tauri-apps/cli": "^2.10.0",
"@types/bun": "^1.2.21", "@types/bun": "^1.2.21",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
+297 -374
View File
File diff suppressed because it is too large Load Diff
+14 -13
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ezpplauncher" name = "ezpplauncher"
version = "3.1.1" version = "3.1.2"
description = "EZPPLauncher redefined." description = "EZPPLauncher redefined."
authors = ["HorizonCode"] authors = ["HorizonCode"]
edition = "2024" edition = "2024"
@@ -15,33 +15,34 @@ name = "ezpplauncher_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.5.3", features = [] } tauri-build = { version = "2.5.5", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.9.5", features = [] } tauri = { version = "2.10.2", features = [] }
tauri-plugin-shell = "2.3.3" tauri-plugin-shell = "2.3.5"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148" serde_json = "1.0.149"
serde_repr = "0.1.20" serde_repr = "0.1.20"
tauri-plugin-dialog = "2.4.2" tauri-plugin-dialog = "2.6.0"
tauri-plugin-fs = "2.4.4" tauri-plugin-fs = "2.4.5"
hardware-id = "0.3.0" hardware-id = "0.3.0"
tauri-plugin-cors-fetch = "4.1.0" tauri-plugin-cors-fetch = "5.0.0"
sysinfo = "0.37.2" sysinfo = "0.38.2"
reqwest = { version = "0.13.1", features = ["json", "stream", "query"] } reqwest = { version = "0.13.2", features = ["json", "stream", "query"] }
md5 = "0.8.0" md5 = "0.8.0"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
open = "5.3.3" open = "5.3.3"
windows-sys = "0.61.2" discord-rich-presence = "1.1.0"
discord-rich-presence = "1.0.0"
once_cell = "1.21.3" once_cell = "1.21.3"
futures-util = "0.3.32"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.55.0" winreg = "0.55.0"
winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] } winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] }
base64 = "0.22.1" base64 = "0.22.1"
widestring = "1.2.1" widestring = "1.2.1"
windows-sys = "0.61.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.6" tauri-plugin-single-instance = "2.4.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+76 -7
View File
@@ -77,10 +77,10 @@ pub fn find_osu_installation() -> Option<String> {
let hklm_registry_paths = ["SOFTWARE\\Classes\\osu\\DefaultIcon"]; let hklm_registry_paths = ["SOFTWARE\\Classes\\osu\\DefaultIcon"];
let hkcr_registry_paths = [ let hkcu_registry_paths = [
"osustable.File.osk\\DefaultIcon", "Software\\Classes\\osustable.File.osk\\DefaultIcon",
"osustable.File.osr\\DefaultIcon", "Software\\Classes\\osustable.File.osr\\DefaultIcon",
"osustable.File.osz\\DefaultIcon", "Software\\Classes\\osustable.File.osz\\DefaultIcon",
]; ];
let osu_folder_files = [ let osu_folder_files = [
@@ -129,10 +129,10 @@ pub fn find_osu_installation() -> Option<String> {
} }
} }
let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); let hkcu = RegKey::predef(HKEY_CURRENT_USER);
for reg_path in &hkcr_registry_paths { for reg_path in &hkcu_registry_paths {
if let Ok(subkey) = hkcr.open_subkey_with_flags(reg_path, KEY_READ | KEY_WOW64_32KEY) { if let Ok(subkey) = hkcu.open_subkey_with_flags(reg_path, KEY_READ | KEY_WOW64_32KEY) {
let value_names = [""]; let value_names = [""];
for value_name in &value_names { for value_name in &value_names {
if let Ok(value) = subkey.get_value::<String, _>(value_name) { if let Ok(value) = subkey.get_value::<String, _>(value_name) {
@@ -185,6 +185,75 @@ pub async fn get_beatmapsets_count(folder: String) -> Option<u64> {
return Some(count); return Some(count);
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SkinInfo {
pub name: String,
pub author: Option<String>,
pub modified: u64,
}
#[tauri::command]
pub async fn get_skins(folder: String) -> Option<Vec<SkinInfo>> {
use std::path::PathBuf;
use std::time::UNIX_EPOCH;
use tokio::fs;
let skins_folder = PathBuf::from(folder).join("Skins");
if !skins_folder.exists() {
return None;
}
let mut entries = fs::read_dir(skins_folder).await.ok()?;
let mut skins = Vec::new();
while let Some(entry) = entries.next_entry().await.ok()? {
if !entry.file_type().await.ok()?.is_dir() {
continue;
}
let dir_path = entry.path();
let skin_ini = dir_path.join("skin.ini");
if !skin_ini.exists() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
let author = fs::read_to_string(&skin_ini)
.await
.ok()
.and_then(|content| {
content.lines().find_map(|line| {
let (key, value) = line.split_once(':')?;
key.trim()
.eq_ignore_ascii_case("Author")
.then(|| value.trim().to_string())
})
});
let modified = entry
.metadata()
.await
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
skins.push(SkinInfo {
name,
author,
modified,
});
}
Some(skins)
}
#[tauri::command] #[tauri::command]
pub async fn get_skins_count(folder: String) -> Option<u64> { pub async fn get_skins_count(folder: String) -> Option<u64> {
let path = PathBuf::from(folder); let path = PathBuf::from(folder);
+3 -2
View File
@@ -8,8 +8,8 @@ use crate::commands::{
check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files, check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files,
encrypt_string, exit, find_osu_installation, get_beatmapsets_count, encrypt_string, exit, find_osu_installation, get_beatmapsets_count,
get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream, get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream,
get_osu_skin, get_osu_version, get_platform, get_skins_count, has_net8, has_osuwinello, get_osu_skin, get_osu_version, get_platform, get_skins, get_skins_count, has_net8,
has_wmctrl, install_ezpp_launcher_update, is_osu_running, open_url_in_browser, 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_connect, presence_disconnect, presence_is_connected, presence_update_status,
presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values, presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values,
set_osu_user_config_values, valid_osu_folder, set_osu_user_config_values, valid_osu_folder,
@@ -37,6 +37,7 @@ pub fn run() {
find_osu_installation, find_osu_installation,
valid_osu_folder, valid_osu_folder,
get_beatmapsets_count, get_beatmapsets_count,
get_skins,
get_skins_count, get_skins_count,
get_osu_version, get_osu_version,
get_osu_release_stream, get_osu_release_stream,
+30 -12
View File
@@ -2,6 +2,7 @@ use discord_rich_presence::{
DiscordIpc, DiscordIpcClient, DiscordIpc, DiscordIpcClient,
activity::{Activity, Assets, Button, Timestamps}, activity::{Activity, Assets, Button, Timestamps},
}; };
use futures_util::future::BoxFuture;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::sync::Mutex as StdMutex; use std::sync::Mutex as StdMutex;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@@ -24,6 +25,7 @@ enum PresenceCommand {
Disconnect(oneshot::Sender<()>), Disconnect(oneshot::Sender<()>),
UpdateData(PresenceData), UpdateData(PresenceData),
IsConnected(oneshot::Sender<bool>), IsConnected(oneshot::Sender<bool>),
Tick,
} }
struct PresenceActor { struct PresenceActor {
@@ -58,7 +60,7 @@ impl PresenceActor {
} }
async fn run(&mut self) { async fn run(&mut self) {
let mut update_interval = interval(Duration::from_millis(2500)); let mut update_interval = interval(Duration::from_secs(5));
loop { loop {
tokio::select! { tokio::select! {
@@ -74,18 +76,22 @@ impl PresenceActor {
PresenceCommand::IsConnected(responder) => { PresenceCommand::IsConnected(responder) => {
let _ = responder.send(self.client.is_some()); let _ = responder.send(self.client.is_some());
} }
PresenceCommand::Tick => {
if self.client.is_some() {
self.handle_update().await;
}
}
} }
} }
_ = update_interval.tick() => { _ = update_interval.tick() => {
if self.client.is_some() { let _ = PRESENCE_TX.send(PresenceCommand::Tick).await;
self.handle_update().await;
}
} }
} }
} }
} }
async fn handle_connect(&mut self, responder: oneshot::Sender<bool>) { fn handle_connect(&mut self, responder: oneshot::Sender<bool>) -> BoxFuture<'_, ()> {
Box::pin(async move {
if self.client.is_some() { if self.client.is_some() {
let _ = responder.send(true); let _ = responder.send(true);
return; return;
@@ -101,10 +107,20 @@ impl PresenceActor {
let _ = responder.send(true); let _ = responder.send(true);
} }
Err(e) => { Err(e) => {
eprintln!("Failed to create Discord client: {:?}", e); eprintln!("Failed to connect to Discord: {:?}. Retrying in 5s...", e);
let _ = responder.send(false); 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);
}
} }
} }
})
} }
async fn handle_disconnect(&mut self, responder: oneshot::Sender<()>) { async fn handle_disconnect(&mut self, responder: oneshot::Sender<()>) {
@@ -143,12 +159,14 @@ impl PresenceActor {
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"), Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
]); ]);
if let Err(e) = client.set_activity(activity).map_err(|e| e.to_string()) { if let Err(e) = client.set_activity(activity) {
eprintln!("Failed to set activity, disconnecting: {:?}", e); eprintln!("Failed to set activity, disconnecting: {:?}", e);
if let Some(mut client) = self.client.take() { self.client = None;
let _ = client.clear_activity();
let _ = client.close(); let (tx, _) = oneshot::channel();
} self.handle_connect(tx).await;
} else {
println!("Actor: Presence updated successfully.");
} }
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "EZPPLauncher", "productName": "EZPPLauncher",
"version": "3.1.1", "version": "3.1.2",
"identifier": "farm.EZPPFarm.Launcher", "identifier": "farm.EZPPFarm.Launcher",
"mainBinaryName": "ezpplauncher", "mainBinaryName": "ezpplauncher",
"build": { "build": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.
@@ -1,4 +1,8 @@
<script lang="ts"> <script lang="ts">
import Logo from '$assets/logo.png';
import { reduceAnimations } from '@/userSettings';
import { fade } from 'svelte/transition';
//TODO: maybe dynamic background images fetched from ezpp? //TODO: maybe dynamic background images fetched from ezpp?
const COUNT = 60; const COUNT = 60;
@@ -7,41 +11,43 @@
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
} }
const streaks = Array.from({ length: COUNT }).map((_, i) => { const streaks = Array.from({ length: COUNT })
const depth = rand(0, 2); .map((_, i) => {
const depth = rand(0, 2);
const scale = 0.6 + depth * 0.8; const scale = 0.6 + depth * 0.8;
const speed = 0.4 + depth * 1.35; const speed = 0.4 + depth * 1.35;
const distance = 150; const distance = 150;
const duration = distance / speed; const duration = distance / speed;
return { return {
width: rand(180, 320) * scale, width: rand(300, 320) * scale,
height: rand(16, 32) * scale, height: 32 * scale,
x: (i / COUNT) * 200 - 50, x: (i / COUNT) * 200 - 50,
y: ((i * 137.5) % 100) - 20, y: ((i * 137.5) % 100) - 20,
opacity: 0.25 + depth * 0.65, opacity: 0.25 + depth * 0.65,
duration, duration,
delay: rand(0, duration), delay: rand(0, duration),
scale, scale,
}; };
}); })
.sort((a, b) => a.scale - b.scale);
</script> </script>
<div class="absolute bg blur-[2px]"> <div class="absolute bg blur-[2px]">
<div <div
class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50" class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50"
></div> ></div>
{#each streaks as s} {#each streaks as s, index (index)}
<div <div
class="streak drop-shadow-lg" class="streak drop-shadow-lg transition-opacity duration-1000"
style=" style="
width:{s.width}px; width:{s.width}px;
height:{s.height}px; height:{s.height}px;
left:{s.x}%; left:{s.x}%;
top:{s.y}%; top:{s.y}%;
opacity:{s.opacity}; opacity:{$reduceAnimations ? 0 : s.opacity};
animation-duration:{s.duration}s; animation-duration:{s.duration}s;
animation-delay:-{s.delay}s; animation-delay:-{s.delay}s;
" "
@@ -54,7 +60,12 @@
position: fixed; position: fixed;
inset: 0; inset: 0;
overflow: hidden; overflow: hidden;
background: linear-gradient(180deg, var(--color-primary-300) 0%, var(--color-primary-600) 40%, var(--color-primary-900) 100%); background: linear-gradient(
180deg,
var(--color-primary-300) 0%,
var(--color-primary-600) 40%,
var(--color-primary-900) 100%
);
} }
.streak { .streak {
@@ -0,0 +1,92 @@
<script lang="ts">
import NyanCat from '$assets/nyan-cat.gif';
import { reduceAnimations } from '@/userSettings';
import { fade } from 'svelte/transition';
//TODO: maybe dynamic background images fetched from ezpp?
const COUNT = 18;
function rand(min: number, max: number) {
return Math.random() * (max - min) + min;
}
const streaks = Array.from({ length: COUNT })
.map((_, i) => {
const depth = rand(0, 2);
const scale = 0.6 + depth * 0.8;
const speed = 0.4 + depth * 1.35;
const distance = 150;
const duration = distance / speed;
return {
width: 290 * scale,
height: 140 * scale,
x: (i / COUNT) * 200 - 50,
y: ((i * 137.5) % 100) - 20,
opacity: 0.25 + depth * 0.65,
duration,
delay: rand(0, duration),
scale,
};
})
.sort((a, b) => a.scale - b.scale);
</script>
<div class="absolute bg blur-[2px]">
<div
class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50"
></div>
{#each streaks as s, index (index)}
<div
class="streak drop-shadow-lg transition-opacity duration-1000"
style="
width:{s.width}px;
height:{s.height}px;
left:{s.x}%;
top:{s.y}%;
opacity:{$reduceAnimations ? 0 : s.opacity};
animation-duration:{s.duration}s;
animation-delay:-{s.delay}s;
"
>
<img
src={NyanCat}
alt="nyan cat"
class="w-full h-full"
style="opacity:{$reduceAnimations ? 0 : s.opacity};"
/>
</div>
{/each}
</div>
<style>
.bg {
position: fixed;
inset: 0;
overflow: hidden;
background: linear-gradient(
180deg,
var(--color-blue-800) 0%,
var(--color-blue-600) 40%,
var(--color-blue-900) 100%
);
}
.streak {
position: absolute;
animation: move linear infinite;
will-change: transform;
}
@keyframes move {
from {
transform: translate(-250vw, 0);
}
to {
transform: translate(250vw, 0);
}
}
</style>
@@ -0,0 +1,143 @@
<script lang="ts">
// Props
let { children, class: className = '', topOffset = 0 } = $props();
let wrapper: HTMLDivElement;
let container: HTMLDivElement;
let thumb: HTMLDivElement;
// Thumb state
let thumbHeight = $state(20);
let thumbTop = $state(0);
// Drag state
let dragging = false;
let startY = 0;
let startScroll = 0;
/** Update thumb height & position */
function updateScrollbar() {
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const scrollable = scrollHeight - clientHeight;
if (scrollable <= 0) {
thumbHeight = 0;
thumbTop = 0;
return;
}
thumbHeight = Math.max((clientHeight / scrollHeight) * clientHeight, 24);
const trackHeight = clientHeight - thumbHeight - topOffset;
thumbTop = (scrollTop / scrollable) * trackHeight;
}
/** Start dragging the thumb */
function startDrag(e: PointerEvent) {
dragging = true;
startY = e.clientY;
startScroll = container.scrollTop;
thumb.setPointerCapture(e.pointerId);
}
/** Drag the thumb */
function drag(e: PointerEvent) {
if (!dragging) return;
const delta = e.clientY - startY;
const { scrollHeight, clientHeight } = container;
const scrollable = scrollHeight - clientHeight;
const trackHeight = clientHeight - thumbHeight - topOffset;
container.scrollTop = startScroll + (delta / trackHeight) * scrollable;
}
/** End dragging */
function endDrag(e: PointerEvent) {
dragging = false;
thumb.releasePointerCapture(e.pointerId);
}
// Reactive effect to update scrollbar
$effect(() => updateScrollbar());
// ResizeObserver for dynamic content
$effect(() => {
if (!container) return;
const observer = new ResizeObserver(updateScrollbar);
observer.observe(container);
return () => observer.disconnect();
});
</script>
<div class="wrapper" bind:this={wrapper}>
<!-- Scrollable content -->
<div class={`content ${className}`} bind:this={container} onscroll={updateScrollbar}>
{@render children?.()}
</div>
<!-- Custom scrollbar -->
<div class="scrollbar" style="top: {topOffset}px;">
<div
class="thumb"
bind:this={thumb}
onpointerdown={startDrag}
onpointermove={drag}
onpointerup={endDrag}
onpointercancel={endDrag}
style="height:{thumbHeight}px; transform:translateY({thumbTop}px);"
></div>
</div>
</div>
<style>
.wrapper {
position: relative;
height: 100%;
width: 100%;
}
/* Scrollable content */
.content {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none; /* Firefox */
}
.content::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
/* Scrollbar track */
.scrollbar {
position: absolute;
right: 2px;
bottom: 0;
width: 12px;
background: var(--color-theme-950);
border-radius: 999px;
}
/* Thumb */
.thumb {
position: absolute;
width: 100%;
background: var(--color-theme-800);
border-radius: 999px;
cursor: grab;
transition: background 0.15s;
}
.thumb:active {
cursor: grabbing;
background: var(--color-theme-600);
}
.thumb:hover {
background: var(--color-theme-700);
}
</style>
+13 -13
View File
@@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import { createElement } from 'react' import { createElement } from 'react';
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client';
import type { SileoOptions } from 'sileo' import type { SileoOptions } from 'sileo';
let props: SileoOptions = $props() let props: SileoOptions = $props();
let rootEl: HTMLElement let rootEl: HTMLElement;
$effect(() => { $effect(() => {
const root = createRoot(rootEl) const root = createRoot(rootEl);
import('sileo').then(({ Toaster }) => { import('sileo').then(({ Toaster }) => {
const sileo = createElement(Toaster, { ...props }) const sileo = createElement(Toaster, { ...props });
root.render(sileo) root.render(sileo);
}) });
return () => root.unmount() return () => root.unmount();
}) });
</script> </script>
<div bind:this={rootEl}></div> <div bind:this={rootEl}></div>
+24 -21
View File
@@ -8,6 +8,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { launcherVersion } from '@/global'; import { launcherVersion } from '@/global';
import Badge from '../badge/badge.svelte'; import Badge from '../badge/badge.svelte';
import { reduceAnimations } from '@/userSettings';
onMount(() => { onMount(() => {
const appWindow = getCurrentWindow(); const appWindow = getCurrentWindow();
@@ -20,22 +21,39 @@
</script> </script>
<div data-tauri-drag-region class="titlebar z-[100]"> <div data-tauri-drag-region class="titlebar z-[100]">
<div class="titlebar-button rounded transition-colors duration-75" id="titlebar-minimize"> <div class="relative flex items-center justify-center p-1 group" id="titlebar-minimize">
<Minimize size={14} /> <div
class="absolute inset-0 bg-background rounded
scale-50 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} />
</div> </div>
<div class="titlebar-button close rounded transition-colors duration-75" id="titlebar-close"> <div class="relative flex items-center justify-center p-1 group" id="titlebar-close">
<Close size={14} /> <div
class="absolute inset-0 bg-red-500 rounded
scale-50 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} />
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
.titlebar { .titlebar {
height: 35px; height: 35px;
/* background: #040612; */
user-select: none; user-select: none;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: 5px;
align-items: center; align-items: center;
position: fixed; position: fixed;
top: 0; top: 0;
@@ -45,19 +63,4 @@
padding-right: 5px; padding-right: 5px;
pointer-events: auto; pointer-events: auto;
} }
.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 25px;
height: 25px;
user-select: none;
-webkit-user-select: none;
}
.titlebar-button:hover {
&.close {
background: #c22e2e;
}
background: #2d3049;
}
</style> </style>
+3 -1
View File
@@ -24,6 +24,7 @@ export const currentLoadingInfo = writable<string>('Initializing...');
export const firstStartup = writable<boolean>(false); export const firstStartup = writable<boolean>(false);
export const launching = writable<boolean>(false); export const launching = writable<boolean>(false);
export const nyanCatSong = writable<HTMLAudioElement | undefined>(undefined);
export const serverPing = writable<number | undefined>(undefined); export const serverPing = writable<number | undefined>(undefined);
export const serverConnectionFails = writable(0); export const serverConnectionFails = writable(0);
@@ -31,7 +32,8 @@ export const serverConnectionFails = writable(0);
export const onlineFriends = writable<number | undefined>(undefined); export const onlineFriends = writable<number | undefined>(undefined);
export const beatmapSets = writable<number | undefined>(undefined); export const beatmapSets = writable<number | undefined>(undefined);
export const skins = writable<number | undefined>(undefined); export const skinsCount = writable<number | undefined>(undefined);
export const skins = writable<{ name: string; author: string | undefined; modified: number }[]>([]);
export const osuStream = writable<string | undefined>(undefined); export const osuStream = writable<string | undefined>(undefined);
export const osuBuild = writable<string | undefined>(undefined); export const osuBuild = writable<string | undefined>(undefined);
+17 -7
View File
@@ -5,6 +5,18 @@ import { betterFetch } from '@better-fetch/fetch';
const updateUrl = 'https://ez-pp.farm/api/ezpplauncher'; const updateUrl = 'https://ez-pp.farm/api/ezpplauncher';
export function normalizeOsuName(name: string): string {
return (
name
.trim()
// Remove leading symbols/spaces
.replace(/^[^a-zA-Z0-9]+/, '')
// Normalize spacing
.replace(/\s+/g, ' ')
.toLowerCase()
);
}
export const getHWID = async () => { export const getHWID = async () => {
const hwid = await invoke('get_hwid'); const hwid = await invoke('get_hwid');
return typeof hwid === 'string' ? hwid : undefined; return typeof hwid === 'string' ? hwid : undefined;
@@ -55,13 +67,6 @@ export const getBeatmapSetsCount = async (folder: string) => {
return typeof result === 'number' ? result : 0; return typeof result === 'number' ? result : 0;
}; };
export const getSkinsCount = async (folder: string) => {
const result = await invoke('get_skins_count', {
folder,
});
return typeof result === 'number' ? result : 0;
};
export const getSkin = async (folder: string) => { export const getSkin = async (folder: string) => {
const result = await invoke('get_osu_skin', { const result = await invoke('get_osu_skin', {
folder, folder,
@@ -151,3 +156,8 @@ export const downloadUpdate = async (
} }
}; };
export const installUpdate = async () => await invoke('install_ezpp_launcher_update'); export const installUpdate = async () => await invoke('install_ezpp_launcher_update');
export const getSkins = async (folder: string) =>
await invoke<{ name: string; author: string | undefined; modified: number }[] | undefined>(
'get_skins',
{ folder }
);
+129 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Logo from '$assets/logo.png'; import Logo from '$assets/logo.png';
import NyanCatSong from '$assets/nyan-cat.mp3';
import '../app.css'; import '../app.css';
import Titlebar from '@/components/ui/titlebar/titlebar.svelte'; import Titlebar from '@/components/ui/titlebar/titlebar.svelte';
@@ -9,6 +10,7 @@
discordPresence, discordPresence,
firstStartup, firstStartup,
launcherVersion, launcherVersion,
nyanCatSong,
platform, platform,
presenceLoading, presenceLoading,
setupValues, setupValues,
@@ -30,14 +32,18 @@
import { encryptString, exit, getLauncherVersion, getPlatform } from '@/osuUtil'; import { encryptString, exit, getLauncherVersion, getPlatform } from '@/osuUtil';
import Button from '@/components/ui/button/button.svelte'; import Button from '@/components/ui/button/button.svelte';
import * as presence from '@/presence'; import * as presence from '@/presence';
import { fade } from 'svelte/transition';
import '@fontsource/sora'; import '@fontsource/sora';
import '@fontsource/space-mono'; import '@fontsource/space-mono';
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte'; import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
import NyanCatBg from '@/components/ui/animated-bg/NyanCatBg.svelte';
import { sileo } from 'sileo';
let { children } = $props(); let { children } = $props();
let unsupported_platform = $state<boolean>(false); let unsupported_platform = $state<boolean>(false);
let secret_enabled = $state<boolean>(false);
function disableReload() { function disableReload() {
if (window.location.hostname !== 'tauri.localhost') { if (window.location.hostname !== 'tauri.localhost') {
@@ -87,6 +93,19 @@
); );
} }
let konami_code = [
'ArrowUp',
'ArrowUp',
'ArrowDown',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'ArrowLeft',
'ArrowRight',
'b',
'a',
];
onMount(async () => { onMount(async () => {
window.Buffer = Buffer; window.Buffer = Buffer;
@@ -98,6 +117,107 @@
const isFirstStartup = await $userSettings.init(); const isFirstStartup = await $userSettings.init();
$userAuth.init(); $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;
}
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 fadeInDuration = 2000; // 2 seconds
const fadeInSteps = 20;
const fadeInStepTime = fadeInDuration / fadeInSteps;
const volumeStep = (1 - audio.volume) / fadeInSteps;
const fadeInInterval = setInterval(() => {
if (audio.volume < 0.3) {
audio.volume = Math.min(0.3, 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',
];
}
});
currentLoadingInfo.set('Loading config...'); currentLoadingInfo.set('Loading config...');
const config_patching = $userSettings.value('patching'); const config_patching = $userSettings.value('patching');
const config_custom_cursor = $userSettings.value('custom_cursor'); const config_custom_cursor = $userSettings.value('custom_cursor');
@@ -180,7 +300,15 @@
<main> <main>
<div class="opacity-30"> <div class="opacity-30">
<AnimatedBg /> {#if secret_enabled}
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<NyanCatBg />
</div>
{:else}
<div transition:fade={{ duration: $reduceAnimations ? 0 : 500 }}>
<AnimatedBg />
</div>
{/if}
</div> </div>
{@render children()} {@render children()}
</main> </main>
+435 -259
View File
@@ -13,6 +13,7 @@
launcherVersion, launcherVersion,
launching, launching,
newVersion, newVersion,
nyanCatSong,
osuBuild, osuBuild,
osuStream, osuStream,
platform, platform,
@@ -20,6 +21,7 @@
serverConnectionFails, serverConnectionFails,
serverPing, serverPing,
skins, skins,
skinsCount,
trackingEnabled, trackingEnabled,
} from '@/global'; } from '@/global';
import { import {
@@ -37,6 +39,8 @@
ArrowRight, ArrowRight,
Settings, Settings,
House, House,
Paintbrush,
FileQuestionMark,
} from 'lucide-svelte'; } from 'lucide-svelte';
import NumberFlow from '@number-flow/svelte'; import NumberFlow from '@number-flow/svelte';
import * as AlertDialog from '@/components/ui/alert-dialog'; import * as AlertDialog from '@/components/ui/alert-dialog';
@@ -49,7 +53,7 @@
releaseStreamToReadable, releaseStreamToReadable,
urlIsValidImage, urlIsValidImage,
} from '@/utils'; } from '@/utils';
import { fade, fly, scale } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import Label from '@/components/ui/label/label.svelte'; import Label from '@/components/ui/label/label.svelte';
import { import {
@@ -84,7 +88,7 @@
getEZPPLauncherUpdateFiles, getEZPPLauncherUpdateFiles,
getReleaseStream, getReleaseStream,
getSkin, getSkin,
getSkinsCount, getSkins,
getVersion, getVersion,
hasNet8, hasNet8,
hasOsuWinello, hasOsuWinello,
@@ -107,8 +111,9 @@
import * as DropdownMenu from '@/components/ui/dropdown-menu'; import * as DropdownMenu from '@/components/ui/dropdown-menu';
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte'; import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
import { animate } from 'animejs'; import { animate } from 'animejs';
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
import { sileo } from 'sileo'; import { sileo } from 'sileo';
import Check from '@lucide/svelte/icons/check';
import ScrollContainer from '@/components/ui/scroll-container/ScrollContainer.svelte';
let selectedView = $state('home'); let selectedView = $state('home');
let progress = $state(-1); let progress = $state(-1);
@@ -172,8 +177,11 @@
const beatmapSetCount: number | null = await getBeatmapSetsCount(selectedPath); const beatmapSetCount: number | null = await getBeatmapSetsCount(selectedPath);
if (beatmapSetCount) beatmapSets.set(beatmapSetCount); if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
const skinsCount: number | null = await getSkinsCount(selectedPath); const skins_list = await getSkins(selectedPath);
if (skinsCount !== null) skins.set(skinsCount); if (skins_list) {
skins.set(skins_list);
skinsCount.set(skins_list.length);
}
const skin: string = await getSkin(selectedPath); const skin: string = await getSkin(selectedPath);
currentSkin.set(skin); currentSkin.set(skin);
@@ -187,6 +195,8 @@
const launch = async () => { const launch = async () => {
if ($launching) return; if ($launching) return;
launchInfo = 'Checking if osu! is already running...';
launching.set(true);
if ($trackingEnabled) umami.track('app_launch_osu'); if ($trackingEnabled) umami.track('app_launch_osu');
const osuRunning = await isOsuRunning(); const osuRunning = await isOsuRunning();
if (osuRunning) { if (osuRunning) {
@@ -199,8 +209,10 @@
description: 'text-center!', description: 'text-center!',
}, },
}); });
launching.set(false);
return; return;
} }
launchInfo = 'Checking osu! installation...';
if (!$osuBuild) { if (!$osuBuild) {
sileo.error({ sileo.error({
title: 'Hmmm...', title: 'Hmmm...',
@@ -210,12 +222,12 @@
description: 'text-center!', description: 'text-center!',
}, },
}); });
launching.set(false);
return; return;
} }
const osuPath = $osuInstallationPath; const osuPath = $osuInstallationPath;
launchInfo = 'Validating osu! installation...'; launchInfo = 'Validating osu! installation...';
launching.set(true);
const validFolder = await isValidOsuFolder(osuPath); const validFolder = await isValidOsuFolder(osuPath);
if (!validFolder) { if (!validFolder) {
@@ -426,6 +438,25 @@
} }
await new Promise((res) => setTimeout(res, 1500)); await new Promise((res) => setTimeout(res, 1500));
launchInfo = 'Launching osu!...'; 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);
}
await replaceUIFiles(osuPath, false); await replaceUIFiles(osuPath, false);
await new Promise((res) => setTimeout(res, 1000)); await new Promise((res) => setTimeout(res, 1000));
await getCurrentWindow().hide(); await getCurrentWindow().hide();
@@ -535,6 +566,26 @@
if ($trackingEnabled) umami.track('app_exit_osu'); if ($trackingEnabled) umami.track('app_exit_osu');
cleanup = true; cleanup = true;
launchInfo = 'Cleaning up...'; launchInfo = 'Cleaning up...';
if ($nyanCatSong) {
const audio = $nyanCatSong;
audio.play();
audio.volume = 0;
const fadeInDuration = 2000; // 2 seconds
const fadeInSteps = 20;
const fadeInStepTime = fadeInDuration / fadeInSteps;
const volumeStep = (1 - audio.volume) / fadeInSteps;
const fadeInInterval = setInterval(() => {
if (audio.volume < 0.3) {
audio.volume = Math.min(0.3, audio.volume + volumeStep);
} else {
clearInterval(fadeInInterval);
}
}, fadeInStepTime);
}
await getCurrentWindow().show(); await getCurrentWindow().show();
if (presenceUpdater) { if (presenceUpdater) {
window.clearInterval(presenceUpdater); window.clearInterval(presenceUpdater);
@@ -563,8 +614,11 @@
const beatmapSetCount = await getBeatmapSetsCount(osuPath); const beatmapSetCount = await getBeatmapSetsCount(osuPath);
if (beatmapSetCount) beatmapSets.set(beatmapSetCount); if (beatmapSetCount) beatmapSets.set(beatmapSetCount);
const skinCount = await getSkinsCount(osuPath); const skinsList = await getSkins(osuPath);
if (skinCount !== null) skins.set(skinCount); if (skinsList) {
skins.set(skinsList);
skinsCount.set(skinsList.length);
}
const skin = await getSkin(osuPath); const skin = await getSkin(osuPath);
currentSkin.set(skin); currentSkin.set(skin);
@@ -874,29 +928,49 @@
<div <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" 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 border border-primary-700/50 rounded-[1.1rem] p-1"> <div class="bg-primary-800/80 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} /> <img class="pointer-events-none" src={Logo} alt="logo" bind:this={ezppLogo} />
</div> </div>
<Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge> <Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge>
<Button <Button
class="flex size-12 items-center gap-2 border {selectedView === 'home' class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
? 'bg-primary-300/50 border-primary-300/50' 'home'
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 hover:border-primary-300/50 rounded-[0.85rem] p-3 mt-3" ? 'bg-primary-300/50'
onclick={() => (selectedView = 'home')} : 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
disabled={$launching}
onclick={() => {
if (!$launching) selectedView = 'home';
}}
> >
<House class="text-theme-200 !size-5" /> <House class="text-theme-200 !size-5" />
</Button> </Button>
<Button <Button
class="flex size-12 items-center gap-2 border {selectedView === 'settings' class="flex size-12 items-center gap-2 ring-1 ring-inset ring-white/15 {selectedView ===
? 'bg-primary-300/50 border-primary-300/50' 'skins'
: 'bg-black/20 border-black/20'} hover:bg-primary-300/50 hover:border-primary-300/50 rounded-[0.85rem] p-3 mt-3" ? 'bg-primary-300/50'
onclick={() => (selectedView = 'settings')} : 'bg-black/20 border-black/20'} hover:bg-primary-300/50 rounded-[0.85rem] p-3 mt-3"
disabled={$launching}
onclick={() => {
if (!$launching) selectedView = 'skins';
}}
>
<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"
disabled={$launching}
onclick={() => {
if (!$launching) selectedView = 'settings';
}}
> >
<Settings class="text-theme-200 !size-5" /> <Settings class="text-theme-200 !size-5" />
</Button> </Button>
<div class="mt-auto"> <div class="mt-auto">
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger disabled={$launching}>
<Avatar.Root class="size-10"> <Avatar.Root class="size-10">
<Avatar.Image src="https://a.ez-pp.farm/{$currentUser?.id ?? 0}" /> <Avatar.Image src="https://a.ez-pp.farm/{$currentUser?.id ?? 0}" />
<Avatar.Fallback class="bg-theme-900"> <Avatar.Fallback class="bg-theme-900">
@@ -942,8 +1016,13 @@
{#if selectedView === 'home'} {#if selectedView === 'home'}
<div <div
class="flex flex-col-reverse h-full" class="flex flex-col-reverse h-full"
in:fly={{ duration: 400, delay: 400, y: 5, opacity: 0 }} in:fly={{
out:fly={{ duration: 400, y: -5, opacity: 0 }} duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 400,
y: 5,
opacity: 0,
}}
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">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
@@ -961,10 +1040,10 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Brush class="size-3.5 text-amber-400" /> <Brush class="size-3.5 text-amber-400" />
<span class="text-[11px] text-white/70 font-medium"> <span class="text-[11px] text-white/70 font-medium">
{#if !$skins && $skins !== 0} {#if !$skinsCount && $skinsCount !== 0}
<LoaderCircle class="animate-spin" size={12} /> <LoaderCircle class="animate-spin" size={12} />
{:else} {:else}
{numberHumanReadable($skins ?? 0)} {numberHumanReadable($skinsCount ?? 0)}
{/if} {/if}
</span> </span>
</div> </div>
@@ -1063,9 +1142,9 @@
{#if $currentUserInfo} {#if $currentUserInfo}
<div <div
class="m-1 w-72 h-48 bg-black/40 backdrop-blur-sm rounded-md border border-black/40 flex flex-col items-center p-3" class="m-1 w-72 h-48 bg-black/40 backdrop-blur-sm rounded-md ring-1 ring-inset ring-white/5 flex flex-col items-center p-3"
in:fly={{ duration: 400, y: 5, opacity: 0 }} in:fly={{ duration: $reduceAnimations ? 0 : 400, y: 5, opacity: 0 }}
out:fly={{ duration: 400, y: -5, opacity: 0 }} out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
> >
<Select.Root <Select.Root
type="single" type="single"
@@ -1216,254 +1295,351 @@
</div> </div>
{:else if selectedView === 'settings'} {:else if selectedView === 'settings'}
<div <div
class="h-[100vh] w-full flex flex-col items-center justify-center" class="h-[100vh] w-full flex flex-col items-center justify-center bg-black/20 backdrop-blur-sm"
in:fly={{ duration: 400, delay: 400, y: 5, opacity: 0 }} in:fly={{
out:fly={{ duration: 400, y: -5, opacity: 0 }} duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 400,
y: 5,
opacity: 0,
}}
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
> >
<div class="p-8 w-full"> <div class="grid grid-cols-[1fr_auto] w-full gap-y-5 items-center px-6">
<div class="bg-black/40 backdrop-blur-sm py-8 px-6 rounded-lg"> <div class="flex flex-col">
<div class="grid grid-cols-[1fr_auto] gap-y-5 items-center px-6"> <Label class="text-sm" for="setting-patch">Patching</Label>
<div class="flex flex-col"> <div class="text-muted-foreground text-xs">
<Label class="text-sm" for="setting-patch">Patching</Label> Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span
<div class="text-muted-foreground text-xs"> class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg !text-[0.55rem]"
Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span >currently only on windows!</span
class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg !text-[0.55rem]" >
>currently only on windows!</span {/if}
>
{/if}
</div>
</div>
<Checkbox
id="setting-patch"
checked={$platform === 'windows' ? $patch : false}
disabled={$platform !== 'windows'}
onCheckedChange={async (e) => {
patch.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Lazer-Style Cursor</Label>
<div class="text-muted-foreground text-xs">
Enable a custom cursor in the Launcher like in the lazer build of osu!
</div>
</div>
<Checkbox
id="setting-custom-cursor"
checked={$customCursor}
onCheckedChange={async (e) => {
if (!e) {
cursorSmoothening.set(false);
}
customCursor.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-cursor-smoothening">Cursor Smoothening</Label>
<div class="text-muted-foreground text-xs">
Makes the custom cursor movement smoother.
</div>
</div>
<Checkbox
id="setting-cursor-smoothening"
checked={$cursorSmoothening}
onCheckedChange={async (e) => {
if (!$customCursor) return;
cursorSmoothening.set(e);
$userSettings.save();
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-reduce-animations">Reduce Animations</Label>
<div class="text-muted-foreground text-xs">
Disables some animations in the Launcher to improve performance on low-end
devices.
</div>
</div>
<Checkbox
id="setting-reduce-animations"
checked={$reduceAnimations}
onCheckedChange={async (e) => {
reduceAnimations.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-rich-presence">Discord Rich Presence</Label>
<div class="text-muted-foreground text-xs">
Let other discord users show what you are doing right now 👀
</div>
</div>
<div class="relative">
{#if $presenceLoading}
<div class="-left-8 absolute" transition:fade>
<LoaderCircle class="animate-spin" />
</div>
{/if}
<Checkbox
id="setting-rich-presence"
bind:checked={$discordPresence}
disabled={$presenceLoading}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div>
<div class="flex flex-col">
<Label class="text-sm" for="setting-tracking">App Tracking</Label>
<div class="text-muted-foreground text-xs">
Allow anonymous usage data to be collected to help improve the application.
</div>
</div>
<Checkbox
id="setting-tracking"
checked={$trackingEnabled}
onCheckedChange={async (e) => {
trackingEnabled.set(e);
$userSettings.value('tracking_consent').set(e);
await $userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div> </div>
<div </div>
class="grid grid-cols-[0.7fr_auto] gap-y-1 items-center border-theme-800 pl-6 pr-5 pb-4" <Checkbox
id="setting-patch"
checked={$platform === 'windows' ? $patch : false}
disabled={$platform !== 'windows'}
onCheckedChange={async (e) => {
patch.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Lazer-Style Cursor</Label>
<div class="text-muted-foreground text-xs">
Enable a custom cursor in the Launcher like in the lazer build of osu!
</div>
</div>
<Checkbox
id="setting-custom-cursor"
checked={$customCursor}
onCheckedChange={async (e) => {
if (!e) {
cursorSmoothening.set(false);
}
customCursor.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-cursor-smoothening">Cursor Smoothening</Label>
<div class="text-muted-foreground text-xs">
Makes the custom cursor movement smoother.
</div>
</div>
<Checkbox
id="setting-cursor-smoothening"
checked={$cursorSmoothening}
onCheckedChange={async (e) => {
if (!$customCursor) return;
cursorSmoothening.set(e);
$userSettings.save();
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-reduce-animations">Reduce Animations</Label>
<div class="text-muted-foreground text-xs">
Disables some animations in the Launcher to improve performance on low-end devices.
</div>
</div>
<Checkbox
id="setting-reduce-animations"
checked={$reduceAnimations}
onCheckedChange={async (e) => {
reduceAnimations.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-rich-presence">Discord Rich Presence</Label>
<div class="text-muted-foreground text-xs">
Let other discord users show what you are doing right now 👀
</div>
</div>
<div class="relative">
{#if $presenceLoading}
<div class="-left-8 absolute" transition:fade>
<LoaderCircle class="animate-spin" />
</div>
{/if}
<Checkbox
id="setting-rich-presence"
bind:checked={$discordPresence}
disabled={$presenceLoading}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div>
<div class="flex flex-col">
<Label class="text-sm" for="setting-tracking">App Tracking</Label>
<div class="text-muted-foreground text-xs">
Allow anonymous usage data to be collected to help improve the application.
</div>
</div>
<Checkbox
id="setting-tracking"
checked={$trackingEnabled}
onCheckedChange={async (e) => {
trackingEnabled.set(e);
$userSettings.value('tracking_consent').set(e);
await $userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div>
<div
class="grid grid-cols-[0.7fr_auto] gap-y-1 mt-3 w-full items-center border-theme-800 pl-6 pr-5 pb-4"
>
<div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">osu! installation path</Label>
<div class="text-muted-foreground text-xs">The path to your osu! installation.</div>
</div>
<div class="flex flex-row w-full">
<Input
class="mt-4 w-full bg-theme-950 border-theme-800 border-r-0 rounded-r-none"
type="text"
value={$osuInstallationPath}
placeholder="Path to osu! installation"
readonly
/>
<Button
class="mt-4 bg-theme-950 border-theme-800 rounded-l-none"
variant="outline"
onclick={browse_osu_installation}>Browse</Button
> >
<div class="flex flex-col"> </div>
<Label class="text-sm" for="setting-custom-cursor">osu! installation path</Label>
<div class="text-muted-foreground text-xs">The path to your osu! installation.</div>
</div>
<div class="flex flex-row w-full">
<Input
class="mt-4 w-full bg-theme-950 border-theme-800 border-r-0 rounded-r-none"
type="text"
value={$osuInstallationPath}
placeholder="Path to osu! installation"
readonly
/>
<Button
class="mt-4 bg-theme-950 border-theme-800 rounded-l-none"
variant="outline"
onclick={browse_osu_installation}>Browse</Button
>
</div>
<div class="flex flex-col"> <div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">patcher release stream</Label> <Label class="text-sm" for="setting-custom-cursor">patcher release stream</Label>
<div class="text-muted-foreground text-xs"> <div class="text-muted-foreground text-xs">test different versions of the patcher</div>
test different versions of the patcher </div>
<div class="flex flex-row w-full">
<Select.Root
type="single"
bind:value={$launcherStream}
onValueChange={async (newStream) => {
const isNet8Installed = await hasNet8();
if (!isNet8Installed) {
launcherStream.set('stable');
sileo.error({
title: 'Hmm...',
description: '.NET 8.0 Desktop Runtime not found!',
fill: '#181825',
styles: {
description: 'text-center!',
},
button: {
title: 'Download .NET 8.0',
onClick: async () =>
await openURL(
'https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.22-windows-x64-installer'
),
},
});
return;
}
$userSettings.value('patcherStream').set(newStream);
launcherStream.set(newStream);
await $userSettings.save();
}}
>
<Select.Trigger
class="border-theme-800 bg-theme-950 !text-muted-foreground font-semibold"
>
<div class="flex flex-row items-center gap-2 font-normal text-foreground">
{$launcherStream}
</div> </div>
</div> </Select.Trigger>
<div class="flex flex-row w-full"> <Select.Content class="bg-theme-950 border border-theme-950 rounded-lg">
<Select.Root {#each $launcherStreams as stream (stream)}
type="single" <Select.Item value={stream}>
bind:value={$launcherStream} <div class="flex flex-row gap-2 items-center">
onValueChange={async (newStream) => { {stream}
const isNet8Installed = await hasNet8();
if (!isNet8Installed) {
launcherStream.set('stable');
sileo.error({
title: 'Hmm...',
description: '.NET 8.0 Desktop Runtime not found!',
fill: '#181825',
styles: {
description: 'text-center!',
},
button: {
title: 'Download .NET 8.0',
onClick: async () =>
await openURL(
'https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.22-windows-x64-installer'
),
},
});
return;
}
$userSettings.value('patcherStream').set(newStream);
launcherStream.set(newStream);
await $userSettings.save();
}}
>
<Select.Trigger
class="border-theme-800 bg-theme-950 !text-muted-foreground font-semibold"
>
<div class="flex flex-row items-center gap-2 font-normal text-foreground">
{$launcherStream}
</div> </div>
</Select.Trigger> </Select.Item>
<Select.Content class="bg-theme-950 border border-theme-950 rounded-lg"> {/each}
{#each $launcherStreams as stream (stream)} </Select.Content>
<Select.Item value={stream}> </Select.Root>
<div class="flex flex-row gap-2 items-center">
{stream}
</div>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{:else if selectedView === 'login'} {:else if selectedView === 'login'}
<div <div
class="h-[100vh] w-full flex flex-col items-center justify-center" class="h-[100vh] w-full flex flex-col items-center justify-center bg-black/20 backdrop-blur-sm"
in:fly={{ duration: 400, delay: 400, y: 5, opacity: 0 }} in:fly={{
out:fly={{ duration: 400, y: -5, opacity: 0 }} duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 400,
y: 5,
opacity: 0,
}}
out:fly={{ duration: $reduceAnimations ? 0 : 400, y: -5, opacity: 0 }}
> >
<div class="p-8 px-52 w-full"> <form onsubmit={performLogin} class="w-96">
<form onsubmit={performLogin} class="bg-black/40 backdrop-blur-sm rounded-lg p-8"> <div class="flex flex-col items-center justify-center mb-8">
<div class="flex flex-col items-center justify-center mb-4"> <span class="text-xl font-semibold">Login to EZPPFarm</span>
<span class="text-xl font-semibold">Login to EZPPFarm</span> <span class="text-xs text-muted-foreground"
<span class="text-xs text-muted-foreground" >Use your EZPPFarm account to login to EZPPLauncher</span
>Use your EZPPFarm account to login to EZPPLauncher</span >
</div>
<div class="mb-4">
<Label for="username" class="block text-sm font-medium">Username</Label>
<Input
class="mt-4 w-full bg-theme-900 border-theme-800"
type="text"
id="username"
bind:value={username}
disabled={loginIsLoading}
autocomplete="off"
autocorrect="off"
/>
</div>
<div class="mb-4">
<Label for="password" class="block text-sm font-medium">Password</Label>
<Input
class="mt-4 w-full bg-theme-900 border-theme-800"
type="password"
id="password"
bind:value={password}
disabled={loginIsLoading}
autocomplete="off"
autocorrect="off"
/>
</div>
<Button class="w-full" type="submit" disabled={loginIsLoading}>
{#if loginIsLoading}
<LoaderCircle class="animate-spin" />
{:else}
Login
{/if}
</Button>
</form>
</div>
{:else if selectedView === 'skins'}
<div
class="h-[100vh] w-full flex flex-col items-center justify-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 }}
>
{#if $skins.length > 0}
{@const skinsSorted = $skins.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })
)}
<ScrollContainer
class="flex flex-col max-h-[100vh] overflow-y-auto items-center gap-4 p-16 w-full"
topOffset={35}
>
{#each skinsSorted as skin (skin.name)}
<div
class={`group w-full flex items-center border rounded-lg transition-all duration-150 ${
skin.name === $currentSkin
? 'bg-primary/20 border-primary/40'
: 'bg-black/40 border-black/60 hover:bg-black/70 hover:border-border'
}`}
> >
</div> <div class="flex-1 min-w-0 px-5 py-3.5">
<div class="mb-4"> <p class="text-sm text-foreground/90 truncate font-medium line-clamp-1">
<Label for="username" class="block text-sm font-medium">Username</Label> {skin.name}
<Input </p>
class="mt-4 w-full bg-theme-900 border-theme-800" <div class="flex items-center gap-4 mt-1">
type="text" <span class="text-[11px] text-muted-foreground">
id="username" {skin.author || 'Unknown'}
bind:value={username} </span>
disabled={loginIsLoading} <span class="text-[11px] text-muted-foreground/50">
autocomplete="off" {new Date(skin.modified * 1000).toLocaleDateString(undefined, {
autocorrect="off" year: '2-digit',
/> month: 'short',
</div> day: 'numeric',
<div class="mb-4"> })}
<Label for="password" class="block text-sm font-medium">Password</Label> </span>
<Input </div>
class="mt-4 w-full bg-theme-900 border-theme-800" </div>
type="password"
id="password" <div
bind:value={password} class="flex items-center gap-1.5 pr-3 opacity-0 group-hover:opacity-100 transition-opacity duration-150"
disabled={loginIsLoading} >
autocomplete="off" {#if $currentSkin !== skin.name}
autocorrect="off" <button
/> transition:fade={{
</div> duration: 150,
<Button class="w-full" type="submit" disabled={loginIsLoading}> }}
{#if loginIsLoading} class="p-2 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
<LoaderCircle class="animate-spin" /> onclick={async () => {
{:else} if (skin.name === $currentSkin) return;
Login await setUserConfigValues($osuInstallationPath, [
{/if} { key: 'Skin', value: skin.name },
</Button> ]);
</form> currentSkin.set(skin.name);
</div> sileo.success({
title: 'Skin applied!',
description: `${skin.name} is now your active skin!`,
fill: '#181825',
styles: {
description: 'text-center!',
},
});
}}
>
<Check class="w-4 h-4" />
</button>
{/if}
<!-- <button
class="p-2 rounded-md transition-colors text-muted-foreground hover:text-destructive hover:bg-destructive/10"
>
<Trash2 class="w-4 h-4" />
</button> -->
</div>
</div>
{/each}
</ScrollContainer>
{:else}
<div class="flex flex-col items-center justify-center gap-4">
<FileQuestionMark class="size-12 text-muted-foreground" strokeWidth={1.5} />
<span class="text-sm text-muted-foreground">No skins found!</span>
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
<style lang="scss">
.custom-scrollbox {
scrollbar-color: #999 transparent;
scrollbar-width: 0px !important;
}
</style>
+11 -3
View File
@@ -14,6 +14,7 @@
osuBuild, osuBuild,
osuStream, osuStream,
skins, skins,
skinsCount,
} from '@/global'; } from '@/global';
import { import {
cursorSmoothness, cursorSmoothness,
@@ -34,7 +35,7 @@
getEZPPLauncherStreams, getEZPPLauncherStreams,
getReleaseStream, getReleaseStream,
getSkin, getSkin,
getSkinsCount, getSkins,
getVersion, getVersion,
isValidOsuFolder, isValidOsuFolder,
} from '@/osuUtil'; } from '@/osuUtil';
@@ -151,8 +152,15 @@
if (beatmapSetCount !== null) beatmapSets.set(beatmapSetCount); if (beatmapSetCount !== null) beatmapSets.set(beatmapSetCount);
currentLoadingInfo.set('Counting skins...'); currentLoadingInfo.set('Counting skins...');
const skinCount = await getSkinsCount($osuInstallationPath); const skins_list = await getSkins($osuInstallationPath);
if (skinCount !== null) skins.set(skinCount); console.log(skins_list);
if (skins_list) {
skins.set(skins_list);
skinsCount.set(skins_list.length);
} else {
skinsCount.set(0);
}
const skin: string = await getSkin($osuInstallationPath); const skin: string = await getSkin($osuInstallationPath);
currentSkin.set(skin); currentSkin.set(skin);
} }
+135 -90
View File
@@ -5,7 +5,7 @@
import { animate } from 'animejs'; import { animate } from 'animejs';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { Check, CheckCircle, CircleOff, Settings2 } from 'lucide-svelte'; import { Check, CircleCheckBig, CircleOff, LoaderCircle } from 'lucide-svelte';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import Checkbox from '@/components/ui/checkbox/checkbox.svelte'; import Checkbox from '@/components/ui/checkbox/checkbox.svelte';
import { import {
@@ -17,7 +17,18 @@
userSettings, userSettings,
} from '@/userSettings'; } from '@/userSettings';
import Label from '@/components/ui/label/label.svelte'; import Label from '@/components/ui/label/label.svelte';
import { beatmapSets, currentSkin, currentView, osuBuild, osuStream, skins } from '@/global'; import {
beatmapSets,
currentSkin,
currentView,
discordPresence,
osuBuild,
osuStream,
platform,
presenceLoading,
skins,
skinsCount,
} from '@/global';
import Launch from './Launch.svelte'; import Launch from './Launch.svelte';
import Confetti from 'svelte-confetti'; import Confetti from 'svelte-confetti';
import { import {
@@ -25,7 +36,7 @@
getBeatmapSetsCount, getBeatmapSetsCount,
getReleaseStream, getReleaseStream,
getSkin, getSkin,
getSkinsCount, getSkins,
getVersion, getVersion,
isValidOsuFolder, isValidOsuFolder,
} from '@/osuUtil'; } from '@/osuUtil';
@@ -92,9 +103,12 @@
beatmapSets.set(beatmapSetCount); beatmapSets.set(beatmapSetCount);
} }
const skinsCount: number | null = await getSkinsCount(osuInstallPath); const skins_list = await getSkins(osuInstallPath);
if (skinsCount) { if (skins_list) {
skins.set(skinsCount); skins.set(skins_list);
skinsCount.set(skins_list.length);
} else {
skinsCount.set(0);
} }
const skin: string = await getSkin(osuInstallPath); const skin: string = await getSkin(osuInstallPath);
@@ -110,6 +124,7 @@
onMount(async () => { onMount(async () => {
const osuPath = await autoDetectOsuInstallFolder(); const osuPath = await autoDetectOsuInstallFolder();
console.log(osuPath);
if (osuPath) { if (osuPath) {
osuInstallPath = osuPath; osuInstallPath = osuPath;
autoDetectedOsuPath = true; autoDetectedOsuPath = true;
@@ -119,7 +134,7 @@
</script> </script>
{#if wizardFinished} {#if wizardFinished}
<div class="relative flex flex-col items-center justify-center h-[100vh]"> <div class="relative flex flex-col items-center justify-center h-[100vh] z-50">
<div class="absolute h-fit w-fit top-1/2 left-1/2"> <div class="absolute h-fit w-fit top-1/2 left-1/2">
<Confetti amount={200} y={[-1, 1.5]} x={[-2.3, 2.3]} colorArray={['#C6A0F3']} /> <Confetti amount={200} y={[-1, 1.5]} x={[-2.3, 2.3]} colorArray={['#C6A0F3']} />
</div> </div>
@@ -128,15 +143,17 @@
<Button class="mt-4" onclick={saveConfig}>Finish</Button> <Button class="mt-4" onclick={saveConfig}>Finish</Button>
</div> </div>
{:else} {:else}
<div class="grid grid-cols-[0.41fr_1fr] h-[100vh]"> <div class="grid grid-cols-[0.41fr_1fr] h-[100vh] z-50">
<div class="w-full h-full border-r border-theme-800/90 flex flex-col gap-6 p-3"> <div
class="w-full h-full border-r border-black/40 bg-black/40 backdrop-blur-md flex flex-col gap-6 p-3 z-50"
>
{#each steps as step, i (step)} {#each steps as step, i (step)}
<div <div
class="flex flex-row items-center gap-2 border {selectedStep === i + 1 class="flex flex-row items-center gap-2 border {selectedStep === i + 1
? 'border-primary-800/30 bg-primary-900/30' ? 'border-primary-600/30 bg-primary-700/30'
: selectedStep > i : selectedStep > i
? 'border-green-800/30 bg-green-900/30' ? 'border-green-800/30 bg-green-900/30'
: 'border-theme-800 bg-theme-900'} rounded-lg p-2 transition-all" : 'border-black/30 bg-black/30'} rounded-lg p-2 transition-all"
> >
<div <div
class="flex flex-col items-center justify-center h-8 w-8 border-[2px] {selectedStep > class="flex flex-col items-center justify-center h-8 w-8 border-[2px] {selectedStep >
@@ -160,10 +177,10 @@
</div> </div>
{/each} {/each}
</div> </div>
<div class="flex flex-col gap-6 w-full h-full bg-theme-900/40 p-6"> <div class="flex flex-col gap-6 w-full h-full backdrop-blur-md p-6 z-50">
{#if selectedStep === 1} {#if selectedStep === 1}
<div <div
class="my-auto h-full w-full bg-theme-800/15 rounded-lg border border-900/60 p-6 mt-5 flex flex-col items-center justify-center" class="my-auto h-full w-full bg-black/40 rounded-lg border border-black/40 p-6 mt-5 flex flex-col items-center justify-center"
in:fade={{ duration: $reduceAnimations ? 0 : 200 }} in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
> >
<img <img
@@ -178,13 +195,13 @@
<p class="text-muted-foreground mt-2"> <p class="text-muted-foreground mt-2">
This setup wizard will guide you through the initial setup of EZPPLauncher. This setup wizard will guide you through the initial setup of EZPPLauncher.
</p> </p>
<div class="bg-red-800/20 border border-red-900/20 text-red-500 p-4 rounded-lg mt-4"> <div class="bg-red-800 border border-red-900 text-red-200 p-4 rounded-lg mt-4">
Please make sure you have osu! installed on your system before proceeding. Please make sure you have osu! installed on your system before proceeding.
</div> </div>
</div> </div>
{:else if selectedStep === 2} {:else if selectedStep === 2}
<div <div
class="my-auto h-full w-full bg-theme-800/15 rounded-lg border border-900/60 p-6 mt-5 flex flex-col items-center justify-center" class="my-auto h-full w-full bg-black/40 rounded-lg border border-black/40 p-6 mt-5 flex flex-col items-center justify-center"
in:fade={{ duration: $reduceAnimations ? 0 : 200 }} in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
> >
<h1 class="text-3xl font-semibold">Locate your osu! Installation</h1> <h1 class="text-3xl font-semibold">Locate your osu! Installation</h1>
@@ -207,14 +224,14 @@
{#if !manualSelect} {#if !manualSelect}
{#if autoDetectedOsuPath} {#if autoDetectedOsuPath}
<div <div
class="flex flex-row gap-3 bg-green-800/20 border border-green-900/20 text-green-500 p-4 rounded-lg mt-4" class="flex flex-row gap-3 bg-green-800 border border-green-900 text-green-500 p-4 rounded-lg mt-4"
> >
<CheckCircle /> <CircleCheckBig />
<span>Auto-detected osu! installation path! Please check if its correct!</span> <span>Auto-detected osu! installation path! Please check if its correct!</span>
</div> </div>
{:else} {:else}
<div <div
class="flex flex-row gap-3 bg-red-800/20 border border-red-900/20 text-red-500 p-4 rounded-lg mt-4" class="flex flex-row gap-3 bg-red-800 border border-red-900 text-red-500 p-4 rounded-lg mt-4"
> >
<CircleOff /> <CircleOff />
<span>Could not auto-detect osu! installation path. Please select it manually.</span <span>Could not auto-detect osu! installation path. Please select it manually.</span
@@ -223,14 +240,14 @@
{/if} {/if}
{:else if manualSelectValid} {:else if manualSelectValid}
<div <div
class="flex flex-row gap-3 bg-green-800/20 border border-green-900/20 text-green-500 p-4 rounded-lg mt-4" class="flex flex-row gap-3 bg-green-800 border border-green-900 text-green-500 p-4 rounded-lg mt-4"
> >
<CheckCircle /> <CircleCheckBig />
<span>Selected osu! installation path is valid!</span> <span>Selected osu! installation path is valid!</span>
</div> </div>
{:else} {:else}
<div <div
class="flex flex-row gap-3 bg-red-800/20 border border-red-900/20 text-red-500 p-4 rounded-lg mt-4" class="flex flex-row gap-3 bg-red-800 border border-red-900 text-red-500 p-4 rounded-lg mt-4"
> >
<CircleOff /> <CircleOff />
<span <span
@@ -241,80 +258,108 @@
{/if} {/if}
</div> </div>
{:else if selectedStep === 3} {:else if selectedStep === 3}
<div <div class="h-full w-full flex items-center justify-center">
class="bg-theme-900/90 flex flex-col justify-center gap-3 mt-5 border border-theme-800/90 rounded-lg"
in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
>
<div class="flex flex-row items-center gap-3 font-semibold text-xl px-3 pt-3">
<Settings2 /> EZPPLauncher Settings
</div>
<div <div
class="grid grid-cols-[1fr_auto] gap-y-5 items-center border-t border-theme-800 py-3 px-6" class="bg-black/40 backdrop-blur-sm py-8 px-8 rounded-lg"
in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
> >
<div class="flex flex-col"> <div class="grid grid-cols-[1fr_auto] gap-y-5 items-center px-6">
<Label class="text-sm" for="setting-custom-cursor">Patching</Label> <div class="flex flex-col">
<div class="text-muted-foreground text-xs">Shows misses in Relax and Autopilot</div> <Label class="text-sm" for="setting-patch">Patching</Label>
</div> <div class="text-muted-foreground text-xs">
<Checkbox Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span
id="setting-custom-cursor" class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg !text-[0.55rem]"
checked={$patch} >currently only on windows!</span
onCheckedChange={async (e) => { >
patch.set(e); {/if}
$userSettings.save(); </div>
}} </div>
class="flex items-center justify-center w-5 h-5" <Checkbox
></Checkbox> id="setting-patch"
checked={$platform === 'windows' ? $patch : false}
disabled={$platform !== 'windows'}
onCheckedChange={async (e) => {
patch.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col"> <div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Lazer-Style Cursor</Label> <Label class="text-sm" for="setting-custom-cursor">Lazer-Style Cursor</Label>
<div class="text-muted-foreground text-xs"> <div class="text-muted-foreground text-xs">
Enable a custom cursor in the Launcher like in the lazer build of osu! Enable a custom cursor in the Launcher like in the lazer build of osu!
</div>
</div>
<Checkbox
id="setting-custom-cursor"
checked={$customCursor}
onCheckedChange={async (e) => {
if (!e) {
cursorSmoothening.set(false);
}
customCursor.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-cursor-smoothening">Cursor Smoothening</Label>
<div class="text-muted-foreground text-xs">
Makes the custom cursor movement smoother.
</div>
</div>
<Checkbox
id="setting-cursor-smoothening"
checked={$cursorSmoothening}
onCheckedChange={async (e) => {
if (!$customCursor) return;
cursorSmoothening.set(e);
$userSettings.save();
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-reduce-animations">Reduce Animations</Label>
<div class="text-muted-foreground text-xs">
Disables some animations in the Launcher to improve performance on low-end
devices.
</div>
</div>
<Checkbox
id="setting-reduce-animations"
checked={$reduceAnimations}
onCheckedChange={async (e) => {
reduceAnimations.set(e);
$userSettings.save();
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-rich-presence">Discord Rich Presence</Label>
<div class="text-muted-foreground text-xs">
Let other discord users show what you are doing right now 👀
</div>
</div>
<div class="relative">
{#if $presenceLoading}
<div class="-left-8 absolute" transition:fade>
<LoaderCircle class="animate-spin" />
</div>
{/if}
<Checkbox
id="setting-rich-presence"
bind:checked={$discordPresence}
disabled={$presenceLoading}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div> </div>
</div> </div>
<Checkbox
id="setting-custom-cursor"
checked={$customCursor}
onCheckedChange={async (e) => {
if (!e) {
cursorSmoothening.set(false);
}
customCursor.set(e);
}}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-cursor-smoothening">Cursor Smoothening</Label>
<div class="text-muted-foreground text-xs">
Makes the custom cursor movement smoother.
</div>
</div>
<Checkbox
id="setting-cursor-smoothening"
checked={$cursorSmoothening}
onCheckedChange={async (e) => {
if (!$customCursor) return;
cursorSmoothening.set(e);
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
<div class="flex flex-col">
<Label class="text-sm" for="setting-cursor-smoothening">Reduce Animations</Label>
<div class="text-muted-foreground text-xs">
Disables some animations in the Launcher to improve performance on low-end devices.
</div>
</div>
<Checkbox
id="setting-cursor-smoothening"
checked={$reduceAnimations}
onCheckedChange={async (e) => {
reduceAnimations.set(e);
}}
disabled={!$customCursor}
class="flex items-center justify-center w-5 h-5"
></Checkbox>
</div> </div>
</div> </div>
{/if} {/if}