Compare commits

..

26 Commits

Author SHA1 Message Date
HorizonCode d9dd21d3be chore: cleanup commands, fix osu folder check 2026-02-16 20:46:58 +01:00
HorizonCode 0df31178f4 fix: make count functions async to prevent frontend lock 2026-02-16 20:20:29 +01:00
HorizonCode 6825f9e99b fix: login box width, update releasestream and version 2026-02-15 19:08:04 +01:00
HorizonCode 08a8e34781 chore: add launch button states 2026-02-15 19:02:07 +01:00
HorizonCode 1aa973f6bb chore: bump version 2026-02-15 18:57:38 +01:00
HorizonCode 0999832985 fix: titlebar margin 2026-02-15 18:57:30 +01:00
HorizonCode 11798318f4 chore: only display user stats when logged in 2026-02-15 18:57:17 +01:00
HorizonCode f1b909e248 chore: make pp value better visible 2026-02-15 18:43:51 +01:00
HorizonCode 1f845f1282 chore: rework launcher design 2026-02-15 18:42:01 +01:00
HorizonCode f9b38ec9eb fix: improve error handling for network connection launch failures 2026-01-04 20:20:19 +01:00
HorizonCode 623559cf8e chore: update syn and other dependencies to latest versions 2026-01-04 20:08:26 +01:00
HorizonCode 80bd096834 chore: bump version 2026-01-04 19:36:57 +01:00
HorizonCode e1c6e35f95 chore: refactor version syncing logic in sync-version.ts 2026-01-04 19:36:39 +01:00
HorizonCode 57158bab15 chore: force dark theme to toast component 2026-01-04 19:27:01 +01:00
HorizonCode 58837053c7 chore: update button text color to improve visibility 2026-01-04 19:26:42 +01:00
HorizonCode 458a2cca36 chore: update @lucide/svelte dependency 2026-01-04 17:16:39 +01:00
HorizonCode cb66df6082 chore: update text color for user name display 2026-01-04 17:16:23 +01:00
HorizonCode 1d1904e995 chore: update interactive role checks in OsuCursor component 2026-01-04 17:16:09 +01:00
HorizonCode c07e390b65 chore: redesign user display, remove tabs switch from main view 2026-01-04 17:03:22 +01:00
HorizonCode bae0fad19b chore: update rust and bun dependencies to latest versions 2026-01-02 22:02:40 +01:00
HorizonCode 993eb167fc chore: bump version for tauri too .-. 2025-12-22 22:31:18 +01:00
HorizonCode ea2e348ccc chore: bump version for tauri backend too 2025-12-22 22:28:41 +01:00
HorizonCode 2855880c18 chore: bump version 2025-12-22 22:27:46 +01:00
HorizonCode dcb57c5452 chore: add seasonal assets 2025-12-22 22:25:18 +01:00
HorizonCode c9def2c46b chore: bump version to 3.0.8 2025-12-07 16:22:29 +01:00
HorizonCode b8ebcf5509 feat: conditionally track app launch, exit, and failure events based on trackingEnabled state 2025-12-07 16:21:21 +01:00
24 changed files with 2016 additions and 1993 deletions
+1
View File
@@ -43,6 +43,7 @@ It enhances the osu! experience with quality-of-life features and integration sp
- **Rust** (installed via [rustup](https://rustup.rs/) – recommended for Windows) - **Rust** (installed via [rustup](https://rustup.rs/) – recommended for Windows)
- **Cargo** (comes with rustup) - **Cargo** (comes with rustup)
- **[CMake](https://cmake.org/download/)**
- **[Bun](https://bun.sh/)** (JavaScript runtime) - **[Bun](https://bun.sh/)** (JavaScript runtime)
### Steps ### Steps
+44 -40
View File
@@ -10,38 +10,38 @@
"@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.8.0", "@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-shell": "^2.3.1", "@tauri-apps/plugin-shell": "^2.3.3",
"@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.542.0", "lucide-svelte": "^0.562.0",
"semver": "^7.7.2", "semver": "^7.7.2",
"svelte-confetti": "^2.3.2", "svelte-confetti": "^2.3.2",
"tw-animate-css": "^1.3.8", "tw-animate-css": "^1.3.8",
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.3.2", "@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.542.0", "@lucide/svelte": "^0.482.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.8.4", "@tauri-apps/cli": "^2.9.6",
"@types/bun": "^1.2.21", "@types/bun": "^1.2.21",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"bits-ui": "^2.9.6", "bits-ui": "^1.4.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.35.0", "eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.2", "eslint-plugin-svelte": "^3.12.2",
"globals": "^16.3.0", "globals": "^17.0.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
@@ -57,7 +57,7 @@
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.42.0",
"vite": "^7.1.5", "vite": "^7.1.5",
"vite-plugin-devtools-json": "^0.4.1", "vite-plugin-devtools-json": "^1.0.0",
}, },
}, },
}, },
@@ -122,13 +122,13 @@
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/compat": ["@eslint/compat@1.3.2", "", { "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA=="], "@eslint/compat": ["@eslint/compat@2.0.0", "", { "dependencies": { "@eslint/core": "^1.0.0" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA=="],
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], "@eslint/core": ["@eslint/core@1.0.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
@@ -174,7 +174,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.542.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-NuWttxTVfMSURpOxcKiKvoCtma3JtEpcJWzF/0cO69saZfXlv6G8NYAvEEGLmk75YPl+I+ROe+F97WhddM8r2A=="], "@lucide/svelte": ["@lucide/svelte@0.482.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-n2ycHU9cNcleRDwwpEHBJ6pYzVhHIaL3a+9dQa8kns9hB2g05bY+v2p2KP8v0pZwtNhYTHk/F2o2uZ1bVtQGhw=="],
"@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=="],
@@ -300,37 +300,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.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], "@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.8.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.8.4", "@tauri-apps/cli-darwin-x64": "2.8.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.8.4", "@tauri-apps/cli-linux-arm64-gnu": "2.8.4", "@tauri-apps/cli-linux-arm64-musl": "2.8.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-gnu": "2.8.4", "@tauri-apps/cli-linux-x64-musl": "2.8.4", "@tauri-apps/cli-win32-arm64-msvc": "2.8.4", "@tauri-apps/cli-win32-ia32-msvc": "2.8.4", "@tauri-apps/cli-win32-x64-msvc": "2.8.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g=="], "@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-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.8.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA=="], "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.8.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g=="], "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.8.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw=="], "@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-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw=="], "@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-musl": ["@tauri-apps/cli-linux-arm64-musl@2.8.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw=="], "@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-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.8.4", "", { "os": "linux", "cpu": "none" }, "sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ=="], "@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-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw=="], "@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-musl": ["@tauri-apps/cli-linux-x64-musl@2.8.4", "", { "os": "linux", "cpu": "x64" }, "sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA=="], "@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-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.8.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w=="], "@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-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.8.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA=="], "@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-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.8.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA=="], "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w=="], "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig=="], "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ=="],
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-jjs2WGDO/9z2pjNlydY/F5yYhNsscv99K5lCmU5uKjsVvQ3dRlDhhtVYoa4OLDmktLtQvgvbQjCFibMl6tgGfw=="], "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
"@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=="],
@@ -392,7 +392,7 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bits-ui": ["bits-ui@2.9.6", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.29.1", "svelte-toolbelt": "^0.9.3", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-OzHktsQRsIz/hIMk5VwHo96Wpp/KY68q/ebUPUzTbvuFBrALB/X+QvO4KLgdczj5dfb3xHs9zpWq8yMH8ZbZlA=="], "bits-ui": ["bits-ui@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", "tabbable": "^6.2.0" }, "peerDependencies": { "svelte": "^5.11.0" } }, "sha512-CXD6Orp7l8QevNDcRPLXc/b8iMVgxDWT2LyTwsdLzJKh9CxesOmPuNePSPqAxKoT59FIdU4aFPS1k7eBdbaCxg=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -432,6 +432,8 @@
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@@ -508,7 +510,7 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], "globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -590,7 +592,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.542.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-KxqJycY4EWaGy1zk/7sqEcf48j4YaP9zEujYczzrsw5j9b9b5pjAJ1qGFKZvD7T6xz9reSYAfUAd6Bz62TdqGw=="], "lucide-svelte": ["lucide-svelte@0.562.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA=="],
"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=="],
@@ -674,7 +676,7 @@
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="], "runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
@@ -748,7 +750,7 @@
"svelte-sonner": ["svelte-sonner@1.0.5", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg=="], "svelte-sonner": ["svelte-sonner@1.0.5", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg=="],
"svelte-toolbelt": ["svelte-toolbelt@0.9.3", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="], "svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="], "sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="],
@@ -800,7 +802,7 @@
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="], "vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
"vite-plugin-devtools-json": ["vite-plugin-devtools-json@0.4.1", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-pN+QJL+NwZUV+Via8w/Sh6X2pDrVClIMDAXdl7+EteXKB6mcHhsFGGclmxrPx6ZPGKSK5ez5ns64oRpjE5wFCg=="], "vite-plugin-devtools-json": ["vite-plugin-devtools-json@1.0.0", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
@@ -820,6 +822,8 @@
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
@@ -842,16 +846,18 @@
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "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=="],
"eslint-plugin-svelte/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], "eslint-plugin-svelte/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"eslint-plugin-svelte/globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="], "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], "svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], "svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
@@ -863,7 +869,5 @@
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"eslint-plugin-svelte/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "eslint-plugin-svelte/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
} }
} }
+12 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "ezpplauncher", "name": "ezpplauncher",
"version": "3.0.7", "version": "3.1.0",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -24,38 +24,38 @@
"@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.8.0", "@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-dialog": "^2.4.2",
"@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-shell": "^2.3.1", "@tauri-apps/plugin-shell": "^2.3.3",
"@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.542.0", "lucide-svelte": "^0.562.0",
"semver": "^7.7.2", "semver": "^7.7.2",
"svelte-confetti": "^2.3.2", "svelte-confetti": "^2.3.2",
"tw-animate-css": "^1.3.8" "tw-animate-css": "^1.3.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.3.2", "@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.542.0", "@lucide/svelte": "^0.562.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.8.4", "@tauri-apps/cli": "^2.9.6",
"@types/bun": "^1.2.21", "@types/bun": "^1.2.21",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"bits-ui": "^2.9.6", "bits-ui": "^1.4.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.35.0", "eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.2", "eslint-plugin-svelte": "^3.12.2",
"globals": "^16.3.0", "globals": "^17.0.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
@@ -71,6 +71,6 @@
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.42.0",
"vite": "^7.1.5", "vite": "^7.1.5",
"vite-plugin-devtools-json": "^0.4.1" "vite-plugin-devtools-json": "^1.0.0"
} }
} }
+12 -5
View File
@@ -3,19 +3,26 @@ import path from 'path';
import * as toml from '@iarna/toml'; import * as toml from '@iarna/toml';
import { cwd } from 'process'; import { cwd } from 'process';
const packageJsonPath = path.join(cwd(), 'package.json');
const cargoTomlPath = path.join(cwd(), 'src-tauri', 'Cargo.toml'); const cargoTomlPath = path.join(cwd(), 'src-tauri', 'Cargo.toml');
const tauriConfPath = path.join(cwd(), 'src-tauri', 'tauri.conf.json'); const tauriConfPath = path.join(cwd(), 'src-tauri', 'tauri.conf.json');
const cargoToml = fs.readFileSync(cargoTomlPath, 'utf8'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const cargo = toml.parse(cargoToml) as Record<string, string>; const version = packageJson.version;
if (!version) throw new Error('Could not find version in package.json');
const version = cargo.package['version']; let cargoTomlContent = fs.readFileSync(cargoTomlPath, 'utf8');
if (!version) throw new Error('Could not find version in Cargo.toml'); const cargo = toml.parse(cargoTomlContent) as { package: { version: string } };
if (cargo.package.version !== version) {
cargoTomlContent = cargoTomlContent.replace(/version = ".*?"/, `version = "${version}"`);
fs.writeFileSync(cargoTomlPath, cargoTomlContent);
console.log(`✅ Synced Cargo.toml version to ${version}`);
}
const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8')); const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
if (tauriConf.version !== version) { if (tauriConf.version !== version) {
tauriConf.version = version; tauriConf.version = version;
fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2)); fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2));
console.log(`✅ Synced tauri.conf.json version to ${version}`); console.log(`✅ Synced tauri.conf.json version to ${version}`);
} }
+967 -1016
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ezpplauncher" name = "ezpplauncher"
version = "3.0.7" version = "3.1.0"
description = "EZPPLauncher redefined." description = "EZPPLauncher redefined."
authors = ["HorizonCode"] authors = ["HorizonCode"]
edition = "2024" edition = "2024"
@@ -15,24 +15,24 @@ name = "ezpplauncher_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.4.1", features = [] } tauri-build = { version = "2.5.3", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.8.5", features = [] } tauri = { version = "2.9.5", features = [] }
tauri-plugin-shell = "2.3.1" tauri-plugin-shell = "2.3.3"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.143" serde_json = "1.0.148"
serde_repr = "0.1.20" serde_repr = "0.1.20"
tauri-plugin-dialog = "2.4.0" tauri-plugin-dialog = "2.4.2"
tauri-plugin-fs = "2.4.2" tauri-plugin-fs = "2.4.4"
hardware-id = "0.3.0" hardware-id = "0.3.0"
tauri-plugin-cors-fetch = "4.1.0" tauri-plugin-cors-fetch = "4.1.0"
sysinfo = "0.37.0" sysinfo = "0.37.2"
reqwest = { version = "0.12.23", features = ["json", "stream"] } reqwest = { version = "0.13.1", features = ["json", "stream", "query"] }
md5 = "0.8.0" md5 = "0.8.0"
tokio = { version = "1.47.1", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
open = "5.3.2" open = "5.3.3"
windows-sys = "0.61.0" windows-sys = "0.61.2"
discord-rich-presence = "1.0.0" discord-rich-presence = "1.0.0"
once_cell = "1.21.3" once_cell = "1.21.3"
@@ -40,8 +40,8 @@ once_cell = "1.21.3"
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.0" widestring = "1.2.1"
[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.4" tauri-plugin-single-instance = "2.3.6"
+41 -55
View File
@@ -56,17 +56,11 @@ pub fn valid_osu_folder(folder: String) -> bool {
"scores.db", "scores.db",
]; ];
let folder_files: Vec<&str> = osu_folder_files.iter().map(|&s| s).collect();
if folder_files.iter().any(|&file| file == "osu!.exe") == false {
return false;
}
let path = PathBuf::from(folder); let path = PathBuf::from(folder);
let match_percentage = check_folder_completeness(path, &osu_folder_files) >= 70.0; if !path.join("osu!.exe").exists() {
if match_percentage {
return true;
}
return false; return false;
}
check_folder_completeness(path, &osu_folder_files) >= 70.0
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -160,7 +154,7 @@ pub fn find_osu_installation() -> Option<String> {
} }
#[tauri::command] #[tauri::command]
pub fn get_beatmapsets_count(folder: String) -> Option<u64> { pub async fn get_beatmapsets_count(folder: String) -> Option<u64> {
let path = PathBuf::from(folder); let path = PathBuf::from(folder);
let osu_user_config = get_osu_user_config(path.clone()); let osu_user_config = get_osu_user_config(path.clone());
let songs_path = osu_user_config let songs_path = osu_user_config
@@ -173,12 +167,12 @@ pub fn get_beatmapsets_count(folder: String) -> Option<u64> {
} }
let mut count = 0; let mut count = 0;
if let Ok(entries) = std::fs::read_dir(songs_folder) { if let Ok(mut entries) = fs::read_dir(songs_folder).await {
for entry in entries.flatten() { while let Ok(Some(entry)) = entries.next_entry().await {
if entry.file_type().map_or(false, |ft| ft.is_dir()) { if entry.file_type().await.map_or(false, |ft| ft.is_dir()) {
let dir_path = entry.path(); let dir_path = entry.path();
if let Ok(files) = std::fs::read_dir(&dir_path) { if let Ok(mut files) = fs::read_dir(&dir_path).await {
for file in files.flatten() { while let Ok(Some(file)) = files.next_entry().await {
if file.path().extension().map_or(false, |ext| ext == "osu") { if file.path().extension().map_or(false, |ext| ext == "osu") {
count += 1; count += 1;
break; break;
@@ -192,7 +186,7 @@ pub fn get_beatmapsets_count(folder: String) -> Option<u64> {
} }
#[tauri::command] #[tauri::command]
pub 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);
let skins_folder = path.join("Skins"); let skins_folder = path.join("Skins");
@@ -201,12 +195,12 @@ pub fn get_skins_count(folder: String) -> Option<u64> {
} }
let mut count = 0; let mut count = 0;
if let Ok(entries) = std::fs::read_dir(skins_folder) { if let Ok(mut entries) = fs::read_dir(skins_folder).await {
for entry in entries.flatten() { while let Ok(Some(entry)) = entries.next_entry().await {
if entry.file_type().map_or(false, |ft| ft.is_dir()) { if entry.file_type().await.map_or(false, |ft| ft.is_dir()) {
let dir_path = entry.path(); let dir_path = entry.path();
if let Ok(files) = std::fs::read_dir(&dir_path) { if let Ok(mut files) = fs::read_dir(&dir_path).await {
for file in files.flatten() { while let Ok(Some(file)) = files.next_entry().await {
if file.path().extension().map_or(false, |ext| ext == "ini") { if file.path().extension().map_or(false, |ext| ext == "ini") {
count += 1; count += 1;
break; break;
@@ -222,28 +216,28 @@ pub fn get_skins_count(folder: String) -> Option<u64> {
#[tauri::command] #[tauri::command]
pub fn get_osu_skin(folder: String) -> String { pub fn get_osu_skin(folder: String) -> String {
let path = PathBuf::from(folder); let path = PathBuf::from(folder);
let osu_user_config = get_osu_user_config(path.clone()); let osu_user_config = get_osu_user_config(path);
return osu_user_config osu_user_config
.and_then(|config| config.get("Skin").cloned()) .and_then(|config| config.get("Skin").cloned())
.unwrap_or_else(|| "Default".to_string()); .unwrap_or_else(|| "Default".to_string())
} }
#[tauri::command] #[tauri::command]
pub fn get_osu_version(folder: String) -> String { pub fn get_osu_version(folder: String) -> String {
let path = PathBuf::from(folder); let path = PathBuf::from(folder);
let osu_user_config = get_osu_user_config(path.clone()); let osu_user_config = get_osu_user_config(path);
return osu_user_config osu_user_config
.and_then(|config| config.get("LastVersion").cloned()) .and_then(|config| config.get("LastVersion").cloned())
.unwrap_or_else(|| "failed".to_string()); .unwrap_or_else(|| "failed".to_string())
} }
#[tauri::command] #[tauri::command]
pub fn get_osu_release_stream(folder: String) -> String { pub fn get_osu_release_stream(folder: String) -> String {
let path = PathBuf::from(folder); let path = PathBuf::from(folder);
let osu_config = get_osu_config(path.clone()); let osu_config = get_osu_config(path);
return osu_config osu_config
.and_then(|config| config.get("_ReleaseStream").cloned()) .and_then(|config| config.get("_ReleaseStream").cloned())
.unwrap_or_else(|| "Stable40".to_string()); .unwrap_or_else(|| "Stable40".to_string())
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@@ -261,10 +255,7 @@ pub fn set_osu_user_config_values(
.iter() .iter()
.map(|entry| (entry.key.as_str(), Some(entry.value.as_str()))) .map(|entry| (entry.key.as_str(), Some(entry.value.as_str())))
.collect(); .collect();
match set_osu_user_config_vals(&osu_folder_path, &converted) { Ok(set_osu_user_config_vals(&osu_folder_path, &converted).is_ok())
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
} }
#[tauri::command] #[tauri::command]
@@ -276,22 +267,16 @@ pub fn set_osu_config_values(
.iter() .iter()
.map(|entry| (entry.key.as_str(), Some(entry.value.as_str()))) .map(|entry| (entry.key.as_str(), Some(entry.value.as_str())))
.collect(); .collect();
match set_osu_config_vals(&osu_folder_path, &converted) { Ok(set_osu_config_vals(&osu_folder_path, &converted).is_ok())
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
} }
#[tauri::command] #[tauri::command]
pub async fn run_osu_updater(folder: String) -> Result<(), String> { pub async fn run_osu_updater(folder: String) -> Result<(), String> {
#[cfg(windows)]
const DETACHED_PROCESS: u32 = 0x00000008;
#[cfg(windows)]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
let mut updater_process = { let mut updater_process = {
#[cfg(windows)] #[cfg(windows)]
{ {
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
let osu_exe_path = PathBuf::from(&folder).join("osu!.exe"); let osu_exe_path = PathBuf::from(&folder).join("osu!.exe");
Command::new(&osu_exe_path) Command::new(&osu_exe_path)
.arg("-repair") .arg("-repair")
@@ -368,9 +353,6 @@ pub async fn run_osu_updater(folder: String) -> Result<(), String> {
#[tauri::command] #[tauri::command]
pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> { pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
/* #[cfg(windows)]
use std::os::windows::process::CommandExt; */
#[cfg(windows)] #[cfg(windows)]
const DETACHED_PROCESS: u32 = 0x00000008; const DETACHED_PROCESS: u32 = 0x00000008;
#[cfg(windows)] #[cfg(windows)]
@@ -385,7 +367,7 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
.arg("ez-pp.farm") .arg("ez-pp.farm")
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP) .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
.spawn() .spawn()
.map_err(|e| format!("Failed to spawn updater: {}", e))? .map_err(|e| format!("Failed to spawn osu: {}", e))?
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -394,7 +376,7 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
.arg("--devserver") .arg("--devserver")
.arg("ez-pp.farm") .arg("ez-pp.farm")
.spawn() .spawn()
.map_err(|e| format!("Failed to spawn updater: {}", e))? .map_err(|e| format!("Failed to spawn osu: {}", e))?
} }
}; };
@@ -566,10 +548,13 @@ pub fn replace_ui_files(folder: String, revert: bool) -> Result<(), ReplaceUIErr
let osu_path = PathBuf::from(folder); let osu_path = PathBuf::from(folder);
let ezpp_ui = osu_path.join("EZPPLauncher").join("ezpp!ui.dll"); let ezpp_ui = osu_path.join("EZPPLauncher").join("ezpp!ui.dll");
let osu_ui = osu_path.join("osu!ui.dll"); let osu_ui = osu_path.join("osu!ui.dll");
let ezpp_seasonal = osu_path.join("EZPPLauncher").join("ezpp!seasonal.dll");
let osu_seasonal = osu_path.join("osu!seasonal.dll");
let ezpp_gameplay = osu_path.join("EZPPLauncher").join("ezpp!gameplay.dll"); let ezpp_gameplay = osu_path.join("EZPPLauncher").join("ezpp!gameplay.dll");
let osu_gameplay = osu_path.join("osu!gameplay.dll"); let osu_gameplay = osu_path.join("osu!gameplay.dll");
let osu_ui_bak = osu_path.join("osu!ui.dll.bak"); let osu_ui_bak = osu_path.join("osu!ui.dll.bak");
let osu_seasonal_bak = osu_path.join("osu!seasonal.dll.bak");
let osu_gameplay_bak = osu_path.join("osu!gameplay.dll.bak"); let osu_gameplay_bak = osu_path.join("osu!gameplay.dll.bak");
let try_rename = |from: &PathBuf, to: &PathBuf| -> Result<(), ReplaceUIError> { let try_rename = |from: &PathBuf, to: &PathBuf| -> Result<(), ReplaceUIError> {
@@ -591,12 +576,18 @@ pub fn replace_ui_files(folder: String, revert: bool) -> Result<(), ReplaceUIErr
try_rename(&osu_ui, &osu_ui_bak)?; try_rename(&osu_ui, &osu_ui_bak)?;
try_rename(&ezpp_ui, &osu_ui)?; try_rename(&ezpp_ui, &osu_ui)?;
try_rename(&osu_seasonal, &osu_seasonal_bak)?;
try_rename(&ezpp_seasonal, &osu_seasonal)?;
try_rename(&osu_gameplay, &osu_gameplay_bak)?; try_rename(&osu_gameplay, &osu_gameplay_bak)?;
try_rename(&ezpp_gameplay, &osu_gameplay)?; try_rename(&ezpp_gameplay, &osu_gameplay)?;
} else { } else {
try_rename(&osu_ui, &ezpp_ui)?; try_rename(&osu_ui, &ezpp_ui)?;
try_rename(&osu_ui_bak, &osu_ui)?; try_rename(&osu_ui_bak, &osu_ui)?;
try_rename(&osu_seasonal, &ezpp_seasonal)?;
try_rename(&osu_seasonal_bak, &osu_seasonal)?;
try_rename(&osu_gameplay, &ezpp_gameplay)?; try_rename(&osu_gameplay, &ezpp_gameplay)?;
try_rename(&osu_gameplay_bak, &osu_gameplay)?; try_rename(&osu_gameplay_bak, &osu_gameplay)?;
} }
@@ -723,12 +714,7 @@ pub async fn has_net8() -> bool {
#[tauri::command] #[tauri::command]
pub fn encrypt_string(string: String, entropy: String) -> String { pub fn encrypt_string(string: String, entropy: String) -> String {
let encrypted = encrypt_password(&string, &entropy); encrypt_password(&string, &entropy).unwrap_or(string)
match encrypted {
Ok(encrypted_vec) => encrypted_vec,
Err(_) => string,
}
} }
#[tauri::command] #[tauri::command]
@@ -764,7 +750,7 @@ pub async fn download_ezpp_launcher_update(app: AppHandle, url: String) -> Resul
UpdateStatus { UpdateStatus {
file_name: "Update".to_string(), file_name: "Update".to_string(),
downloaded, downloaded,
size: size, size,
progress: ((downloaded as f64 / size as f64 * 100.0) * 100.0).trunc() / 100.0, progress: ((downloaded as f64 / size as f64 * 100.0) * 100.0).trunc() / 100.0,
}, },
) )
+2 -2
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.0.7", "version": "3.1.0",
"identifier": "farm.EZPPFarm.Launcher", "identifier": "farm.EZPPFarm.Launcher",
"mainBinaryName": "ezpplauncher", "mainBinaryName": "ezpplauncher",
"build": { "build": {
@@ -15,7 +15,7 @@
{ {
"title": "EZPPLauncher", "title": "EZPPLauncher",
"width": 1000, "width": 1000,
"height": 737, "height": 650,
"decorations": false, "decorations": false,
"resizable": false, "resizable": false,
"maximizable": false "maximizable": false
+4 -6
View File
@@ -1,15 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en" class="dark"> <html lang="en" class="dark">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EZPPLauncher</title> <title>EZPPLauncher</title>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body class="bg-theme-950" data-sveltekit-preload-data="hover"> <body class="bg-theme-950" data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>
@@ -0,0 +1,77 @@
<script lang="ts">
//TODO: maybe dynamic background images fetched from ezpp?
const COUNT = 120;
function rand(min: number, max: number) {
return Math.random() * (max - min) + min;
}
const streaks = Array.from({ length: COUNT }).map(() => {
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: rand(180, 320) * scale,
height: rand(16, 32) * scale,
x: rand(-80, 100),
y: rand(-80, 100),
opacity: 0.25 + depth * 0.65,
duration,
delay: rand(0, duration),
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}
<div
class="streak drop-shadow-lg"
style="
width:{s.width}px;
height:{s.height}px;
left:{s.x}%;
top:{s.y}%;
opacity:{s.opacity};
animation-duration:{s.duration}s;
animation-delay:-{s.delay}s;
"
></div>
{/each}
</div>
<style>
.bg {
position: fixed;
inset: 0;
overflow: hidden;
background: linear-gradient(180deg, #8f2f64 0%, #963f75 40%, #9a6c7f 100%);
}
.streak {
position: absolute;
background: #d6f3ff;
border-radius: 999px;
transform: rotate(-35deg);
animation: move linear infinite;
will-change: transform;
}
@keyframes move {
from {
transform: translate(-150vw, 150vh) rotate(-35deg);
}
to {
transform: translate(150vw, -150vh) rotate(-35deg);
}
}
</style>
@@ -0,0 +1,91 @@
<script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis';
import Play from 'lucide-svelte/icons/play';
const prop: {
downloading: boolean;
progress: number;
text: string;
subtext?: string;
disabled?: boolean;
onClick?: () => void;
} = $props();
const radius = 16;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = $derived(circumference - (prop.progress / 100) * circumference);
</script>
<button
class="group flex items-center h-12 rounded-full bg-pink-400 disabled:bg-pink-400/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}
>
<div class="relative size-12 shrink-0 flex items-center justify-center">
{#if prop.downloading}
{#if prop.progress === -1}
<svg class="absolute inset-0 animate-spin" viewBox="0 0 48 48">
<circle class="stroke-black/10" cx="24" cy="24" r={radius} fill="none" stroke-width="3" />
<circle
class="stroke-pink-950"
cx="24"
cy="24"
r={radius}
fill="none"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray={circumference}
stroke-dashoffset={circumference * 0.7}
/>
</svg>
{:else}
<svg class="absolute inset-0 -rotate-90" viewBox="0 0 48 48">
<circle
cx="24"
cy="24"
r={radius}
fill="none"
stroke="rgba(0,0,0,0.2)"
stroke-width="3"
/>
<circle
cx="24"
cy="24"
r={radius}
fill="none"
stroke="rgba(0,0,0,0.7)"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray={circumference}
stroke-dashoffset={strokeDashoffset}
class="transition-[stroke-dashoffset] duration-100"
/>
</svg>
{/if}
{/if}
<div class="size-9 rounded-full bg-black/30 flex items-center justify-center relative z-10">
{#if prop.downloading}
{#if prop.progress === -1}
<Ellipsis class="size-4 text-primary-foreground fill-current animate-pulse" />
{:else}
<span class="text-[10px] font-bold text-primary-foreground">
{prop.progress.toFixed()}
</span>
{/if}
{:else}
<Play class="size-4 text-primary-foreground fill-current ml-0.5" />
{/if}
</div>
</div>
<div class="pr-6 pl-1 text-center w-52">
<p class="text-sm font-bold text-primary-foreground leading-tight">
{prop.text}
</p>
{#if prop.subtext}
<p class="text-[10px] text-primary-foreground/70 leading-tight">
{prop.subtext}
</p>
{/if}
</div>
</button>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import Check from 'lucide-svelte/icons/check'; import Check from '@lucide/svelte/icons/check';
import Minus from 'lucide-svelte/icons/minus'; import Minus from '@lucide/svelte/icons/minus';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui';
import Circle from 'lucide-svelte/icons/circle'; import Circle from '@lucide/svelte/icons/circle';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
let { let {
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRight from 'lucide-svelte/icons/chevron-right'; import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
let { let {
@@ -25,7 +25,7 @@
const computed = getComputedStyle(el); const computed = getComputedStyle(el);
if ( if (
['button', 'a', 'input', 'select', 'textarea', 'label', 'option'].includes(tag) || ['button', 'a', 'input', 'select', 'textarea', 'label', 'option'].includes(tag) ||
(role && ['button', 'link', 'checkbox', 'combobox'].includes(role)) || (role && ['button', 'link', 'checkbox', 'combobox', 'menuitem', 'option'].includes(role)) ||
computed.cursor === 'pointer' computed.cursor === 'pointer'
) { ) {
return true; return true;
@@ -85,7 +85,7 @@
animate(cursor, { animate(cursor, {
duration: smoothCursor ? $cursorSmoothness : 0, duration: smoothCursor ? $cursorSmoothness : 0,
translateX: mouseX, translateX: mouseX,
translateY: mouseY - 50, translateY: mouseY,
ease: (t: number) => (t - 1) ** 5 + 1, ease: (t: number) => (t - 1) ** 5 + 1,
}); });
@@ -17,7 +17,7 @@
bind:ref bind:ref
{value} {value}
class={cn( class={cn(
'data-[highlighted]:bg-primary-800/30 data-[highlighted]:text-accent-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-accent data-[highlighted]:text-accent-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 className
)} )}
{...restProps} {...restProps}
+7
View File
@@ -0,0 +1,7 @@
import Root from './switch.svelte';
export {
Root,
//
Root as Switch,
};
@@ -0,0 +1,27 @@
<script lang="ts">
import { Switch as SwitchPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
...restProps
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
</script>
<SwitchPrimitive.Root
bind:ref
bind:checked
class={cn(
'focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...restProps}
>
<SwitchPrimitive.Thumb
class={cn(
'bg-background pointer-events-none block size-5 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>
+9 -18
View File
@@ -19,27 +19,18 @@
}); });
</script> </script>
<div data-tauri-drag-region class="titlebar z-[60] border-b border-theme-800/90"> <div data-tauri-drag-region class="titlebar z-[100]">
<div <div class="titlebar-button rounded transition-colors duration-75" id="titlebar-minimize">
class="mr-auto ms-2 flex flex-row gap-2 items-center text-[1.05rem] font-semibold pointer-events-none" <Minimize size={14} />
>
<img src={Logo} alt="EZPP Launcher Logo" class="h-11 w-11 inline-block" />
<span>EZPPLauncher</span>
<Badge class="!text-[0.6rem] py-[0.5px] bg-primary-500 hover:bg-primary-500"
>{$launcherVersion}</Badge
>
</div> </div>
<div class="titlebar-button rounded-lg transition-colors duration-75" id="titlebar-minimize"> <div class="titlebar-button close rounded transition-colors duration-75" id="titlebar-close">
<Minimize size={18} /> <Close size={14} />
</div>
<div class="titlebar-button close rounded-lg transition-colors duration-75" id="titlebar-close">
<Close size={18} />
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
.titlebar { .titlebar {
height: 50px; height: 35px;
/* background: #040612; */ /* background: #040612; */
user-select: none; user-select: none;
display: flex; display: flex;
@@ -52,14 +43,14 @@
right: 0; right: 0;
margin-bottom: 10px; margin-bottom: 10px;
padding-right: 5px; padding-right: 5px;
pointer-events: all !important; pointer-events: auto;
} }
.titlebar-button { .titlebar-button {
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 40px; width: 25px;
height: 40px; height: 25px;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
} }
+1 -1
View File
@@ -150,7 +150,7 @@
<OsuCursor smoothCursor={$cursorSmoothening} /> <OsuCursor smoothCursor={$cursorSmoothening} />
{/if} {/if}
<Toaster richColors closeButton /> <Toaster richColors closeButton theme="dark" position="top-right" />
<Titlebar /> <Titlebar />
+378 -368
View File
@@ -7,7 +7,6 @@
import { import {
beatmapSets, beatmapSets,
currentSkin, currentSkin,
currentView,
discordPresence, discordPresence,
launcherStream, launcherStream,
launcherStreams, launcherStreams,
@@ -25,13 +24,9 @@
} from '@/global'; } from '@/global';
import { import {
LoaderCircle, LoaderCircle,
Logs,
Music2, Music2,
Play,
Wifi, Wifi,
Gamepad2,
WifiOff, WifiOff,
Settings2,
Drum, Drum,
Cherry, Cherry,
Piano, Piano,
@@ -39,8 +34,9 @@
LogOut, LogOut,
LogIn, LogIn,
Brush, Brush,
Heart,
ArrowRight, ArrowRight,
Settings,
House,
} 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';
@@ -48,13 +44,12 @@
import { import {
compareBuildNumbers, compareBuildNumbers,
formatBytes, formatBytes,
formatTimeReadable,
numberHumanReadable, numberHumanReadable,
openURL, openURL,
releaseStreamToReadable, releaseStreamToReadable,
urlIsValidImage, urlIsValidImage,
} from '@/utils'; } from '@/utils';
import { crossfade, fade, scale } from 'svelte/transition'; import { fade, fly, scale } 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 {
@@ -70,7 +65,6 @@
import Input from '@/components/ui/input/input.svelte'; import Input from '@/components/ui/input/input.svelte';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Login from './Login.svelte';
import { currentUser, userAuth } from '@/userAuthentication'; import { currentUser, userAuth } from '@/userAuthentication';
import { import {
getGamemodeInt, getGamemodeInt,
@@ -108,33 +102,27 @@
} from '@/osuUtil'; } from '@/osuUtil';
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import { ezppfarm } from '@/api/ezpp'; import { ezppfarm } from '@/api/ezpp';
import Hearts from '@/components/ui/effects/Hearts.svelte';
import { EZPPActionStatus } from '@/types'; import { EZPPActionStatus } from '@/types';
import * as presence from '@/presence'; import * as presence from '@/presence';
import { expoOut } from 'svelte/easing';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import * as DropdownMenu from '@/components/ui/dropdown-menu';
import DownloadButton from '@/components/ui/download-button/DownloadButton.svelte';
import { animate } from 'animejs';
import AnimatedBg from '@/components/ui/animated-bg/AnimatedBg.svelte';
const tabs = [ let selectedView = $state('home');
{ name: 'Home', key: 'home', show: true },
{
name: 'Settings',
key: 'settings',
show: true,
},
];
let selectedTab = $state('home');
let [tab_send, tab_receive] = crossfade({
duration: $reduceAnimations ? 0 : 400,
easing: expoOut,
});
let progress = $state(-1); let progress = $state(-1);
let launchInfo = $state(''); let launchInfo = $state('');
let launchError = $state<Error | undefined>(undefined); let launchError = $state<Error | undefined>(undefined);
let ezppLogo: HTMLImageElement;
let askForTrackingPermission = $state(false); let askForTrackingPermission = $state(false);
let downloadingUpdate = $state(false); let downloadingUpdate = $state(false);
let downloadingEZPPFiles = $state(false);
let cleanup = $state(false);
let selectedGamemode = $derived( let selectedGamemode = $derived(
getGamemodeInt(modeIntToStr($preferredMode), typeIntToStr($preferredType)) getGamemodeInt(modeIntToStr($preferredMode), typeIntToStr($preferredType))
); );
@@ -177,11 +165,17 @@
const skin: string = await getSkin(selectedPath); const skin: string = await getSkin(selectedPath);
currentSkin.set(skin); currentSkin.set(skin);
const osuReleaseStream = await getReleaseStream($osuInstallationPath);
osuStream.set(osuReleaseStream);
const osuVersion = await getVersion($osuInstallationPath);
osuBuild.set(osuVersion);
} }
}; };
const launch = async () => { const launch = async () => {
umami.track('app_launch_osu'); if ($launching) return;
if ($trackingEnabled) umami.track('app_launch_osu');
const osuRunning = await isOsuRunning(); const osuRunning = await isOsuRunning();
if (osuRunning) { if (osuRunning) {
toast.error('Hold on a second!', { toast.error('Hold on a second!', {
@@ -228,12 +222,13 @@
} }
try { try {
launchInfo = 'Looking for EZPPLauncher File updates...'; launchInfo = 'Looking for file updates...';
const updateResult = await getEZPPLauncherUpdateFiles(osuPath, $launcherStream); const updateResult = await getEZPPLauncherUpdateFiles(osuPath, $launcherStream);
if (updateResult) { if (updateResult) {
if (updateResult.filesToDownload.length > 0) { if (updateResult.filesToDownload.length > 0) {
launchInfo = 'Found EZPPLauncher File updates!'; downloadingEZPPFiles = true;
launchInfo = 'Found file updates!';
await new Promise((res) => setTimeout(res, 1000)); await new Promise((res) => setTimeout(res, 1000));
await downloadEZPPLauncherUpdateFiles( await downloadEZPPLauncherUpdateFiles(
osuPath, osuPath,
@@ -241,12 +236,13 @@
updateResult.updateFiles, updateResult.updateFiles,
(file) => { (file) => {
progress = file.progress; progress = file.progress;
launchInfo = `Downloading ${file.fileName}(${formatBytes( launchInfo = `${file.fileName}(${formatBytes(
file.downloaded file.downloaded
)}/${formatBytes(file.size)})...`; )}/${formatBytes(file.size)})...`;
} }
); );
progress = -1; progress = -1;
downloadingEZPPFiles = false;
} else { } else {
launchInfo = 'EZPPLauncher Files are up to date!'; launchInfo = 'EZPPLauncher Files are up to date!';
await new Promise((res) => setTimeout(res, 1500)); await new Promise((res) => setTimeout(res, 1500));
@@ -485,7 +481,8 @@
} }
await runOsu(osuPath, true); await runOsu(osuPath, true);
umami.track('app_exit_osu'); if ($trackingEnabled) umami.track('app_exit_osu');
cleanup = true;
launchInfo = 'Cleaning up...'; launchInfo = 'Cleaning up...';
await getCurrentWindow().show(); await getCurrentWindow().show();
if (presenceUpdater) { if (presenceUpdater) {
@@ -529,15 +526,92 @@
} }
launching.set(false); launching.set(false);
cleanup = false;
} catch (err) { } catch (err) {
launchError = err as Error; cleanup = false;
console.log(err); const error = err as Error;
if (error.name === 'AbortError') {
toast.error('Hmmm...', { toast.error('Hmmm...', {
description: 'Failed to launch.', description: 'Failed to launch.',
}); });
launching.set(false); launching.set(false);
umami.track('app_launch_fail', { error: err }); launchError = {
name: error.name,
message: 'Network request connection timed out.',
};
} else {
launchError = error;
toast.error('Hmmm...', {
description: 'Failed to launch.',
});
launching.set(false);
if ($trackingEnabled) umami.track('app_launch_fail', { error: err });
} }
}
};
let username = $state('');
let password = $state('');
let loginIsLoading = $state(false);
const performLogin = async () => {
loginIsLoading = true;
try {
const loginResult = await ezppfarm.login(username, password);
if (loginResult && loginResult.user) {
toast.success('Login successful!', {
description: `Welcome back, ${loginResult.user.name}!`,
});
$userAuth.value('username').set(username);
$userAuth.value('password').set(password);
await $userAuth.save();
currentUser.set(loginResult.user);
selectedView = 'home';
} else {
toast.error('Login failed!', {
description: 'Please check your username and password.',
});
loginIsLoading = false;
}
} catch {
toast.error('Server error occurred during login.', {
description: 'There was an issue connecting to the server. Please try again later.',
});
loginIsLoading = false;
}
if ($currentUser) {
const userInfo = await ezppfarm.getUserInfo($currentUser.id);
if (userInfo) {
currentUserInfo.set(userInfo.player);
preferredMode.set(userInfo.player.info.preferred_mode);
preferredType.set(userInfo.player.info.preferred_type);
}
}
};
let animateInterval: number | undefined;
const doBPMAnimation = () => {
if (animateInterval) return;
animateInterval = window.setInterval(async () => {
animate(ezppLogo, {
scale: 1.1,
duration: 900,
ease: (t: number) => Math.pow(2, -5 * t) * Math.sin((t - 0.075) * 20.94) + 1 - 0.0005 * t,
onComplete: () => {},
});
await new Promise((resolve) => setTimeout(resolve, 200));
animate(ezppLogo, {
scale: 1,
duration: 900,
ease: (t: number) => (t - 1) ** 7 + 1,
onComplete: () => {},
});
}, 450);
}; };
onMount(() => { onMount(() => {
@@ -548,6 +622,17 @@
} else { } else {
askForTrackingPermission = true; askForTrackingPermission = true;
} }
animate(ezppLogo, {
opacity: [0, 1],
scale: [0.95, 1],
duration: 900,
ease: (t: number) => (t - 1) ** 7 + 1,
onComplete: doBPMAnimation,
});
return () => {
window.clearInterval(animateInterval);
};
}); });
</script> </script>
@@ -706,56 +791,47 @@
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Root> </AlertDialog.Root>
<AlertDialog.Root bind:open={$launching}> <div class="grid grid-cols-[0.085fr_1fr] h-[100vh] relative">
<AlertDialog.Content <div class="absolute top-0 left-0 w-full h-[100vh] opacity-30 z-0">
class="bg-theme-950 border-theme-800 p-0" <AnimatedBg />
escapeKeydownBehavior="ignore" </div>
interactOutsideBehavior="ignore"
>
<div <div
class="flex flex-col items-center justify-center border-b border-theme-800 bg-black/40 rounded-t-lg p-3" class="p-3 border-r border-r-theme-900 flex flex-col items-center gap-2 z-10 bg-black/40 backdrop-blur-sm"
> >
<img class="h-20 w-20" src={Logo} alt="logo" /> <div class="bg-pink-950/80 border border-pink-900/50 rounded-[1.1rem] p-1">
<span class="font-semibold text-xl">Launching...</span> <img class="pointer-events-none" src={Logo} alt="logo" bind:this={ezppLogo} />
</div> </div>
<div class="flex flex-col items-center justify-center gap-2 p-3 rounded-lg"> <Badge class="text-[0.5rem] py-0 px-2">{$launcherVersion || 'dev'}</Badge>
<Progress indeterminate={progress === -1} value={progress} /> <Button
<span class="text-muted-foreground text-sm mt-4">{launchInfo}</span> class="flex size-12 items-center gap-2 {selectedView === 'home'
</div> ? 'bg-pink-300/50'
</AlertDialog.Content> : 'bg-black/20'} hover:bg-pink-300/50 rounded-[0.85rem] p-3 mt-3"
</AlertDialog.Root> onclick={() => (selectedView = 'home')}
>
<div class="grid grid-cols-[0.41fr_1fr] mt-[50px] h-[calc(100vh-50px)]"> <House class="text-theme-200 !size-5" />
<div class="w-full h-full border-r border-theme-800/90 flex flex-col gap-6 py-3"> </Button>
<div class="flex flex-col items-center gap-2 border-b pb-6"> <Button
<div class="size-20 relative"> class="flex size-12 items-center gap-2 {selectedView === 'settings'
{#if $currentUser?.donor} ? 'bg-pink-300/50'
<Hearts className="top-0 left-0 h-[70px] w-[60px] absolute"></Hearts> : 'bg-black/20'} hover:bg-pink-300/50 rounded-[0.85rem] p-3 mt-3"
{/if} onclick={() => (selectedView = 'settings')}
<Avatar.Root class="w-20 h-20 border-2 border-theme-800"> >
<Settings class="text-theme-200 !size-5" />
</Button>
<div class="mt-auto">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<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">
<LoaderCircle class="animate-spin" size={32} /> <LoaderCircle class="animate-spin" size={32} />
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
</div> </DropdownMenu.Trigger>
<span class="font-semibold text-2xl text-theme-50">{$currentUser?.name ?? 'Guest'}</span> <DropdownMenu.Content align="end" side="right" class="w-44">
<div class="flex flex-row gap-2"> {#if $currentUser}
{#if !$currentUser} <DropdownMenu.Item
<Button class="text-destructive focus:text-destructive text-xs"
variant="outline"
size="sm"
class="bg-theme-900 hover:bg-theme-700/40 border-theme-800 text-xs"
onclick={() => currentView.set(Login)}
>
<LogIn class="!size-4" />
Login
</Button>
{:else}
<Button
variant="outline"
size="sm"
class="bg-theme-900 hover:bg-theme-700/40 border-theme-800 text-xs"
onclick={async () => { onclick={async () => {
$userAuth.value('username').del(); $userAuth.value('username').del();
$userAuth.value('password').del(); $userAuth.value('password').del();
@@ -765,19 +841,150 @@
}); });
currentUser.set(undefined); currentUser.set(undefined);
currentUserInfo.set(undefined); currentUserInfo.set(undefined);
selectedView = 'home';
}} }}
> >
<LogOut class="!size-4" /> <LogOut class="size-3.5 mr-2" />
Logout Logout
</Button> </DropdownMenu.Item>
{:else}
<DropdownMenu.Item class="text-xs" onclick={() => (selectedView = 'login')}>
<LogIn class="size-3.5 mr-2" />
Login
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
<div class="z-10 h-full overflow-hidden">
{#if selectedView === 'home'}
<div
class="flex flex-col-reverse h-full"
in:fly={{ duration: 400, delay: 400, y: 10, opacity: 0 }}
out:fly={{ duration: 400, y: -10, opacity: 0 }}
>
<div class="relative z-10 px-8 py-4 flex items-center justify-between">
<div class="flex items-center gap-4">
{#if $osuInstallationPath !== ''}
<div class="flex items-center gap-2">
<Music2 class="size-3.5 text-blue-400" />
<span class="text-[11px] text-white/70 font-medium">
{#if !$beatmapSets && $beatmapSets !== 0}
<LoaderCircle class="animate-spin" size={12} />
{:else}
{numberHumanReadable($beatmapSets ?? 0)}
{/if}
</span>
</div>
<div class="flex items-center gap-2">
<Brush class="size-3.5 text-amber-400" />
<span class="text-[11px] text-white/70 font-medium">
{#if !$skins && $skins !== 0}
<LoaderCircle class="animate-spin" size={12} />
{:else}
{numberHumanReadable($skins ?? 0)}
{/if}
</span>
</div>
{/if}
<div class="flex items-center gap-2">
{#if $serverConnectionFails > 1}
<WifiOff class="size-3.5 text-red-400" />
{:else}
<Wifi class="size-3.5 text-emerald-400" />
{/if}
<span class="relative text-[11px] text-white/70 font-medium">
<div
class="absolute top-0.5 left-1/2 -translate-x-1/2 {!$serverPing ||
$serverConnectionFails > 1
? 'opacity-100'
: 'opacity-0'} transition-opacity duration-1000"
>
<LoaderCircle class="size-3.5 animate-spin" />
</div>
<div
class="{!$serverPing || $serverConnectionFails > 1
? 'opacity-0'
: 'opacity-100'} transition-opacity duration-1000"
>
{#if $reduceAnimations}
<span>{$serverPing}ms</span>
{:else}
<NumberFlow value={$serverPing ?? 0} trend={0} suffix="ms" />
{/if} {/if}
</div> </div>
</span>
</div> </div>
{#if $currentUser}
<div class="flex flex-col gap-6 h-full px-3"> {#if $osuInstallationPath !== ''}
<div class="w-px h-4 bg-white/10"></div>
<span
class="text-[10px] px-2 py-0.5 rounded-full bg-white/10 text-white/50 border border-white/[0.06]"
>
{#if $osuStream}
{releaseStreamToReadable($osuStream)}
{:else}
<LoaderCircle class="animate-spin" size={10} />
{/if}
</span>
<span
class="text-[10px] px-2 py-0.5 rounded-full bg-white/10 text-white/50 font-mono border border-white/[0.06]"
>
{#if $osuBuild}
{$osuBuild}
{:else}
<LoaderCircle class="animate-spin" size={10} />
{/if}
</span>
<span
class="text-[10px] px-2 py-0.5 rounded-full bg-primary/20 text-primary border border-primary/20"
>
{#if $currentSkin}
{#if $currentSkin.length > 23}
{$currentSkin.slice(0, 23) + '...'}
{:else}
{$currentSkin}
{/if}
{:else}
<LoaderCircle class="animate-spin" size={10} />
{/if}
</span>
{/if}
</div>
<DownloadButton
downloading={$launching}
{progress}
text={$launching
? downloadingEZPPFiles
? 'Downloading...'
: cleanup
? 'Cleaning up...'
: 'Launching...'
: $serverConnectionFails > 1
? 'No connection'
: $osuInstallationPath === ''
? 'Hmmm..'
: 'Launch'}
subtext={$launching && !cleanup
? launchInfo
: $osuInstallationPath === ''
? 'osu! path is not set!'
: $serverConnectionFails > 1
? 'No connection...'
: undefined}
disabled={$osuInstallationPath === '' || $serverConnectionFails > 1}
onClick={launch}
/>
</div>
{#if $currentUserInfo}
<div <div
in:scale={{ duration: $reduceAnimations ? 0 : 400, start: 0.98 }} 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"
out:scale={{ duration: $reduceAnimations ? 0 : 400, start: 0.98 }} in:fly={{ duration: 400, y: 10, opacity: 0 }}
out:fly={{ duration: 400, y: -10, opacity: 0 }}
> >
<Select.Root <Select.Root
type="single" type="single"
@@ -785,7 +992,7 @@
onValueChange={updateGamemode} onValueChange={updateGamemode}
> >
<Select.Trigger <Select.Trigger
class="border-theme-800/90 bg-theme-900/90 !text-muted-foreground font-semibold" class="border-theme-800/90 bg-theme-900/90 hover:bg-theme-800/90 !text-muted-foreground font-semibold"
> >
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
{#if selectedMode === 0} {#if selectedMode === 0}
@@ -806,13 +1013,13 @@
<Select.Item value={gamemode.toFixed()}> <Select.Item value={gamemode.toFixed()}>
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
{#if gamemod.mode === 0} {#if gamemod.mode === 0}
<Circle size={16} class="text-theme-200" /> <Circle size={16} />
{:else if gamemod.mode === 1} {:else if gamemod.mode === 1}
<Drum size={16} class="text-theme-200" /> <Drum size={16} />
{:else if gamemod.mode === 2} {:else if gamemod.mode === 2}
<Cherry size={16} class="text-theme-200" /> <Cherry size={16} />
{:else if gamemod.mode === 3} {:else if gamemod.mode === 3}
<Piano size={16} class="text-theme-200" /> <Piano size={16} />
{/if} {/if}
{getGamemodeName(modeIntToStr(gamemod.mode), typeIntToStr(gamemod.type))} {getGamemodeName(modeIntToStr(gamemod.mode), typeIntToStr(gamemod.type))}
</div> </div>
@@ -820,28 +1027,10 @@
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> <div class="grid grid-cols-2 gap-2 w-full m-3">
<div <div class="flex flex-col">
class="bg-theme-900/90 border border-theme-800/90 rounded-lg p-2" <span class="text-xs text-muted-foreground font-semibold">Rank</span>
in:scale={{ <div class="flex items-center h-full font-semibold text-theme-50">
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 50,
start: 0.98,
}}
out:scale={{
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 50,
start: 0.98,
}}
>
<div class="flex flex-row items-center gap-2">
<Logs class="text-muted-foreground" size="16" />
<span class="font-semibold text-muted-foreground text-sm">Mode Stats</span>
</div>
<div class="grid grid-cols-2 mt-2 border-t border-theme-800 pt-2 pb-2">
<div class="flex flex-col gap-0.5">
<span class="text-sm text-muted-foreground font-semibold">Rank</span>
<div class="flex items-center h-full text-lg font-semibold text-theme-50">
{#if $currentUserInfo} {#if $currentUserInfo}
<div in:fade> <div in:fade>
{#if $reduceAnimations} {#if $reduceAnimations}
@@ -863,18 +1052,19 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="flex flex-col gap-0.5"> <div class="flex flex-col">
<span class="text-sm text-muted-foreground font-semibold">PP</span> <span class="text-xs text-muted-foreground font-semibold">PP</span>
<div class="flex items-center h-full text-lg font-semibold text-theme-50"> <div class="flex items-center h-full font-semibold text-pink-300">
{#if $currentUserInfo} {#if $currentUserInfo}
<div in:fade> <div in:fade>
{#if $reduceAnimations} {#if $reduceAnimations}
<span> <span>
{numberHumanReadable($currentUserInfo.stats[selectedGamemode].pp ?? 0)} {numberHumanReadable($currentUserInfo.stats[selectedGamemode].pp ?? 0)}pp
</span> </span>
{:else} {:else}
<NumberFlow <NumberFlow
trend={0} trend={0}
suffix="pp"
value={$currentUserInfo.stats[selectedGamemode].pp ?? 0} value={$currentUserInfo.stats[selectedGamemode].pp ?? 0}
/> />
{/if} {/if}
@@ -886,22 +1076,20 @@
{/if} {/if}
</div> </div>
</div> </div>
</div> <div class="flex flex-col">
<div class="grid grid-cols-[1fr_auto] border-t border-theme-800 pt-2">
<span class="text-xs text-muted-foreground font-semibold">Accuracy</span> <span class="text-xs text-muted-foreground font-semibold">Accuracy</span>
<div <div class="flex items-center h-full font-semibold text-theme-50">
class="flex items-center flex-row-reverse h-full text-xs text-end font-semibold text-theme-50"
>
{#if $currentUserInfo} {#if $currentUserInfo}
<div in:fade> <div in:fade>
{#if $reduceAnimations} {#if $reduceAnimations}
<span>{($currentUserInfo.stats[selectedGamemode].acc ?? 0).toFixed(2)}%</span> <span>
{($currentUserInfo.stats[selectedGamemode].acc ?? 0).toFixed(2)}%
</span>
{:else} {:else}
<NumberFlow <NumberFlow
class="leading-none"
trend={0} trend={0}
suffix="%" suffix="%"
value={($currentUserInfo.stats[selectedGamemode].acc ?? 0).toFixed(2)} value={$currentUserInfo.stats[selectedGamemode].acc.toFixed(2) ?? 0}
/> />
{/if} {/if}
</div> </div>
@@ -911,22 +1099,18 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
<span class="text-xs text-muted-foreground font-semibold">Play Count</span> <div class="flex flex-col">
<div <span class="text-xs text-muted-foreground font-semibold">Playcount</span>
class="flex items-center flex-row-reverse h-full text-xs text-end font-semibold text-theme-50" <div class="flex items-center h-full font-semibold text-theme-50">
>
{#if $currentUserInfo} {#if $currentUserInfo}
<div in:fade> <div in:fade>
{#if $reduceAnimations} {#if $reduceAnimations}
<span <span>
>{numberHumanReadable( {numberHumanReadable($currentUserInfo.stats[selectedGamemode].plays ?? 0)}
$currentUserInfo.stats[selectedGamemode].plays ?? 0 </span>
)}</span
>
{:else} {:else}
<NumberFlow <NumberFlow
class="leading-none"
trend={0} trend={0}
value={$currentUserInfo.stats[selectedGamemode].plays ?? 0} value={$currentUserInfo.stats[selectedGamemode].plays ?? 0}
/> />
@@ -938,244 +1122,38 @@
</div> </div>
{/if} {/if}
</div> </div>
<span class="text-xs text-muted-foreground font-semibold">Play Time</span>
<div
class="flex items-center flex-row-reverse h-full text-xs text-end font-semibold text-theme-50"
>
{#if $currentUserInfo}
<div in:fade>
{formatTimeReadable($currentUserInfo.stats[selectedGamemode].playtime ?? 0)}
</div> </div>
{:else} </div>
<div in:fade>
<LoaderCircle class="animate-spin" size={21} />
</div> </div>
{/if} {/if}
<div class="flex flex-col mb-auto mt-12 px-6">
<span class="text-4xl font-bold drop-shadow-lg">EZPPLauncher</span>
<span class="text-muted-foreground font-semibold drop-shadow-lg"
>Hello {$currentUser?.name ?? 'Guest'}!</span
>
</div> </div>
</div> </div>
</div> {:else if selectedView === 'settings'}
<!-- <div class="mt-auto bg-theme-900/90 border border-theme-800/90 rounded-lg p-2">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center gap-2">
<Users class="text-muted-foreground" size="16" />
<span class="font-semibold text-muted-foreground text-sm"> Friends </span>
</div>
<Badge class="h-5 bg-green-500/20 hover:bg-green-500/20 text-green-500">3 online</Badge>
</div>
</div> -->
</div>
{/if}
</div>
<div class="flex flex-col gap-6 w-full h-full bg-theme-900/40 p-6">
<div <div
class="flex flex-row flex-nowrap h-11 gap-1 w-full bg-theme-800/50 border border-theme-800/90 rounded-lg p-[4px]" class="h-[100vh] w-full flex flex-col items-center justify-center"
> in:fly={{ duration: 400, delay: 400, y: 10, opacity: 0 }}
{#each tabs as tab (tab.key)} out:fly={{ duration: 400, y: -10, opacity: 0 }}
{#if tab.show}
<button
class="inline-block relative py-2 px-2 disabled:opacity-25 transition-opacity w-full text-center"
onclick={() => (selectedTab = tab.key)}
>
<div
class="flex flex-row items-center justify-center gap-1 text-xs md:text-sm font-semibold w-full text-center"
>
<div>{tab.name}</div>
</div>
{#if selectedTab === tab.key}
<div
in:tab_receive={{ key: 'tab' }}
out:tab_send={{ key: 'tab' }}
class="absolute left-0 bottom-0 w-full h-full rounded-lg bg-white/10 border border-white/10"
></div>
{/if}
</button>
{/if}
{/each}
</div>
{#if selectedTab === 'home'}
<div
class="my-auto bg-theme-900/90 flex flex-col items-center justify-center gap-6 border border-theme-800/90 rounded-lg p-6"
in:scale={{ duration: $reduceAnimations ? 0 : 400, start: 0.98 }}
>
<div class="grid grid-cols-3 w-full gap-3">
<div
class="bg-theme-800/90 border border-theme-700/90 rounded-lg px-2 py-4 w-full flex flex-col gap-1 items-center justify-center"
>
<div class="flex items-center justify-center p-2 rounded-lg bg-blue-500/20">
<Music2 class="text-blue-500" size="26" />
</div>
<div class="relative font-bold text-xl text-blue-400">
<div
class="absolute top-1 left-1/2 -translate-x-1/2 {!$beatmapSets
? 'opacity-100'
: 'opacity-0'} transition-opacity duration-1000"
>
<LoaderCircle class="animate-spin" />
</div>
<div
class="{!$beatmapSets
? 'opacity-0'
: 'opacity-100'} transition-opacity duration-1000"
>
{#if $reduceAnimations}
<span>{numberHumanReadable($beatmapSets ?? 0)}</span>
{:else}
<NumberFlow value={$beatmapSets ?? 0} trend={0} />
{/if}
</div>
</div>
<div class="text-muted-foreground text-[12px] leading-4">Beatmap Sets imported</div>
</div>
<div
class="bg-theme-800/90 border border-theme-700/90 rounded-lg px-2 py-4 w-full flex flex-col gap-1 items-center justify-center"
>
<div class="flex items-center justify-center p-2 rounded-lg bg-yellow-500/20">
<Brush class="text-yellow-500" size="26" />
</div>
<div class="relative font-bold text-xl text-yellow-400">
<div
class="absolute top-1 left-1/2 -translate-x-1/2 {!$skins && $skins !== 0
? 'opacity-100'
: 'opacity-0'} transition-opacity duration-1000"
>
<LoaderCircle class="animate-spin" />
</div>
<div
class="{!$skins && $skins !== 0
? 'opacity-0'
: 'opacity-100'} transition-opacity duration-1000"
>
{#if $reduceAnimations}
<span>{numberHumanReadable($skins ?? 0)}</span>
{:else}
<NumberFlow value={$skins ?? 0} trend={0} />
{/if}
</div>
</div>
<div class="text-muted-foreground text-[12px] leading-4">Skins</div>
</div>
<div
class="bg-theme-800/90 border border-theme-700/90 rounded-lg px-2 py-4 w-full flex flex-col gap-1 items-center justify-center"
>
<div
class="flex items-center justify-center p-2 rounded-lg {$serverConnectionFails > 1
? 'bg-red-500/20'
: 'bg-green-500/20'}"
>
{#if $serverConnectionFails > 1}
<WifiOff class="text-red-500" size="26" />
{:else}
<Wifi class="text-green-500" size="26" />
{/if}
</div>
<div
class="relative font-bold text-xl {$serverConnectionFails > 1
? 'text-red-400'
: 'text-green-400'}"
>
<div
class="absolute top-1 left-1/2 -translate-x-1/2 {!$serverPing ||
$serverConnectionFails > 1
? 'opacity-100'
: 'opacity-0'} transition-opacity duration-1000"
>
<LoaderCircle class="animate-spin" />
</div>
<div
class="{!$serverPing || $serverConnectionFails > 1
? 'opacity-0'
: 'opacity-100'} transition-opacity duration-1000"
>
{#if $reduceAnimations}
<span>{$serverPing}ms</span>
{:else}
<NumberFlow value={$serverPing ?? 0} trend={0} suffix="ms" />
{/if}
</div>
</div>
<div class="text-muted-foreground text-[12px] leading-4">Ping to Server</div>
</div>
</div>
<Button
size="lg"
class="text-xl h-16 px-16 border-2 border-violet-500/40"
disabled={$launching || $osuInstallationPath === '' || $serverConnectionFails > 1}
onclick={launch}
>
<Play class="!size-5" />
{$launching ? 'Launching...' : $serverConnectionFails > 1 ? 'No connection' : 'Launch'}
</Button>
</div>
<div
class="mt-auto bg-theme-900/90 border border-theme-800/90 rounded-lg px-6 py-3"
in:scale={{
duration: $reduceAnimations ? 0 : 400,
delay: $reduceAnimations ? 0 : 50,
start: 0.98,
}}
>
<div class="flex flex-row items-center gap-2">
<Gamepad2 class="text-muted-foreground" size="24" />
<span class="font-semibold text-muted-foreground text-sm">Client Info</span>
</div>
<div class="grid grid-cols-[1fr_auto] gap-1 mt-2 border-t border-theme-800 pt-2 px-2 pb-0">
<span class="text-xs text-muted-foreground font-semibold">osu! Release Stream</span>
<span class="text-xs font-semibold text-end text-theme-50">
<Badge class="text-[0.65rem] py-0.5 px-2 leading-none">
{#if $osuStream}
{releaseStreamToReadable($osuStream)}
{:else}
<LoaderCircle class="animate-spin" size={12} />
{/if}
</Badge>
</span>
<span class="text-xs text-muted-foreground font-semibold">osu! Version</span>
<span class="text-xs font-semibold text-end text-theme-50">
<Badge class="text-[0.65rem] py-0.5 px-2 leading-none">
{#if $osuBuild}
{$osuBuild}
{:else}
<LoaderCircle class="animate-spin" size={12} />
{/if}
</Badge>
</span>
<span class="text-xs text-muted-foreground font-semibold">Skin</span>
<span class="text-xs font-semibold text-end text-theme-50">
<Badge class="text-[0.65rem] py-0.5 px-2 leading-none">
{#if $currentSkin}
{$currentSkin}
{:else}
<LoaderCircle class="animate-spin" size={12} />
{/if}
</Badge>
</span>
</div>
</div>
{:else if selectedTab === 'settings'}
<div
class="bg-theme-900/90 flex flex-col justify-center gap-3 border border-theme-800/90 rounded-lg"
in:scale={{ duration: $reduceAnimations ? 0 : 400, start: 0.98 }}
>
<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 pt-4 pb-1 px-6"
> >
<div class="p-8 w-full">
<div class="bg-black/40 backdrop-blur-sm py-8 px-6 rounded-lg">
<div class="grid grid-cols-[1fr_auto] gap-y-5 items-center px-6">
<div class="flex flex-col"> <div class="flex flex-col">
<Label class="text-sm" for="setting-custom-cursor">Patching</Label> <Label class="text-sm" for="setting-patch">Patching</Label>
<div class="text-muted-foreground text-xs"> <div class="text-muted-foreground text-xs">
Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span Shows misses in Relax and Autopilot {#if $platform !== 'windows'}<span
class="text-red-500 bg-red-800/20 border border-red-600/20 p-0.5 mx-1 px-2 rounded-lg" 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 >currently only on windows!</span
> >
{/if} {/if}
</div> </div>
</div> </div>
<Checkbox <Checkbox
id="setting-custom-cursor" id="setting-patch"
checked={$platform === 'windows' ? $patch : false} checked={$platform === 'windows' ? $patch : false}
disabled={$platform !== 'windows'} disabled={$platform !== 'windows'}
onCheckedChange={async (e) => { onCheckedChange={async (e) => {
@@ -1226,7 +1204,8 @@
<div class="flex flex-col"> <div class="flex flex-col">
<Label class="text-sm" for="setting-reduce-animations">Reduce Animations</Label> <Label class="text-sm" for="setting-reduce-animations">Reduce Animations</Label>
<div class="text-muted-foreground text-xs"> <div class="text-muted-foreground text-xs">
Disables some animations in the Launcher to improve performance on low-end devices. Disables some animations in the Launcher to improve performance on low-end
devices.
</div> </div>
</div> </div>
<Checkbox <Checkbox
@@ -1260,13 +1239,13 @@
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<Label class="text-sm" for="setting-reduce-animations">App Tracking</Label> <Label class="text-sm" for="setting-tracking">App Tracking</Label>
<div class="text-muted-foreground text-xs"> <div class="text-muted-foreground text-xs">
Allow anonymous usage data to be collected to help improve the application. Allow anonymous usage data to be collected to help improve the application.
</div> </div>
</div> </div>
<Checkbox <Checkbox
id="setting-reduce-animations" id="setting-tracking"
checked={$trackingEnabled} checked={$trackingEnabled}
onCheckedChange={async (e) => { onCheckedChange={async (e) => {
trackingEnabled.set(e); trackingEnabled.set(e);
@@ -1349,23 +1328,54 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{:else if selectedView === 'login'}
<div <div
class="mt-auto mx-auto flex flex-row items-center gap-2" class="h-[100vh] w-full flex flex-col items-center justify-center"
in:scale={{ in:fly={{ duration: 400, delay: 400, y: 10, opacity: 0 }}
duration: $reduceAnimations ? 0 : 400, out:fly={{ duration: 400, y: -10, opacity: 0 }}
delay: $reduceAnimations ? 0 : 50,
start: 0.97,
}}
> >
<Button <div class="p-8 px-52 w-full">
variant="link" <form onsubmit={performLogin} class="bg-black/40 backdrop-blur-sm rounded-lg p-8">
class="font-light font-mono text-sm text-theme-100/70" <div class="flex flex-col items-center justify-center mb-4">
onclick={() => openURL('https://ez-pp.farm/u/1001')} <span class="text-xl font-semibold">Login to EZPPFarm</span>
<span class="text-xs text-muted-foreground"
>Use your EZPPFarm account to login to EZPPLauncher</span
> >
made with </div>
<Heart class="text-red-600 fill-red-600 animate-pulse" /> <div class="mb-4">
by horizoncode <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> </Button>
</form>
</div>
</div> </div>
{/if} {/if}
</div> </div>
+1 -1
View File
@@ -202,7 +202,7 @@
}); });
</script> </script>
<div class="flex flex-col items-center justify-center mt-[50px] h-[calc(100vh-50px)] w-full"> <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="relative w-80 h-80 flex items-center justify-center">
<svg <svg
class="absolute top-0 left-0 w-full h-full animate-spin" class="absolute top-0 left-0 w-full h-full animate-spin"
-127
View File
@@ -1,127 +0,0 @@
<script lang="ts">
import Logo from '$assets/logo.png';
import { ezppfarm } from '@/api/ezpp';
import Button from '@/components/ui/button/button.svelte';
import Input from '@/components/ui/input/input.svelte';
import Label from '@/components/ui/label/label.svelte';
import { currentView } from '@/global';
import { currentUser, userAuth } from '@/userAuthentication';
import { animate } from 'animejs';
import { LoaderCircle } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import Launch from './Launch.svelte';
import { currentUserInfo } from '@/data';
import { preferredMode, preferredType } from '@/userSettings';
let username = $state('');
let password = $state('');
let isLoading = $state(false);
let ezppLogo: HTMLImageElement | undefined = $state(undefined);
const logo_mouseenter = () => {
if (ezppLogo) {
animate(ezppLogo, {
duration: 700,
scale: 1.2,
ease: (t: number) => Math.pow(2, -5 * t) * Math.sin((t - 0.075) * 20.94) + 1 - 0.0005 * t,
});
}
};
const logo_mouseleave = () => {
if (ezppLogo) {
animate(ezppLogo, {
duration: 700,
scale: 1,
ease: (t: number) => (t - 1) ** 7 + 1,
});
}
};
const performLogin = async () => {
isLoading = true;
try {
const loginResult = await ezppfarm.login(username, password);
if (loginResult && loginResult.user) {
toast.success('Login successful!', {
description: `Welcome back, ${loginResult.user.name}!`,
});
$userAuth.value('username').set(username);
$userAuth.value('password').set(password);
await $userAuth.save();
currentUser.set(loginResult.user);
currentView.set(Launch);
} else {
toast.error('Login failed!', {
description: 'Please check your username and password.',
});
isLoading = false;
}
} catch {
toast.error('Server error occurred during login.', {
description: 'There was an issue connecting to the server. Please try again later.',
});
isLoading = false;
}
if ($currentUser) {
const userInfo = await ezppfarm.getUserInfo($currentUser.id);
if (userInfo) {
currentUserInfo.set(userInfo.player);
preferredMode.set(userInfo.player.info.preferred_mode);
preferredType.set(userInfo.player.info.preferred_type);
}
}
};
</script>
<div class="mt-[50px] h-[calc(100vh-50px)] w-full">
<div class="w-full h-full flex flex-col items-center justify-center">
<img
src={Logo}
alt="EZPPLauncher Logo"
class="w-52 h-52 mb-2"
bind:this={ezppLogo}
onmouseenter={logo_mouseenter}
onmouseleave={logo_mouseleave}
/>
<form onsubmit={performLogin} class="w-full max-w-sm">
<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={isLoading}
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={isLoading}
autocomplete="off"
autocorrect="off"
/>
</div>
<Button class="w-full" type="submit" disabled={isLoading}>
{#if isLoading}
<LoaderCircle class="animate-spin" />
{:else}
Login
{/if}
</Button>
</form>
</div>
</div>
+5 -5
View File
@@ -119,7 +119,7 @@
</script> </script>
{#if wizardFinished} {#if wizardFinished}
<div class="relative flex flex-col items-center justify-center mt-[50px] h-[calc(100vh-50px)]"> <div class="relative flex flex-col items-center justify-center h-[100vh]">
<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,7 +128,7 @@
<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] mt-[50px] h-[calc(100vh-50px)]"> <div class="grid grid-cols-[0.41fr_1fr] h-[100vh]">
<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-theme-800/90 flex flex-col gap-6 p-3">
{#each steps as step, i (step)} {#each steps as step, i (step)}
<div <div
@@ -163,7 +163,7 @@
<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 bg-theme-900/40 p-6">
{#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 flex flex-col items-center justify-center" 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"
in:fade={{ duration: $reduceAnimations ? 0 : 200 }} in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
> >
<img <img
@@ -184,7 +184,7 @@
</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 flex flex-col items-center justify-center" 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"
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>
@@ -242,7 +242,7 @@
</div> </div>
{:else if selectedStep === 3} {:else if selectedStep === 3}
<div <div
class="bg-theme-900/90 flex flex-col justify-center gap-3 border border-theme-800/90 rounded-lg" 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 }} in:fade={{ duration: $reduceAnimations ? 0 : 200 }}
> >
<div class="flex flex-row items-center gap-3 font-semibold text-xl px-3 pt-3"> <div class="flex flex-row items-center gap-3 font-semibold text-xl px-3 pt-3">