Compare commits

..

145 Commits

Author SHA1 Message Date
HorizonCode eaffc35288 fix: build for linux 2026-06-03 01:01:17 +02:00
HorizonCode 78c7fdb756 chore: bump version 2026-06-03 01:01:03 +02:00
HorizonCode ec7ce53016 chore: bump version 2026-06-03 00:51:28 +02:00
HorizonCode 0610d4beef chore: make patch settings available to linux users, force stable patcher stream 2026-06-03 00:49:35 +02:00
HorizonCode 416067f2ea Merge branch 'master' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher 2026-06-02 23:46:02 +02:00
HorizonCode e58be09bdb chore: add patcher support for linux 2026-06-02 23:45:43 +02:00
HorizonCode ccb0c467f2 chore: add type usage for views in selectedView 2026-06-01 10:47:58 +02:00
HorizonCode 67bc968497 feat: add disableMediaControls function to prevent media key actions 2026-05-30 00:06:16 +02:00
HorizonCode dc06b53089 fix: if external imported theme is current active theme change to default for installation 2026-05-29 21:36:37 +02:00
HorizonCode d5e5fd4c43 fix: csp font-src rule, remove debug log 2026-05-28 22:52:46 +02:00
HorizonCode 29359c9242 chore: bump version 2026-05-28 22:34:34 +02:00
HorizonCode 71bd64a2cf fix: ahead value read 2026-05-28 22:21:18 +02:00
HorizonCode 7fa83d6d0a chore: add theme css support 2026-05-28 22:02:38 +02:00
HorizonCode f2e795e2c9 chore: remove old theme variables 2026-05-28 22:02:07 +02:00
HorizonCode cece28dd46 fix: only validate psu path if its not empty 2026-05-28 21:53:19 +02:00
HorizonCode 474f1f0c33 Merge branch 'master' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher 2026-05-28 11:41:26 +02:00
HorizonCode 871c40922e chore: replace json config with database 2026-05-28 11:35:42 +02:00
HorizonCode 20e8389409 chore: replace json config with database 2026-05-28 11:35:34 +02:00
HorizonCode 051a86943e fix: update Content Security Policy to include analytics and osu.ppy.sh 2026-05-28 00:39:45 +02:00
HorizonCode 7d0147edb9 fix: Content Security Policy rules 2026-05-28 00:09:23 +02:00
HorizonCode 7e2404753b fix: set csp to null, breaks assets in prod 2026-05-27 23:31:32 +02:00
HorizonCode 9952e164c5 chore: add new feature to readme 2026-05-27 23:15:59 +02:00
HorizonCode df8ee34e73 chore: move import of Stdio to head 2026-05-27 19:55:56 +02:00
HorizonCode 3a27646da8 chore: add validation for OpenTabletDriver path and improve process handling 2026-05-27 19:31:53 +02:00
HorizonCode ace3320632 chore: implement otd commands, check if otd daemon is already running 2026-05-27 18:15:50 +02:00
HorizonCode 2ba094f07e chore: remove theme delete logging, add notifications 2026-05-27 15:02:26 +02:00
HorizonCode fcf9e46a8f chore: remove theme delete logging, add notifications 2026-05-27 15:02:06 +02:00
HorizonCode 3768ab1064 chore: move download and extracting themes to tauri backend 2026-05-27 14:59:43 +02:00
HorizonCode e5e09505a0 chore: remove old unused nyancat bg 2026-05-27 11:45:27 +02:00
HorizonCode 94f78f281d chore: move theme extraction to tauri backend 2026-05-27 11:44:16 +02:00
HorizonCode d2ecc5210f refactor: replace @ alias with 2026-05-27 10:41:57 +02:00
HorizonCode 6f8036e9d4 refactor: fix tw class names 2026-05-27 10:39:00 +02:00
HorizonCode 73256f32c4 fix: correct className usage in badge component 2026-05-26 16:18:29 +02:00
HorizonCode cfb4c8a611 chore: lint 2026-05-26 14:46:29 +02:00
HorizonCode f2d8056cfd chore: update sileo type props 2026-05-26 13:31:07 +02:00
HorizonCode 2eb95cd802 chore: update dependencies 2026-05-26 13:09:30 +02:00
HorizonCode a28d56e283 fix: correct className usage in button component for proper styling 2026-05-26 12:59:26 +02:00
HorizonCode f88a33c2aa fix: if theme is a store theme and installed externally, update status in importFromFile 2026-05-26 11:39:36 +02:00
HorizonCode 8a964d0a6a chore: add import sort plugin, sort imports 2026-05-26 11:23:37 +02:00
HorizonCode cc4697efbc refactor: simplify stopOpenTabletDriver, cleanup code 2026-05-26 10:33:14 +02:00
HorizonCode 5b1e32d4eb chore: bump version and enhance security settings in tauri configuration 2026-05-25 21:54:45 +02:00
HorizonCode d0b0011910 chore: automatically start/stop otd on osu launch/close 2026-05-25 21:53:42 +02:00
HorizonCode 1467d3e296 fix: adjust layout and positioning of user stats and login prompts in Launch screen 2026-05-24 16:41:01 +02:00
HorizonCode 2ded0d4b94 feat: add login and registration prompts to the launch screen 2026-05-24 16:33:48 +02:00
HorizonCode 9d80811e83 fix: improve error handling and logging in various components 2026-05-24 16:17:16 +02:00
HorizonCode 730a27b397 chore: remove doctest 2026-05-22 13:23:04 +02:00
HorizonCode e84feb292b Merge branch 'master' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher 2026-05-22 08:18:54 +02:00
HorizonCode 485cb3acf7 fix: clear opened URLs after retrieving them 2026-05-22 08:17:02 +02:00
HorizonCode 44ad025d3d fix: font importing 2026-05-21 23:12:41 +02:00
HorizonCode ec80b13de3 chore: remove unneeded tailwind config file 2026-05-21 23:12:03 +02:00
HorizonCode 6a279de728 chore: update deps, switch from lucide-svelte to @lucide/svelte 2026-05-21 23:02:45 +02:00
HorizonCode 4e33ec3451 fix: resolve vite dynamic import warning 2026-05-21 23:02:14 +02:00
HorizonCode 6fdb2376a1 chore: switch to async rpc framework, fix buttons 2026-05-21 21:24:17 +02:00
HorizonCode afdd74175a fix: cursor smooth movement on higher refreshrate monitors 2026-05-21 20:30:00 +02:00
HorizonCode 6f8043d2d0 fix: cursor rotating on higher refreshrate monitors 2026-05-21 20:23:04 +02:00
HorizonCode f7fff7748a chore: format 2026-05-21 15:31:13 +02:00
HorizonCode d5286263b9 chore: add nsis hook on uninstall to delete user .ezpplauncher dir 2026-05-21 15:30:54 +02:00
HorizonCode 1cc1a4a0c3 chore: remove theme debug script 2026-05-21 14:54:33 +02:00
HorizonCode d2ae9e939f chore: add dynamic button for beatmap display 2026-05-21 14:48:10 +02:00
HorizonCode bde922ab9f fix: file theme import by association file 2026-05-21 14:26:16 +02:00
HorizonCode a1f96a7de3 fix: build 2026-05-21 14:25:13 +02:00
HorizonCode 3b2c902264 fix: build 2026-05-21 14:25:08 +02:00
HorizonCode 40a32d7a60 chore: add theme import for file association 2026-05-21 13:27:31 +02:00
HorizonCode 52c60acf37 chore: add drag and drop theme import 2026-05-21 13:23:03 +02:00
HorizonCode 2788cd6e8b chore: format 2026-05-21 11:44:56 +02:00
HorizonCode 2d1941db33 chore: add drag-and-drop box to Launch page 2026-05-21 11:43:59 +02:00
HorizonCode 00958d5238 chore: only handle .ezpplauncher-theme files 2026-05-21 10:51:51 +02:00
HorizonCode b8a0b448c7 fix: type def for beatmapSetCount 2026-05-21 10:42:13 +02:00
HorizonCode 09487e945e fix: cleanup type defs 2026-05-21 10:41:22 +02:00
HorizonCode 2532ab0059 fix: add ezpplauncher-theme file association to tauri.config.json 2026-05-21 10:27:44 +02:00
HorizonCode 16fe91b3a5 chore: rework presence, add dynamic button functions to presence 2026-05-21 09:21:15 +02:00
HorizonCode eca08f16c5 fix: wait spawning patcher until osu is spawned 2026-05-21 08:18:50 +02:00
HorizonCode e76d67f3a8 fix: add missing imports 2026-05-20 16:32:26 +02:00
HorizonCode 9811b36e58 wip: theme file import from fs 2026-05-20 16:09:05 +02:00
HorizonCode 0f10beb3b4 chore: bump version 2026-05-08 18:06:58 +02:00
HorizonCode 4326199a56 chore: add theme hash checking and alerts 2026-05-08 18:03:26 +02:00
HorizonCode 7f65384b1c chore: add code signing to vscode config 2026-05-08 18:02:32 +02:00
HorizonCode aacd0d1d91 chore: update readme 2026-05-08 18:02:16 +02:00
HorizonCode 079a076f99 chore: update launcher preview 2026-05-08 18:01:55 +02:00
HorizonCode 6d88c39ce2 chore: pause media when launcher is hidden 2026-05-03 17:50:06 +02:00
HorizonCode 56608a64fc chore: use folder name as identification 2026-05-03 15:24:31 +02:00
HorizonCode eaf6f5976d fix: always use underscore 2026-05-03 00:17:50 +02:00
HorizonCode 93f1f380ec fix: regex es6 issue 2026-05-03 00:16:26 +02:00
HorizonCode c47ea1f976 fix: toSafeName windows forbidden symbols 2026-05-03 00:09:01 +02:00
HorizonCode 09af78f1aa fix: use actual setted volume for fade 2026-05-02 22:21:28 +02:00
HorizonCode 3900b5c6cf chore: add volume slider
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 22:15:39 +02:00
HorizonCode adf607e24c chore: clean up comments 2026-05-02 19:56:37 +02:00
HorizonCode 1128b51778 fix: multiple downloads 2026-05-02 19:55:49 +02:00
HorizonCode d435f0c09a chore: add theme uninstall and update 2026-05-02 02:19:00 +02:00
HorizonCode b188e2dee8 chore: remove bundled themes 2026-05-02 00:39:47 +02:00
HorizonCode 8fd56ae749 chore: add custom themes
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 00:38:56 +02:00
HorizonCode b2e7d22d28 chore: add lost umbrella video 2026-04-29 23:33:51 +02:00
HorizonCode d871e06172 chore: add execution clap video 2026-04-29 23:33:15 +02:00
HorizonCode 6a1c5441c3 chore: remove konami code, add themes 2026-04-29 23:32:47 +02:00
HorizonCode ef36f49a6f fix: improve presence reconnection logic and error handling
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 14:04:06 +02:00
HorizonCode 7e40536ec8 chore: add installerIcon to NSIS configuration 2026-04-26 11:29:31 +02:00
HorizonCode 694d403aeb chore: bump version 2026-04-26 11:19:53 +02:00
HorizonCode 9515892b2f fix: remove console log 2026-04-26 11:19:42 +02:00
HorizonCode 0a58f0f63b chore: update profile settings 2026-04-26 11:19:27 +02:00
HorizonCode 60e3ef4131 chore: add umami typing 2026-04-21 09:25:59 +02:00
HorizonCode 7375cccc05 chore: bump version 2026-04-20 12:16:26 +02:00
HorizonCode 251bf8177c chore: add all avalilable fields to beatmap struct 2026-04-20 08:22:40 +02:00
HorizonCode bf4ce18671 chore: replace osynic_osudb with own osudb parser 2026-04-19 18:43:22 +02:00
HorizonCode 4dc83c882d todo: check if this is still needed for linux 2026-04-18 02:34:16 +02:00
HorizonCode e20f11be64 chore: bump version 2026-04-18 02:34:00 +02:00
HorizonCode 407996269e fix: do not save config in setupwizard on value change 2026-04-18 02:33:43 +02:00
HorizonCode 95c6db6a1d fix: remove unused import, set firstStartup on top 2026-04-18 02:33:24 +02:00
HorizonCode a87a4bcc54 chore: bump version and update deps 2026-04-18 02:32:54 +02:00
HorizonCode 97f072f4ce refactor: improve error handling and code readability in replace_ui_files and check_for_corruption functions 2026-04-14 22:52:16 +02:00
HorizonCode 1aa3070ea1 refactor: remove PermissionDenied variant from ReplaceUIError enum 2026-04-14 22:37:33 +02:00
HorizonCode d3c3e4e771 refactor: replace symlink approach with a marker file approach 2026-04-14 22:36:26 +02:00
HorizonCode 921dbd716c chore: update dependencies and version bump to 3.1.3 2026-04-12 22:46:15 +02:00
HorizonCode 34e82f89e8 chore: remove todo note 2026-04-11 21:50:30 +02:00
HorizonCode 901fbefd1b chore: remove unused imports 2026-04-10 12:00:16 +02:00
HorizonCode a795978827 Merge branch 'master' of https://git.ez-pp.farm/EZPPFarm/EZPPLauncher 2026-04-10 11:59:20 +02:00
HorizonCode a9152cc376 chore: remove unused imports 2026-04-10 11:59:18 +02:00
HorizonCode 8d790c7da3 chore: improve UI file replacement logic and fix corruption check 2026-04-10 11:58:11 +02:00
HorizonCode f5a2c4365b fix: check connection issues on load 2026-03-20 13:33:07 +01:00
HorizonCode a925548ae9 refactor: conditionally render patcher release stream for Windows platform 2026-03-05 21:24:37 +01:00
HorizonCode a6495ea353 fix: add schema.tauri.app as trusted schema url 2026-03-03 23:22:10 +01:00
HorizonCode 24802b5132 fix: read beatmapset count from osu!.db instead of iterating songs folder 2026-03-01 23:50:36 +01:00
HorizonCode f09405ed58 refactor: enhance audio fade-in logic for improved user experience 2026-03-01 23:50:06 +01:00
HorizonCode eeda85f48f style: enhance button animations in titlebar for improved visual feedback 2026-03-01 23:49:42 +01:00
HorizonCode 9770e81d6f refactor: improve presence handling and update disconnect logic for clarity 2026-02-28 16:56:46 +01:00
HorizonCode 31b7d17b09 feat: implement sorting functionality for skins using default windows sorting algotythm 2026-02-23 18:23:46 +01:00
HorizonCode 1fc0391113 style: format code for consistency and readability in sileo-toast component 2026-02-22 20:18:37 +01:00
HorizonCode 62f03cf7c0 fix: update ESLint configuration to disable no-empty rule and adjust unused vars handling 2026-02-22 20:18:21 +01:00
HorizonCode 94f6e850cf feat: update icons and launcher files 2026-02-22 20:14:38 +01:00
HorizonCode d6fdb2a152 chore: update Tauri dependencies to latest versions 2026-02-22 20:14:11 +01:00
HorizonCode 9cd990cf96 chore: update dependencies in Cargo.toml 2026-02-22 20:10:15 +01:00
HorizonCode 2e0ef81bcb feat: implement skin management 2026-02-22 17:05:52 +01:00
HorizonCode 96680ff8b5 fix: update button styles to use ring for better accessibility 2026-02-22 14:12:32 +01:00
HorizonCode 0b9d6373bf fix: add fade to bg and audio 2026-02-22 14:03:44 +01:00
HorizonCode 295c1424c9 chore: add audio 2026-02-22 12:35:08 +01:00
HorizonCode 0e12a801d2 chore: add secret :^) 2026-02-22 00:21:42 +01:00
HorizonCode cf86dba5c4 chore: rework titlebar buttons 2026-02-21 23:31:43 +01:00
HorizonCode 540c653399 fix: reconnect presence after pipe was closed 2026-02-21 23:18:01 +01:00
HorizonCode c9b8d9f599 fix: respect reducedAnimations 2026-02-21 22:59:19 +01:00
HorizonCode 49665d94bc fix: Setup WIzard styling 2026-02-19 19:38:21 +01:00
HorizonCode 1a19c19399 chore: bump version 2026-02-19 15:48:26 +01:00
HorizonCode cbfe5ec868 fix: Setup Wizard not working 2026-02-19 15:41:29 +01:00
HorizonCode c5b18dacbb chore: update lucide-svelte 2026-02-19 15:41:10 +01:00
HorizonCode ef01984875 fix: osu installation detection 2026-02-19 15:40:56 +01:00
HorizonCode 46ee36ce81 fix: disable all buttons and dropdowns on launch 2026-02-17 20:34:57 +01:00
HorizonCode c4658ae4cc fix: sort streaks by depth 2026-02-17 20:13:43 +01:00
150 changed files with 6621 additions and 2520 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"bracketSpacing": true, "bracketSpacing": true,
"plugins": ["prettier-plugin-svelte"], "plugins": ["prettier-plugin-svelte", "@trivago/prettier-plugin-sort-imports"],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",
+11
View File
@@ -1,7 +1,18 @@
{ {
"svelte.enable-ts-plugin": true, "svelte.enable-ts-plugin": true,
"git.enableCommitSigning": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"[rust]": { "[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer" "editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"json.schemaDownload.trustedDomains": {
"https://schemastore.azurewebsites.net/": true,
"https://raw.githubusercontent.com/": true,
"https://www.schemastore.org/": true,
"https://json.schemastore.org/": true,
"https://json-schema.org/": true,
"https://schema.tauri.app/": true
} }
} }
+8
View File
@@ -14,6 +14,7 @@ It enhances the osu! experience with quality-of-life features and integration sp
## 🚀 Features ## 🚀 Features
- [x] Automatically updates osu! before launching - [x] Automatically updates osu! before launching
- [x] Automatically start/stop the OpenTabletDriver Daemon
- [x] Automatic ingame login - [x] Automatic ingame login
- [x] Patches the osu! client to show misses in Relax and Autopilot - [x] Patches the osu! client to show misses in Relax and Autopilot
- [x] Displays your EZPPFarm stats in the launcher - [x] Displays your EZPPFarm stats in the launcher
@@ -22,6 +23,7 @@ It enhances the osu! experience with quality-of-life features and integration sp
- [x] Displays ping to the EZPPFarm server - [x] Displays ping to the EZPPFarm server
- [x] Discord Rich Presence - [x] Discord Rich Presence
- [x] Performance Display Overlay (currently in experimental patcher release stream) - [x] Performance Display Overlay (currently in experimental patcher release stream)
- [x] Launcher Theming
--- ---
@@ -73,3 +75,9 @@ src-tauri/target/release/ezpplauncher.exe
├── package.json # Bun project config ├── package.json # Bun project config
└── README.md └── README.md
``` ```
---
## 🎨 Launcher Themes
By default, the launcher includes several downloadable themes. If you want to create your own custom theme, please use the [create-ezpplauncher-theme](https://git.ez-pp.farm/EZPPFarm/create-ezpplauncher-theme) repository for a template and instructions.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 157 KiB

+336 -356
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"$schema": "https://next.shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": { "tailwind": {
"css": "src\\app.css", "css": "src\\app.css",
"baseColor": "gray" "baseColor": "gray"
@@ -12,5 +12,5 @@
"lib": "$lib" "lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://tw3.shadcn-svelte.com/registry/default" "registry": "https://shadcn-svelte.com/registry"
} }
+11 -2
View File
@@ -1,11 +1,11 @@
import prettier from 'eslint-config-prettier'; import svelteConfig from './svelte.config.js';
import { includeIgnoreFile } from '@eslint/compat'; import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js'; import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte'; import svelte from 'eslint-plugin-svelte';
import globals from 'globals'; import globals from 'globals';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint'; import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
@@ -36,5 +36,14 @@ export default ts.config(
svelteConfig, svelteConfig,
}, },
}, },
},
{
rules: {
'no-empty': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^\\$\\$(Props|Events|Slots|Generic)$' },
],
},
} }
); );
+47 -46
View File
@@ -1,13 +1,13 @@
{ {
"name": "ezpplauncher", "name": "ezpplauncher",
"version": "3.1.1", "version": "3.2.3",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
"debug": "bun run tauri dev", "debug": "bun run tauri dev",
"build": "bun ./scripts/sync-version.ts && bun run vite:build && bun run tauri:build", "build": "bun ./scripts/build.ts",
"vite:dev": "vite dev", "vite:dev": "vite dev",
"vite:build": "vite build", "vite:build": "vite build && exit 0",
"tauri:build": "tauri build", "tauri:build": "tauri build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -18,64 +18,65 @@
}, },
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@better-fetch/fetch": "^1.1.18", "@better-fetch/fetch": "^1.1.21",
"@fontsource/sora": "^5.2.6", "@fontsource-variable/sora": "^5.2.8",
"@fontsource/space-mono": "^5.2.8", "@fontsource/space-mono": "^5.2.9",
"@number-flow/svelte": "^0.3.9", "@number-flow/svelte": "^0.4.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.3.0",
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.4.4", "@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-shell": "^2.3.3", "@tauri-apps/plugin-shell": "^2.3.5",
"@types/umami": "^2.10.1", "@types/node": "^25.9.1",
"animejs": "^4.1.3", "animejs": "^4.4.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"lucide-svelte": "^0.562.0", "jszip": "^3.10.1",
"react": "^19.2.4", "react": "^19.2.6",
"react-dom": "^19.2.4", "react-dom": "^19.2.6",
"react-to-webcomponent": "^2.0.1", "react-to-webcomponent": "^2.0.1",
"semver": "^7.7.2", "semver": "^7.8.1",
"sileo": "^0.1.3", "sileo": "^0.1.5",
"svelte-confetti": "^2.3.2", "svelte-confetti": "^2.3.2",
"tw-animate-css": "^1.3.8" "tw-animate-css": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.0", "@eslint/compat": "^2.1.0",
"@eslint/js": "^9.35.0", "@eslint/js": "^10.0.1",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@lucide/svelte": "^0.562.0", "@lucide/svelte": "^1.16.0",
"@sveltejs/adapter-static": "^3.0.9", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.37.1", "@sveltejs/kit": "^2.61.1",
"@sveltejs/vite-plugin-svelte": "^6.1.4", "@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2.9.6", "@tauri-apps/cli": "^2.11.2",
"@types/bun": "^1.2.21", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/bun": "^1.3.14",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"autoprefixer": "10.4.21", "autoprefixer": "10.5.0",
"bits-ui": "^1.4.7", "bits-ui": "^2.18.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.35.0", "eslint": "^10.4.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.2", "eslint-plugin-svelte": "^3.17.1",
"globals": "^17.0.0", "globals": "^17.6.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
"prettier": "^3.6.2", "prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^4.0.1",
"sass-embedded": "^1.92.1", "sass-embedded": "^1.100.0",
"svelte": "^5.38.7", "svelte": "^5.55.9",
"svelte-check": "^4.3.1", "svelte-check": "^4.4.8",
"svelte-sonner": "^1.0.5", "svelte-sonner": "^1.1.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.1.0", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.2", "typescript": "^6.0.3",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.60.0",
"vite": "^7.1.5", "vite": "^8.0.14",
"vite-plugin-devtools-json": "^1.0.0" "vite-plugin-devtools-json": "^1.0.0"
} }
} }
+40
View File
@@ -0,0 +1,40 @@
import { spawn } from 'child_process';
function runVite(): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('bunx', ['--bun', 'vite', 'build'], { stdio: ['inherit', 'pipe', 'pipe'] });
proc.stdout?.on('data', (data: Buffer) => {
process.stdout.write(data);
if (data.toString().includes('✔ done')) {
setTimeout(() => {
proc.kill();
resolve();
}, 500);
}
});
proc.stderr?.on('data', (data: Buffer) => process.stderr.write(data));
proc.on('close', (code) => {
if (code === 0 || code === null) resolve();
else reject(new Error(`vite build exited with code ${code}`));
});
});
}
function run(cmd: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn(cmd, args, { stdio: 'inherit' });
proc.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`${cmd} exited with code ${code}`));
});
});
}
await run('bun', ['./scripts/sync-version.ts']);
await runVite();
//set NO_STRIP=1 in env for linux to prevent stripping of debug symbols in tauri builds, which causes issues with source maps
if (process.platform === 'linux') process.env.NO_STRIP = '1';
await run('bunx', ['tauri', 'build']);
+1 -1
View File
@@ -1,6 +1,6 @@
import * as toml from '@iarna/toml';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import * as toml from '@iarna/toml';
import { cwd } from 'process'; import { cwd } from 'process';
const packageJsonPath = path.join(cwd(), 'package.json'); const packageJsonPath = path.join(cwd(), 'package.json');
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"types": ["@types/bun", "@types/node"]
},
"include": ["*.ts"]
}
+1438 -526
View File
File diff suppressed because it is too large Load Diff
+40 -19
View File
@@ -1,12 +1,22 @@
[package] [package]
name = "ezpplauncher" name = "ezpplauncher"
version = "3.1.1" version = "3.2.3"
description = "EZPPLauncher redefined." description = "EZPPLauncher redefined."
authors = ["HorizonCode"] authors = ["HorizonCode"]
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.dev]
incremental = true # Compile your binary in smaller steps.
[profile.release]
codegen-units = 1 # Allows LLVM to perform better optimization.
lto = true # Enables link-time-optimizations.
opt-level = "s" # Prioritizes small binary size. Use `3` if you prefer speed.
panic = "abort" # Higher performance by disabling panic handlers.
strip = true # Ensures debug symbols are removed.
[lib] [lib]
# The `_lib` suffix may seem redundant but it is necessary # The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name. # to make the lib name unique and wouldn't conflict with the bin name.
@@ -15,33 +25,44 @@ name = "ezpplauncher_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.5.3", features = [] } tauri-build = { version = "2.6.2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.9.5", features = [] } tauri = { version = "2.11.2", features = ["protocol-asset"] }
tauri-plugin-shell = "2.3.3" tauri-plugin-shell = "2.3.5"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148" serde_json = "1.0.150"
serde_repr = "0.1.20" serde_repr = "0.1.20"
tauri-plugin-dialog = "2.4.2" tauri-plugin-dialog = "2.7.1"
tauri-plugin-fs = "2.4.4" tauri-plugin-fs = "2.5.1"
hardware-id = "0.3.0" hardware-id = "0.3.0"
tauri-plugin-cors-fetch = "4.1.0" tauri-plugin-cors-fetch = "5.0.0"
sysinfo = "0.37.2" sysinfo = "0.39.2"
reqwest = { version = "0.13.1", features = ["json", "stream", "query"] } reqwest = { version = "0.13.4", features = ["json", "stream", "query"] }
md5 = "0.8.0" md5 = "0.8.0"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
open = "5.3.3" open = "5.3.5"
windows-sys = "0.61.2" presenceforge = { version = "0.2.1", features = ["tokio-runtime"] }
discord-rich-presence = "1.0.0" once_cell = "1.21.4"
once_cell = "1.21.3" futures-util = "0.3.32"
byteorder = "1.5.0"
anyhow = "1.0.102"
url = "2.5.8"
zip = "8.6.0"
sha1 = "0.11.0"
hex = "0.4.3"
rusqlite = { version = "0.40.0", features = ["bundled"] }
aes-gcm = "0.10.3"
base64 = "0.22.1"
sha2 = "0.11.0"
thiserror = "2.0.18"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.55.0" winreg = "0.56.0"
winapi = { version = "0.3", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] } winapi = { version = "0.3.9", features = ["winuser", "wincrypt", "memoryapi", "winbase", "dpapi"] }
base64 = "0.22.1"
widestring = "1.2.1" widestring = "1.2.1"
windows-sys = "0.61.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.3.6" tauri-plugin-single-instance = "2.4.2"
+94 -22
View File
@@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": [
"main"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"shell:allow-open", "shell:allow-open",
@@ -19,57 +21,127 @@
{ {
"identifier": "fs:allow-read", "identifier": "fs:allow-read",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
}, },
{ {
"identifier": "fs:allow-write", "identifier": "fs:allow-write",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
}, },
{ {
"identifier": "fs:allow-exists", "identifier": "fs:allow-exists",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
}, },
{ {
"identifier": "fs:allow-read-file", "identifier": "fs:allow-read-file",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
}, },
{ {
"identifier": "fs:allow-write-file", "identifier": "fs:allow-write-file",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
}, },
{ {
"identifier": "fs:allow-read-text-file", "identifier": "fs:allow-read-text-file",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
}, },
{ {
"identifier": "fs:allow-mkdir", "identifier": "fs:allow-mkdir",
"allow": [ "allow": [
{ "path": "$HOME/.ezpplauncher" }, {
{ "path": "$HOME/.ezpplauncher/*" }, "path": "$HOME/.ezpplauncher"
{ "path": "$HOME/.ezpplauncher/**/*" } },
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-read-dir",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
]
},
{
"identifier": "fs:allow-remove",
"allow": [
{
"path": "$HOME/.ezpplauncher"
},
{
"path": "$HOME/.ezpplauncher/*"
},
{
"path": "$HOME/.ezpplauncher/**/*"
}
] ]
} }
] ]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

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

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

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

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+15
View File
@@ -4,3 +4,18 @@
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"' Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
${EndIf} ${EndIf}
!macroend !macroend
!macro NSIS_HOOK_PREUNINSTALL
${If} $DeleteAppDataCheckboxState = 1
MessageBox MB_YESNO "Do you want to remove all EZPPLauncher data and themes?" IDYES do_delete IDNO skip_delete
do_delete:
StrCpy $R9 1
skip_delete:
${EndIf}
!macroend
!macro NSIS_HOOK_POSTUNINSTALL
${If} $R9 = 1
RMDir /r "$PROFILE\.ezpplauncher"
${EndIf}
!macroend
+658 -95
View File
@@ -1,24 +1,40 @@
use crate::config::ConfigError;
use hardware_id::get_id; use hardware_id::get_id;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Stdio;
use sysinfo::System; use sysinfo::System;
use tauri::AppHandle; use tauri::AppHandle;
use tauri::Emitter; use tauri::Emitter;
use tauri::Manager; use tauri::Manager;
use tauri::State;
use tauri_plugin_fs::FsExt;
use tokio::fs; use tokio::fs;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::process::Command; use tokio::process::Command;
use tokio::time::{Duration, sleep}; use tokio::time::{Duration, sleep};
use crate::osudb::parse_osudb;
use crate::presence; use crate::presence;
use crate::state::AppState;
use crate::utils::{ use crate::utils::{
check_folder_completeness, encrypt_password, get_osu_config, get_osu_user_config, check_folder_completeness, encrypt_password, get_osu_config, get_osu_user_config,
get_window_title_by_pid, is_net8_installed, is_osuwinello_available, is_wmctrl_available, get_window_title_by_pid, is_net8_installed, is_osuwinello_available, is_wmctrl_available,
set_osu_config_vals, set_osu_user_config_vals, set_osu_config_vals, set_osu_user_config_vals,
}; };
#[tauri::command]
pub fn opened_urls(app: tauri::AppHandle) -> Vec<PathBuf> {
let args = app.state::<AppState>().opened_files.lock().unwrap().clone();
app.state::<AppState>().opened_files.lock().unwrap().clear();
args
}
#[tauri::command] #[tauri::command]
pub fn get_launcher_version() -> String { pub fn get_launcher_version() -> String {
env!("CARGO_PKG_VERSION").to_string() env!("CARGO_PKG_VERSION").to_string()
@@ -63,24 +79,19 @@ pub fn valid_osu_folder(folder: String) -> bool {
check_folder_completeness(path, &osu_folder_files) >= 70.0 check_folder_completeness(path, &osu_folder_files) >= 70.0
} }
#[cfg(not(windows))]
#[tauri::command]
pub fn find_osu_installation() -> Option<String> {
None
}
#[cfg(windows)]
#[tauri::command] #[tauri::command]
pub fn find_osu_installation() -> Option<String> { pub fn find_osu_installation() -> Option<String> {
#[cfg(windows)]
{
use winreg::RegKey; use winreg::RegKey;
use winreg::enums::*; use winreg::enums::*;
let hklm_registry_paths = ["SOFTWARE\\Classes\\osu\\DefaultIcon"]; let hklm_registry_paths = ["SOFTWARE\\Classes\\osu\\DefaultIcon"];
let hkcr_registry_paths = [ let hkcu_registry_paths = [
"osustable.File.osk\\DefaultIcon", "Software\\Classes\\osustable.File.osk\\DefaultIcon",
"osustable.File.osr\\DefaultIcon", "Software\\Classes\\osustable.File.osr\\DefaultIcon",
"osustable.File.osz\\DefaultIcon", "Software\\Classes\\osustable.File.osz\\DefaultIcon",
]; ];
let osu_folder_files = [ let osu_folder_files = [
@@ -118,7 +129,8 @@ pub fn find_osu_installation() -> Option<String> {
let stripped = trimmed.strip_suffix(",0").unwrap_or(trimmed); let stripped = trimmed.strip_suffix(",0").unwrap_or(trimmed);
let path = PathBuf::from(stripped.trim()); let path = PathBuf::from(stripped.trim());
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
let match_percentage = check_folder_completeness(parent, &osu_folder_files); let match_percentage =
check_folder_completeness(parent, &osu_folder_files);
if match_percentage >= 70.0 { if match_percentage >= 70.0 {
return Some(parent.to_string_lossy().into()); return Some(parent.to_string_lossy().into());
@@ -129,10 +141,10 @@ pub fn find_osu_installation() -> Option<String> {
} }
} }
let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); let hkcu = RegKey::predef(HKEY_CURRENT_USER);
for reg_path in &hkcr_registry_paths { for reg_path in &hkcu_registry_paths {
if let Ok(subkey) = hkcr.open_subkey_with_flags(reg_path, KEY_READ | KEY_WOW64_32KEY) { if let Ok(subkey) = hkcu.open_subkey_with_flags(reg_path, KEY_READ | KEY_WOW64_32KEY) {
let value_names = [""]; let value_names = [""];
for value_name in &value_names { for value_name in &value_names {
if let Ok(value) = subkey.get_value::<String, _>(value_name) { if let Ok(value) = subkey.get_value::<String, _>(value_name) {
@@ -140,7 +152,8 @@ pub fn find_osu_installation() -> Option<String> {
let stripped = trimmed.strip_suffix(",1").unwrap_or(trimmed); let stripped = trimmed.strip_suffix(",1").unwrap_or(trimmed);
let path = PathBuf::from(stripped.trim()); let path = PathBuf::from(stripped.trim());
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
let match_percentage = check_folder_completeness(parent, &osu_folder_files); let match_percentage =
check_folder_completeness(parent, &osu_folder_files);
if match_percentage >= 70.0 { if match_percentage >= 70.0 {
return Some(parent.to_string_lossy().into()); return Some(parent.to_string_lossy().into());
@@ -150,39 +163,95 @@ pub fn find_osu_installation() -> Option<String> {
} }
} }
} }
}
None None
} }
#[tauri::command] #[tauri::command]
pub async 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_db_path = path.join("osu!.db");
let songs_path = osu_user_config if !osu_db_path.exists() {
.and_then(|config| config.get("Songs").cloned()) return Some(0);
.unwrap_or_else(|| path.join("Songs").to_string_lossy().into_owned()); }
let songs_folder = PathBuf::from(songs_path); let osu_db_bytes = fs::read(osu_db_path).await.ok()?;
let osu_db = parse_osudb(osu_db_bytes).ok()?;
let beatmap_sets = osu_db
.beatmaps
.iter()
.map(|b| b.beatmapset_id)
.collect::<HashSet<_>>()
.len();
Some(beatmap_sets as u64)
}
if !songs_folder.exists() { #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SkinInfo {
pub name: String,
pub author: Option<String>,
pub modified: u64,
}
#[tauri::command]
pub async fn get_skins(folder: String) -> Option<Vec<SkinInfo>> {
use std::path::PathBuf;
use std::time::UNIX_EPOCH;
use tokio::fs;
let skins_folder = PathBuf::from(folder).join("Skins");
if !skins_folder.exists() {
return None; return None;
} }
let mut count = 0; let mut entries = fs::read_dir(skins_folder).await.ok()?;
if let Ok(mut entries) = fs::read_dir(songs_folder).await {
while let Ok(Some(entry)) = entries.next_entry().await { let mut skins = Vec::new();
if entry.file_type().await.map_or(false, |ft| ft.is_dir()) {
while let Some(entry) = entries.next_entry().await.ok()? {
if !entry.file_type().await.ok()?.is_dir() {
continue;
}
let dir_path = entry.path(); let dir_path = entry.path();
if let Ok(mut files) = fs::read_dir(&dir_path).await { let skin_ini = dir_path.join("skin.ini");
while let Ok(Some(file)) = files.next_entry().await {
if file.path().extension().map_or(false, |ext| ext == "osu") { if !skin_ini.exists() {
count += 1; continue;
break;
} }
let name = entry.file_name().to_string_lossy().to_string();
let author = fs::read_to_string(&skin_ini)
.await
.ok()
.and_then(|content| {
content.lines().find_map(|line| {
let (key, value) = line.split_once(':')?;
key.trim()
.eq_ignore_ascii_case("Author")
.then(|| value.trim().to_string())
})
});
let modified = entry
.metadata()
.await
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
skins.push(SkinInfo {
name,
author,
modified,
});
} }
}
} Some(skins)
}
}
return Some(count);
} }
#[tauri::command] #[tauri::command]
@@ -191,7 +260,7 @@ pub async fn get_skins_count(folder: String) -> Option<u64> {
let skins_folder = path.join("Skins"); let skins_folder = path.join("Skins");
if !skins_folder.exists() { if !skins_folder.exists() {
return None; return Some(0);
} }
let mut count = 0; let mut count = 0;
@@ -381,14 +450,37 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
}; };
if patch { if patch {
sleep(Duration::from_secs(3)).await;
let patcher_exe_path = PathBuf::from(&folder) let patcher_exe_path = PathBuf::from(&folder)
.join("EZPPLauncher") .join("EZPPLauncher")
.join("patcher") .join("patcher")
.join("osu!.patcher.exe"); .join("osu!.patcher.exe");
if patcher_exe_path.exists() { if patcher_exe_path.exists() {
let mut sys = System::new_all();
loop {
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let mut found = false;
for (_pid, process) in sys.processes() {
if process.name() == "osu!.exe" {
let pid = process.pid();
let title = get_window_title_by_pid(pid);
if !title.is_empty() && !title.contains("updater") {
found = true;
break;
}
}
}
if found {
break;
}
sleep(Duration::from_millis(500)).await;
}
#[cfg(windows)] #[cfg(windows)]
{ {
let _ = Command::new(&patcher_exe_path) let _ = Command::new(&patcher_exe_path)
@@ -396,6 +488,14 @@ pub async fn run_osu(folder: String, patch: bool) -> Result<(), String> {
.spawn() .spawn()
.map_err(|e| format!("Failed to run patcher: {e}"))?; .map_err(|e| format!("Failed to run patcher: {e}"))?;
} }
#[cfg(not(windows))]
{
let _ = Command::new("osu-wine")
.arg("--wine")
.arg(&patcher_exe_path)
.spawn()
.map_err(|e| format!("Failed to run patcher: {}", e))?;
}
} }
} }
@@ -539,7 +639,6 @@ pub async fn download_ezpp_launcher_update_files(
#[serde(tag = "type", content = "details")] #[serde(tag = "type", content = "details")]
pub enum ReplaceUIError { pub enum ReplaceUIError {
FileNotFound(String), FileNotFound(String),
PermissionDenied(String),
IoError(String), IoError(String),
} }
@@ -557,44 +656,160 @@ pub fn replace_ui_files(folder: String, revert: bool) -> Result<(), ReplaceUIErr
let osu_seasonal_bak = osu_path.join("osu!seasonal.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 temp_file = osu_path.join(".ezpplauncher_files");
if !from.exists() {
return Err(ReplaceUIError::FileNotFound(from.display().to_string())); let copy_file = |source: &PathBuf, dest: &PathBuf, name: &str| -> Result<(), ReplaceUIError> {
if !source.exists() {
return Err(ReplaceUIError::FileNotFound(source.display().to_string()));
} }
std::fs::rename(from, to).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => { if dest.exists() {
ReplaceUIError::FileNotFound(from.display().to_string()) std::fs::remove_file(dest).map_err(|e| {
ReplaceUIError::IoError(format!("Failed to remove {}: {}", name, e))
})?;
} }
std::io::ErrorKind::PermissionDenied => {
ReplaceUIError::PermissionDenied(from.display().to_string()) std::fs::copy(source, dest)
.map_err(|e| ReplaceUIError::IoError(format!("Failed to copy {}: {}", name, e)))?;
Ok(())
};
let restore_backup =
|bak: &PathBuf, dest: &PathBuf, name: &str| -> Result<(), ReplaceUIError> {
if bak.exists() {
if dest.exists() {
std::fs::remove_file(dest).map_err(|e| {
ReplaceUIError::IoError(format!("Failed to remove {}: {}", name, e))
})?;
} }
_ => ReplaceUIError::IoError(e.to_string()), std::fs::rename(bak, dest).map_err(|e| {
}) ReplaceUIError::IoError(format!("Failed to rename {} from backup: {}", name, e))
})?;
}
Ok(())
}; };
if !revert { if !revert {
try_rename(&osu_ui, &osu_ui_bak)?; if osu_ui.exists() && !osu_ui_bak.exists() {
try_rename(&ezpp_ui, &osu_ui)?; std::fs::rename(&osu_ui, &osu_ui_bak).map_err(|e| {
ReplaceUIError::IoError(format!("Failed to backup osu!ui.dll: {}", e))
})?;
}
if osu_seasonal.exists() && !osu_seasonal_bak.exists() {
std::fs::rename(&osu_seasonal, &osu_seasonal_bak).map_err(|e| {
ReplaceUIError::IoError(format!("Failed to backup osu!seasonal.dll: {}", e))
})?;
}
if osu_gameplay.exists() && !osu_gameplay_bak.exists() {
std::fs::rename(&osu_gameplay, &osu_gameplay_bak).map_err(|e| {
ReplaceUIError::IoError(format!("Failed to backup osu!gameplay.dll: {}", e))
})?;
}
try_rename(&osu_seasonal, &osu_seasonal_bak)?; copy_file(&ezpp_ui, &osu_ui, "osu!ui.dll")?;
try_rename(&ezpp_seasonal, &osu_seasonal)?; copy_file(&ezpp_seasonal, &osu_seasonal, "osu!seasonal.dll")?;
copy_file(&ezpp_gameplay, &osu_gameplay, "osu!gameplay.dll")?;
try_rename(&osu_gameplay, &osu_gameplay_bak)?; std::fs::write(&temp_file, b"1")
try_rename(&ezpp_gameplay, &osu_gameplay)?; .map_err(|e| ReplaceUIError::IoError(format!("Failed to write marker file: {}", e)))?;
} else { } else {
try_rename(&osu_ui, &ezpp_ui)?; restore_backup(&osu_ui_bak, &osu_ui, "osu!ui.dll")?;
try_rename(&osu_ui_bak, &osu_ui)?; restore_backup(&osu_seasonal_bak, &osu_seasonal, "osu!seasonal.dll")?;
restore_backup(&osu_gameplay_bak, &osu_gameplay, "osu!gameplay.dll")?;
try_rename(&osu_seasonal, &ezpp_seasonal)?; if osu_ui.exists() && osu_seasonal.exists() && osu_gameplay.exists() {
try_rename(&osu_seasonal_bak, &osu_seasonal)?; std::fs::remove_file(&temp_file).map_err(|e| {
ReplaceUIError::IoError(format!("Failed to remove marker file: {}", e))
try_rename(&osu_gameplay, &ezpp_gameplay)?; })?;
try_rename(&osu_gameplay_bak, &osu_gameplay)?; }
} }
Ok(()) Ok(())
} }
#[derive(Serialize)]
#[serde(tag = "type", content = "details")]
pub enum CorruptionCheckError {
IoError(String),
FileNotFound(String),
}
#[tauri::command]
pub async fn check_for_corruption(folder: String) -> Result<bool, CorruptionCheckError> {
let osu_path = PathBuf::from(folder);
let osu_ui = osu_path.join("osu!ui.dll");
let osu_gameplay = osu_path.join("osu!gameplay.dll");
let osu_seasonal = osu_path.join("osu!seasonal.dll");
let osu_ui_bak = osu_path.join("osu!ui.dll.bak");
let osu_gameplay_bak = osu_path.join("osu!gameplay.dll.bak");
let osu_seasonal_bak = osu_path.join("osu!seasonal.dll.bak");
let temp_file = osu_path.join(".ezpplauncher_files");
if temp_file.exists() {
if osu_ui_bak.exists() {
if osu_ui.exists() {
fs::remove_file(&osu_ui).await.map_err(|e| {
CorruptionCheckError::IoError(format!("Failed to remove osu!ui.dll: {}", e))
})?;
}
fs::rename(&osu_ui_bak, &osu_ui).await.map_err(|e| {
CorruptionCheckError::IoError(format!(
"Failed to restore osu!ui.dll from backup: {}",
e
))
})?;
}
if osu_gameplay_bak.exists() {
if osu_gameplay.exists() {
fs::remove_file(&osu_gameplay).await.map_err(|e| {
CorruptionCheckError::IoError(format!(
"Failed to remove osu!gameplay.dll: {}",
e
))
})?;
}
fs::rename(&osu_gameplay_bak, &osu_gameplay)
.await
.map_err(|e| {
CorruptionCheckError::IoError(format!(
"Failed to restore osu!gameplay.dll from backup: {}",
e
))
})?;
}
if osu_seasonal_bak.exists() {
if osu_seasonal.exists() {
fs::remove_file(&osu_seasonal).await.map_err(|e| {
CorruptionCheckError::IoError(format!(
"Failed to remove osu!seasonal.dll: {}",
e
))
})?;
}
fs::rename(&osu_seasonal_bak, &osu_seasonal)
.await
.map_err(|e| {
CorruptionCheckError::IoError(format!(
"Failed to restore osu!seasonal.dll from backup: {}",
e
))
})?;
}
return Ok(false);
}
if !osu_ui.exists() || !osu_gameplay.exists() || !osu_seasonal.exists() {
return Ok(true);
}
Ok(false)
}
#[tauri::command] #[tauri::command]
pub fn is_osu_running() -> bool { pub fn is_osu_running() -> bool {
let mut sys = System::new_all(); let mut sys = System::new_all();
@@ -624,35 +839,6 @@ pub fn get_platform() -> String {
std::env::consts::OS.to_string() std::env::consts::OS.to_string()
} }
#[tauri::command]
pub async fn check_for_corruption(folder: String) -> Result<bool, String> {
let osu_path = PathBuf::from(folder);
let osu_ui = osu_path.join("osu!ui.dll");
let osu_gameplay = osu_path.join("osu!gameplay.dll");
let osu_ui_bak = osu_path.join("osu!ui.dll.bak");
let osu_gameplay_bak = osu_path.join("osu!gameplay.dll.bak");
let required_files = [&osu_ui, &osu_gameplay];
for file in &required_files {
if !file.exists() {
return Ok(true);
}
}
let bak_files = [&osu_ui_bak, &osu_gameplay_bak];
for bak in &bak_files {
if bak.exists() {
if let Err(e) = fs::remove_file(bak).await {
return Err(format!("Failed to delete {}: {}", bak.display(), e));
}
}
}
Ok(false)
}
#[tauri::command] #[tauri::command]
pub async fn presence_connect() -> bool { pub async fn presence_connect() -> bool {
presence::connect().await presence::connect().await
@@ -692,6 +878,21 @@ pub async fn presence_update_user(user: PresenceUser) {
presence::update_user(user.username.as_deref(), user.id.as_deref()); presence::update_user(user.username.as_deref(), user.id.as_deref());
} }
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PresenceButton {
text: Option<String>,
url: Option<String>,
}
#[tauri::command]
pub async fn presence_update_button(button: PresenceButton) {
match (button.text.as_deref(), button.url.as_deref()) {
(Some(text), Some(url)) => presence::set_button(text, url),
_ => presence::clear_button(),
}
}
#[tauri::command] #[tauri::command]
pub async fn presence_is_connected() -> bool { pub async fn presence_is_connected() -> bool {
presence::has_presence().await presence::has_presence().await
@@ -760,9 +961,10 @@ pub async fn download_ezpp_launcher_update(app: AppHandle, url: String) -> Resul
Ok(()) Ok(())
} }
#[cfg(windows)]
#[tauri::command] #[tauri::command]
pub async fn install_ezpp_launcher_update(app: AppHandle) -> Result<(), String> { pub async fn install_ezpp_launcher_update(app: AppHandle) -> Result<(), String> {
#[cfg(windows)]
{
let temp_dir = app.path().temp_dir().expect("Failed to get temp directory"); let temp_dir = app.path().temp_dir().expect("Failed to get temp directory");
let file_path = temp_dir.join("ezpplauncher_update.exe"); let file_path = temp_dir.join("ezpplauncher_update.exe");
if !file_path.exists() { if !file_path.exists() {
@@ -783,12 +985,373 @@ pub async fn install_ezpp_launcher_update(app: AppHandle) -> Result<(), String>
sleep(Duration::from_millis(250)).await; sleep(Duration::from_millis(250)).await;
app.exit(0x0100); app.exit(0x0100);
}
Ok(()) Ok(())
} }
#[cfg(not(windows))]
#[tauri::command] #[tauri::command]
pub async fn install_ezpp_launcher_update(_app: AppHandle) -> Result<(), String> { pub async fn run_open_tablet_driver(path: String) -> Result<(), String> {
let otd_path = PathBuf::from(&path);
if !otd_path.exists() {
return Err(format!("OpenTabletDriver not found at: {}", path));
}
#[cfg(windows)]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
Command::new(&otd_path)
.creation_flags(CREATE_NO_WINDOW)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start OpenTabletDriver: {}", e))?;
}
Ok(()) Ok(())
} }
#[tauri::command]
pub async fn stop_open_tablet_driver() -> Result<(), String> {
#[cfg(windows)]
{
let mut sys = System::new_all();
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let process = sys
.processes()
.values()
.find(|p| p.name().eq_ignore_ascii_case("OpenTabletDriver.Daemon.exe"))
.ok_or("OpenTabletDriver is not running")?;
process.kill();
}
Ok(())
}
#[tauri::command]
pub fn is_open_tablet_driver_running() -> bool {
#[cfg(windows)]
{
let mut sys = System::new_all();
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
sys.processes()
.values()
.any(|p| p.name().eq_ignore_ascii_case("OpenTabletDriver.Daemon.exe"))
}
#[cfg(not(windows))]
{
true
}
}
#[tauri::command]
pub async fn is_open_tablet_driver_path_valid(app: AppHandle, path: String) -> bool {
#[cfg(windows)]
{
app.fs_scope().allow_file(&path).ok();
let otd_path = PathBuf::from(&path);
otd_path.exists()
}
#[cfg(not(windows))]
{
true
}
}
#[derive(Serialize, Clone)]
pub struct ExtractProgress {
pub theme_name: String,
pub total: usize,
pub extracted: usize,
pub progress: f64,
pub current_file: String,
}
#[derive(Serialize, Clone)]
pub struct DownloadProgress {
pub theme_name: String,
pub received: u64,
pub total: u64,
pub progress: f64,
}
#[tauri::command]
pub async fn download_and_extract_theme(
app: AppHandle,
download_url: String,
expected_sha: String,
theme_folder: String,
theme_name: String,
) -> Result<(), String> {
let client = reqwest::Client::new();
let response = client
.get(&download_url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
)
.send()
.await
.map_err(|e| format!("Failed to download theme: {}", e))?;
let total = response.content_length().unwrap_or(0);
let mut received: u64 = 0;
let mut chunks: Vec<u8> = Vec::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = futures_util::StreamExt::next(&mut stream).await {
let chunk = chunk.map_err(|e| format!("Failed to read chunk: {}", e))?;
received += chunk.len() as u64;
chunks.extend_from_slice(&chunk);
let _ = app.emit(
"download_progress",
DownloadProgress {
theme_name: theme_name.clone(),
received,
total,
progress: if total > 0 {
received as f64 / total as f64
} else {
0.0
},
},
);
}
let sha = tokio::task::spawn_blocking({
let chunks = chunks.clone();
move || {
use sha1::{Digest, Sha1};
let header = format!("blob {}\0", chunks.len());
let mut hasher = Sha1::new();
hasher.update(header.as_bytes());
hasher.update(&chunks);
hex::encode(hasher.finalize())
}
})
.await
.map_err(|e| format!("Thread error: {}", e))?;
if sha != expected_sha {
return Err("Failed to verify downloaded theme file hash.".to_string());
}
// Parse zip on blocking thread
let entries: Vec<(String, Vec<u8>, bool)> = tokio::task::spawn_blocking(move || {
let cursor = std::io::Cursor::new(chunks);
let mut archive = zip::ZipArchive::new(cursor)
.map_err(|e| format!("Failed to read zip archive: {}", e))?;
let mut entries = Vec::new();
for i in 0..archive.len() {
let mut zip_entry = archive
.by_index(i)
.map_err(|e| format!("Failed to read zip entry: {}", e))?;
let name = match zip_entry.enclosed_name() {
Some(p) => p.to_string_lossy().to_string(),
None => continue,
};
let is_dir = zip_entry.is_dir();
let mut buf = Vec::new();
if !is_dir {
std::io::copy(&mut zip_entry, &mut buf)
.map_err(|e| format!("Failed to read zip entry data: {}", e))?;
}
entries.push((name, buf, is_dir));
}
Ok::<_, String>(entries)
})
.await
.map_err(|e| format!("Thread error: {}", e))??;
let total_files = entries.len();
let mut extracted = 0;
for (name, buf, is_dir) in entries {
let entry_path = std::path::Path::new(&theme_folder).join(&name);
if is_dir {
tokio::fs::create_dir_all(&entry_path)
.await
.map_err(|e| format!("Failed to create directory: {}", e))?;
} else {
if let Some(parent) = entry_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
}
tokio::fs::write(&entry_path, &buf)
.await
.map_err(|e| format!("Failed to write file: {}", e))?;
}
extracted += 1;
let _ = app.emit(
"extract_progress",
ExtractProgress {
theme_name: theme_name.clone(),
total: total_files,
extracted,
progress: extracted as f64 / total_files as f64,
current_file: name,
},
);
}
Ok(())
}
#[tauri::command]
pub async fn extract_theme(
app: AppHandle,
file_path: String,
theme_folder: String,
theme_name: String,
) -> Result<(), String> {
app.fs_scope().allow_file(&file_path).ok();
let entries: Vec<(String, Vec<u8>, bool)> = tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&file_path)
.map_err(|e| format!("Failed to open theme file: {}", e))?;
let mut archive =
zip::ZipArchive::new(file).map_err(|e| format!("Failed to read zip archive: {}", e))?;
let mut entries = Vec::new();
for i in 0..archive.len() {
let mut zip_entry = archive
.by_index(i)
.map_err(|e| format!("Failed to read zip entry: {}", e))?;
let name = match zip_entry.enclosed_name() {
Some(p) => p.to_string_lossy().to_string(),
None => continue,
};
let is_dir = zip_entry.is_dir();
let mut buf = Vec::new();
if !is_dir {
zip_entry
.read_to_end(&mut buf)
.map_err(|e| format!("Failed to read zip entry data: {}", e))?;
}
entries.push((name, buf, is_dir));
}
Ok::<_, String>(entries)
})
.await
.map_err(|e| format!("Thread error: {}", e))??;
let total = entries.len();
let mut extracted = 0;
for (name, buf, is_dir) in entries {
let entry_path = std::path::Path::new(&theme_folder).join(&name);
if is_dir {
tokio::fs::create_dir_all(&entry_path)
.await
.map_err(|e| format!("Failed to create directory: {}", e))?;
} else {
if let Some(parent) = entry_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("Failed to create parent directory: {}", e))?;
}
tokio::fs::write(&entry_path, &buf)
.await
.map_err(|e| format!("Failed to write file: {}", e))?;
}
extracted += 1;
let _ = app.emit(
"extract_progress",
ExtractProgress {
theme_name: theme_name.clone(),
total,
extracted,
progress: extracted as f64 / total as f64,
current_file: name,
},
);
}
Ok(())
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ThemeInfo {
pub name: String,
pub entry: String,
pub preview: String,
}
#[tauri::command]
pub async fn read_theme_info(app: AppHandle, file_path: String) -> Result<ThemeInfo, String> {
app.fs_scope().allow_file(&file_path).ok();
tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&file_path)
.map_err(|e| format!("Failed to open theme file: {}", e))?;
let mut archive =
zip::ZipArchive::new(file).map_err(|e| format!("Failed to read zip archive: {}", e))?;
let mut theme_config = archive
.by_name("theme.json")
.map_err(|_| "Theme config file not found".to_string())?;
let mut contents = String::new();
std::io::Read::read_to_string(&mut theme_config, &mut contents)
.map_err(|e| format!("Failed to read theme config: {}", e))?;
let theme_info: ThemeInfo = serde_json::from_str(&contents)
.map_err(|e| format!("Failed to parse theme config: {}", e))?;
Ok(theme_info)
})
.await
.map_err(|e| format!("Thread error: {}", e))?
}
#[tauri::command]
pub fn config_get(key: String, state: State<AppState>) -> Result<Option<Value>, ConfigError> {
state.config.lock().unwrap().get(&key)
}
#[tauri::command]
pub fn config_set(
key: String,
value: Value,
encrypt: bool,
state: State<AppState>,
) -> Result<(), ConfigError> {
state.config.lock().unwrap().set(&key, value, encrypt)
}
#[tauri::command]
pub fn config_exists(key: String, state: State<AppState>) -> Result<bool, ConfigError> {
state.config.lock().unwrap().exists(&key)
}
#[tauri::command]
pub fn config_delete(key: String, state: State<AppState>) -> Result<(), ConfigError> {
state.config.lock().unwrap().delete(&key)
}
#[tauri::command]
pub fn config_clear(state: State<AppState>) -> Result<(), ConfigError> {
state.config.lock().unwrap().clear()
}
#[tauri::command]
pub fn config_all(state: State<AppState>) -> Result<Vec<(String, Value)>, ConfigError> {
state.config.lock().unwrap().all()
}
+219
View File
@@ -0,0 +1,219 @@
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rusqlite::{Connection, OptionalExtension, params};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("SQLite error: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("Encryption error: {0}")]
Encryption(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Base64 error: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Config not initialized")]
NotInitialized,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl serde::Serialize for ConfigError {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
pub struct Config {
db_path: PathBuf,
conn: Option<Connection>,
hwid_key: Option<[u8; 32]>,
}
impl Config {
pub fn new(db_path: impl Into<PathBuf>) -> Self {
Self {
db_path: db_path.into(),
conn: None,
hwid_key: None,
}
}
pub fn init(&mut self, hwid: &str) -> Result<bool, ConfigError> {
let mut hasher = Sha256::new();
hasher.update(hwid.as_bytes());
let key_bytes: [u8; 32] = hasher.finalize().into();
self.hwid_key = Some(key_bytes);
let is_new = !self.db_path.exists();
if let Some(parent) = self.db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(&self.db_path)?;
conn.execute_batch(
"
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
encrypted INTEGER NOT NULL DEFAULT 0
);
",
)?;
self.conn = Some(conn);
Ok(is_new)
}
pub fn get(&self, key: &str) -> Result<Option<Value>, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let mut stmt = conn.prepare("SELECT value, encrypted FROM config WHERE key = ?1")?;
let row: Option<(String, bool)> = stmt
.query_row(params![key], |r| Ok((r.get(0)?, r.get(1)?)))
.optional()?;
match row {
None => Ok(None),
Some((raw, false)) => Ok(Some(serde_json::from_str::<Value>(&raw)?)),
Some((raw, true)) => {
let plaintext = self
.decrypt_value(&raw)
.map_err(|_| ConfigError::Encryption(format!("failed to decrypt '{}'", key)))?;
serde_json::from_str::<Value>(&plaintext)
.map(Some)
.map_err(|_| {
ConfigError::Encryption(format!("decrypted '{}' is not valid JSON", key))
})
}
}
}
pub fn exists(&self, key: &str) -> Result<bool, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM config WHERE key = ?1",
params![key],
|r| r.get(0),
)?;
Ok(count > 0)
}
/// Return all key/value pairs (decrypting encrypted ones).
pub fn all(&self) -> Result<Vec<(String, Value)>, ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
let mut stmt = conn.prepare("SELECT key, value, encrypted FROM config")?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, bool>(2)?,
))
})?;
let mut out = Vec::new();
for row in rows {
let (k, raw, enc) = row?;
let v: Value = if enc {
let pt = self.decrypt_value(&raw)?;
serde_json::from_str(&pt)?
} else {
serde_json::from_str(&raw)?
};
out.push((k, v));
}
Ok(out)
}
pub fn set<T: serde::Serialize>(
&self,
key: &str,
value: T,
encrypt: bool,
) -> Result<(), ConfigError> {
let json = serde_json::to_string(&value)?;
if encrypt {
let blob = self.encrypt_value(&json)?;
self.upsert(key, &blob, true)
} else {
self.upsert(key, &json, false)
}
}
pub fn delete(&self, key: &str) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute("DELETE FROM config WHERE key = ?1", params![key])?;
Ok(())
}
pub fn clear(&self) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute("DELETE FROM config", [])?;
Ok(())
}
fn upsert(&self, key: &str, raw: &str, encrypted: bool) -> Result<(), ConfigError> {
let conn = self.conn.as_ref().ok_or(ConfigError::NotInitialized)?;
conn.execute(
"INSERT INTO config (key, value, encrypted)
VALUES (?1, ?2, ?3)
ON CONFLICT(key) DO UPDATE SET value = excluded.value,
encrypted = excluded.encrypted",
params![key, raw, encrypted as i32],
)?;
Ok(())
}
fn cipher(&self) -> Result<Aes256Gcm, ConfigError> {
let key_bytes = self.hwid_key.ok_or(ConfigError::NotInitialized)?;
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
Ok(Aes256Gcm::new(key))
}
fn encrypt_value(&self, plaintext: &str) -> Result<String, ConfigError> {
let cipher = self.cipher()?;
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| ConfigError::Encryption(e.to_string()))?;
let mut combined = nonce_bytes.to_vec();
combined.extend_from_slice(&ciphertext);
Ok(BASE64.encode(&combined))
}
fn decrypt_value(&self, blob: &str) -> Result<String, ConfigError> {
let cipher = self.cipher()?;
let combined = BASE64.decode(blob)?;
if combined.len() < 12 {
return Err(ConfigError::Encryption("blob too short".into()));
}
let (nonce_bytes, ciphertext) = combined.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| ConfigError::Encryption(e.to_string()))?;
String::from_utf8(plaintext).map_err(|e| ConfigError::Encryption(e.to_string()))
}
}
+99 -19
View File
@@ -1,42 +1,113 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use hardware_id::get_id;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::Manager; use tauri::Manager;
use tauri_plugin_fs::FsExt;
pub mod commands; pub mod commands;
pub mod config;
pub mod osudb;
pub mod presence; pub mod presence;
pub mod state;
pub mod utils; pub mod utils;
use crate::commands::{ use crate::{
check_for_corruption, download_ezpp_launcher_update, download_ezpp_launcher_update_files, commands::{
encrypt_string, exit, find_osu_installation, get_beatmapsets_count, check_for_corruption, config_all, config_clear, config_delete, config_exists, config_get,
get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream, config_set, download_and_extract_theme, download_ezpp_launcher_update,
get_osu_skin, get_osu_version, get_platform, get_skins_count, has_net8, has_osuwinello, download_ezpp_launcher_update_files, encrypt_string, exit, extract_theme,
has_wmctrl, install_ezpp_launcher_update, is_osu_running, open_url_in_browser, find_osu_installation, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid,
presence_connect, presence_disconnect, presence_is_connected, presence_update_status, get_launcher_version, get_osu_release_stream, get_osu_skin, get_osu_version, get_platform,
presence_update_user, replace_ui_files, run_osu, run_osu_updater, set_osu_config_values, get_skins, get_skins_count, has_net8, has_osuwinello, has_wmctrl,
set_osu_user_config_values, valid_osu_folder, install_ezpp_launcher_update, is_open_tablet_driver_path_valid,
is_open_tablet_driver_running, is_osu_running, open_url_in_browser, opened_urls,
presence_connect, presence_disconnect, presence_is_connected, presence_update_button,
presence_update_status, presence_update_user, read_theme_info, replace_ui_files,
run_open_tablet_driver, run_osu, run_osu_updater, set_osu_config_values,
set_osu_user_config_values, stop_open_tablet_driver, valid_osu_folder,
},
state::AppState,
}; };
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
unsafe { unsafe {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
} }
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
#[cfg(desktop)] #[cfg(desktop)]
{ {
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
let app_window = app.get_webview_window("main").expect("no main window"); let app_window = app.get_webview_window("main").expect("no main window");
app_window.set_focus().expect("failed to focus"); app_window.set_focus().expect("failed to focus");
if args.len() > 1 {
use tauri::Emitter;
let mut files = Vec::new();
for maybe_file in args.iter().skip(1) {
if maybe_file.starts_with("-") {
continue;
}
let path = PathBuf::from(maybe_file);
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
app.fs_scope().allow_file(&path).ok();
files.push(path);
}
}
app.emit("opened", files.clone()).ok();
}
})); }));
} }
let app = builder let app = builder
.setup(|app| {
// ── Config ────────────────────────────────────────────────────────
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".into());
let db_path = PathBuf::from(home).join(".ezpplauncher").join("config.db");
let hwid = get_id().unwrap_or_else(|_| "recorderinsandybridge".into());
let mut cfg = config::Config::new(db_path);
cfg.init(&hwid)?;
// ── Opened files ──────────────────────────────────────────────────
let mut opened_files = Vec::new();
let args = std::env::args().collect::<Vec<_>>();
if args.len() > 1 {
for maybe_file in args.iter().skip(1) {
if maybe_file.starts_with("-") {
continue;
}
let path = PathBuf::from(maybe_file);
if path.extension().and_then(|e| e.to_str()) == Some("ezpplauncher-theme") {
app.fs_scope().allow_file(&path).ok();
opened_files.push(path);
}
}
}
app.manage(AppState {
config: Mutex::new(cfg),
opened_files: Mutex::new(opened_files),
});
Ok(())
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
opened_urls,
get_hwid, get_hwid,
find_osu_installation, find_osu_installation,
valid_osu_folder, valid_osu_folder,
get_beatmapsets_count, get_beatmapsets_count,
get_skins,
get_skins_count, get_skins_count,
get_osu_version, get_osu_version,
get_osu_release_stream, get_osu_release_stream,
@@ -58,13 +129,27 @@ pub fn run() {
presence_disconnect, presence_disconnect,
presence_update_status, presence_update_status,
presence_update_user, presence_update_user,
presence_update_button,
presence_is_connected, presence_is_connected,
has_osuwinello, has_osuwinello,
has_wmctrl, has_wmctrl,
has_net8, has_net8,
encrypt_string, encrypt_string,
download_ezpp_launcher_update, download_ezpp_launcher_update,
install_ezpp_launcher_update install_ezpp_launcher_update,
read_theme_info,
extract_theme,
download_and_extract_theme,
config_get,
config_set,
config_exists,
config_delete,
config_clear,
config_all,
run_open_tablet_driver,
stop_open_tablet_driver,
is_open_tablet_driver_running,
is_open_tablet_driver_path_valid
]) ])
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
@@ -73,12 +158,7 @@ pub fn run() {
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while building tauri application"); .expect("error while building tauri application");
app.run(|_app_handle, event| { app.run(|app, event| {
if let tauri::RunEvent::ExitRequested { api, .. } = event { presence::handle_run_event(app, &event);
api.prevent_exit();
tauri::async_runtime::block_on(presence::disconnect());
std::process::exit(0);
}
}); });
} }
+370
View File
@@ -0,0 +1,370 @@
use std::io::{Cursor, Read};
use anyhow::{Result, anyhow};
use byteorder::{LittleEndian, ReadBytesExt};
use std::collections::HashMap;
pub struct OsuReader {
cursor: Cursor<Vec<u8>>,
}
impl OsuReader {
pub fn new(data: Vec<u8>) -> Self {
Self {
cursor: Cursor::new(data),
}
}
fn read_i16(&mut self) -> Result<i16> {
Ok(self.cursor.read_i16::<LittleEndian>()?)
}
fn read_i32(&mut self) -> Result<i32> {
Ok(self.cursor.read_i32::<LittleEndian>()?)
}
fn read_i64(&mut self) -> Result<i64> {
Ok(self.cursor.read_i64::<LittleEndian>()?)
}
fn read_u8(&mut self) -> Result<u8> {
Ok(self.cursor.read_u8()?)
}
fn read_f32(&mut self) -> Result<f32> {
Ok(self.cursor.read_f32::<LittleEndian>()?)
}
fn read_f64(&mut self) -> Result<f64> {
Ok(self.cursor.read_f64::<LittleEndian>()?)
}
fn read_bool(&mut self) -> Result<bool> {
Ok(self.read_u8()? != 0)
}
fn read_string(&mut self) -> Result<Option<String>> {
let indicator = self.read_u8()?;
if indicator == 0 {
return Ok(None);
}
if indicator != 0x0B {
return Err(anyhow!("Invalid osu string indicator"));
}
let len = self.read_uleb128()? as usize;
let mut buf = vec![0; len];
self.cursor.read_exact(&mut buf)?;
Ok(Some(String::from_utf8(buf)?))
}
fn read_uleb128(&mut self) -> Result<u64> {
let mut result = 0;
let mut shift = 0;
loop {
let byte = self.read_u8()?;
result |= ((byte & 0x7F) as u64) << shift;
if (byte & 0x80) == 0 {
break;
}
shift += 7;
}
Ok(result)
}
}
#[derive(Debug)]
pub struct TimingPoint {
pub bpm: f64,
pub offset: f64,
pub inherited: bool,
}
#[derive(Debug)]
pub struct Beatmap {
pub artist_name: Option<String>,
pub artist_unicode: Option<String>,
pub song_title: Option<String>,
pub song_title_unicode: Option<String>,
pub creator: Option<String>,
pub difficulty: Option<String>,
pub audio: Option<String>,
pub md5: Option<String>,
pub osu_file_name: Option<String>,
pub ranked_status: u8,
pub hitcircles: i16,
pub slider_count: i16,
pub spinner_count: i16,
pub last_modification: i64,
pub approach_rate: f32,
pub circle_size: f32,
pub hp_drain: f32,
pub overall_difficulty: f32,
pub slider_velocity: f64,
pub difficulties: HashMap<i32, f64>,
pub drain_time: i32,
pub total_time: i32,
pub preview_offset: i32,
pub beatmap_id: i32,
pub beatmapset_id: i32,
pub thread_id: i32,
pub grade_standard: u8,
pub grade_taiko: u8,
pub grade_catch: u8,
pub grade_mania: u8,
pub local_beatmap_offset: i16,
pub stack_leniency: f32,
pub mode: u8,
pub song_source: Option<String>,
pub song_tags: Option<String>,
pub online_beatmap_offset: i16,
pub title_font: Option<String>,
pub unplayed: bool,
pub last_played: i64,
pub osz2: bool,
pub folder_name: Option<String>,
pub last_checked_against_repository: i64,
pub ignore_sound: bool,
pub ignore_skin: bool,
pub disable_storyboard: bool,
pub disable_video: bool,
pub visual_override: bool,
pub last_modification_time: i32,
pub mania_scroll_speed: u8,
pub timing_points: Vec<TimingPoint>,
}
#[derive(Debug)]
pub struct OsuDbData {
pub osuver: i32,
pub folder_count: i32,
pub is_unlocked: bool,
pub date_unlock_ticks: i64,
pub username: Option<String>,
pub beatmaps: Vec<Beatmap>,
pub userperms: i32,
}
//
// =========================
// PARSER
// =========================
//
pub fn parse_osudb(bytes: Vec<u8>) -> Result<OsuDbData> {
let mut reader = OsuReader::new(bytes);
let osuver = reader.read_i32()?;
let folder_count = reader.read_i32()?;
let is_unlocked = reader.read_bool()?;
let date_unlock_ticks = reader.read_i64()?;
let username = reader.read_string()?;
let beatmaps_count = reader.read_i32()?;
let mut beatmaps = Vec::new();
for _ in 0..beatmaps_count {
if osuver < 20191107 {
reader.read_i32()?; // entry size
}
let artist_name = reader.read_string()?;
let artist_unicode = reader.read_string()?;
let song_title = reader.read_string()?;
let song_title_unicode = reader.read_string()?;
let creator = reader.read_string()?;
let difficulty = reader.read_string()?;
let audio = reader.read_string()?;
let md5 = reader.read_string()?;
let osu_file_name = reader.read_string()?;
let ranked_status = reader.read_u8()?;
let hitcircles = reader.read_i16()?;
let slider_count = reader.read_i16()?;
let spinner_count = reader.read_i16()?;
let last_modification = reader.read_i64()?;
let approach_rate = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let circle_size = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let hp_drain = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let overall_difficulty = if osuver < 20140609 {
reader.read_u8()? as f32
} else {
reader.read_f32()?
};
let slider_velocity = reader.read_f64()?;
let mut difficulties: HashMap<i32, f64> = HashMap::new();
if osuver >= 20140609 {
for _ in 0..4 {
let len = reader.read_i32()?;
for _ in 0..len {
reader.read_u8()?;
let mode = reader.read_i32()?;
reader.read_u8()?;
let diff = if osuver > 20250107 {
reader.read_f32()? as f64
} else {
reader.read_f64()?
};
difficulties.insert(mode, diff);
}
}
}
let drain_time = reader.read_i32()?;
let total_time = reader.read_i32()?;
let preview_offset = reader.read_i32()?;
let timing_len = reader.read_i32()?;
let mut timing_points = Vec::new();
for _ in 0..timing_len {
timing_points.push(TimingPoint {
bpm: reader.read_f64()?,
offset: reader.read_f64()?,
inherited: reader.read_bool()?,
});
}
let beatmap_id = reader.read_i32()?;
let beatmapset_id = reader.read_i32()?;
let thread_id = reader.read_i32()?;
let grade_standard = reader.read_u8()?;
let grade_taiko = reader.read_u8()?;
let grade_catch = reader.read_u8()?;
let grade_mania = reader.read_u8()?;
let local_beatmap_offset = reader.read_i16()?;
let stack_leniency = reader.read_f32()?;
let mode = reader.read_u8()?;
let song_source = reader.read_string()?;
let song_tags = reader.read_string()?;
let online_beatmap_offset = reader.read_i16()?;
let title_font = reader.read_string()?;
let unplayed = reader.read_bool()?;
let last_played = reader.read_i64()?;
let osz2 = reader.read_bool()?;
let folder_name = reader.read_string()?;
let last_checked_against_repository = reader.read_i64()?;
let ignore_sound = reader.read_bool()?;
let ignore_skin = reader.read_bool()?;
let disable_storyboard = reader.read_bool()?;
let disable_video = reader.read_bool()?;
let visual_override = reader.read_bool()?;
if osuver < 20140609 {
reader.read_i16()?;
}
let last_modification_time = reader.read_i32()?;
let mania_scroll_speed = reader.read_u8()?;
beatmaps.push(Beatmap {
artist_name,
artist_unicode,
song_title,
song_title_unicode,
creator,
difficulty,
audio,
md5,
osu_file_name,
ranked_status,
hitcircles,
slider_count,
spinner_count,
last_modification,
approach_rate,
circle_size,
hp_drain,
overall_difficulty,
slider_velocity,
difficulties,
drain_time,
total_time,
preview_offset,
beatmap_id,
beatmapset_id,
thread_id,
grade_standard,
grade_taiko,
grade_catch,
grade_mania,
local_beatmap_offset,
stack_leniency,
mode,
song_source,
song_tags,
online_beatmap_offset,
title_font,
unplayed,
last_played,
osz2,
folder_name,
last_checked_against_repository,
ignore_sound,
ignore_skin,
disable_storyboard,
disable_video,
visual_override,
last_modification_time,
mania_scroll_speed,
timing_points,
});
}
let userperms = reader.read_i32()?;
Ok(OsuDbData {
osuver,
folder_count,
is_unlocked,
date_unlock_ticks,
username,
beatmaps,
userperms,
})
}
+208 -97
View File
@@ -1,13 +1,17 @@
use discord_rich_presence::{
DiscordIpc, DiscordIpcClient,
activity::{Activity, Assets, Button, Timestamps},
};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use presenceforge::{ActivityBuilder, AsyncDiscordIpcClient};
use std::sync::Mutex as StdMutex; use std::sync::Mutex as StdMutex;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, RunEvent};
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tokio::time::{Duration, interval}; use tokio::time::{Duration, interval};
#[derive(Clone, Debug)]
pub struct PresenceButton {
pub label: String,
pub url: String,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PresenceData { pub struct PresenceData {
pub state: String, pub state: String,
@@ -16,6 +20,21 @@ pub struct PresenceData {
pub large_image_text: String, pub large_image_text: String,
pub small_image_key: Option<String>, pub small_image_key: Option<String>,
pub small_image_text: Option<String>, pub small_image_text: Option<String>,
pub dynamic_button: Option<PresenceButton>,
}
impl Default for PresenceData {
fn default() -> Self {
Self {
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
dynamic_button: None,
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -26,11 +45,19 @@ enum PresenceCommand {
IsConnected(oneshot::Sender<bool>), IsConnected(oneshot::Sender<bool>),
} }
enum ReconnectResult {
Connected(AsyncDiscordIpcClient),
Failed,
}
struct PresenceActor { struct PresenceActor {
receiver: mpsc::Receiver<PresenceCommand>, receiver: mpsc::Receiver<PresenceCommand>,
client: Option<DiscordIpcClient>, reconnect_rx: mpsc::Receiver<ReconnectResult>,
reconnect_tx: mpsc::Sender<ReconnectResult>,
client: Option<AsyncDiscordIpcClient>,
data: PresenceData, data: PresenceData,
start_timestamp: i64, start_timestamp: i64,
reconnecting: bool,
} }
impl PresenceActor { impl PresenceActor {
@@ -40,137 +67,184 @@ impl PresenceActor {
.unwrap() .unwrap()
.as_secs() as i64; .as_secs() as i64;
let data = PresenceData { let (reconnect_tx, reconnect_rx) = mpsc::channel(1);
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
};
PresenceActor { Self {
receiver, receiver,
reconnect_rx,
reconnect_tx,
client: None, client: None,
data, data: PresenceData::default(),
start_timestamp: start, start_timestamp: start,
reconnecting: false,
} }
} }
async fn run(&mut self) { async fn run(&mut self) {
let mut update_interval = interval(Duration::from_millis(2500)); let mut heartbeat = interval(Duration::from_secs(5));
heartbeat.tick().await;
loop { loop {
tokio::select! { tokio::select! {
Some(cmd) = self.receiver.recv() => { Some(cmd) = self.receiver.recv() => {
match cmd { match cmd {
PresenceCommand::Connect(responder) => self.handle_connect(responder).await, PresenceCommand::Connect(resp) => self.cmd_connect(resp).await,
PresenceCommand::Disconnect(responder) => { PresenceCommand::Disconnect(resp) => self.cmd_disconnect(resp).await,
self.handle_disconnect(responder).await; PresenceCommand::UpdateData(data) => {
}, self.data = data;
PresenceCommand::UpdateData(new_data) => { self.push_presence().await;
self.data = new_data; }
}, PresenceCommand::IsConnected(resp) => {
PresenceCommand::IsConnected(responder) => { let _ = resp.send(self.client.is_some());
let _ = responder.send(self.client.is_some());
} }
} }
} }
_ = update_interval.tick() => {
if self.client.is_some() { Some(result) = self.reconnect_rx.recv() => {
self.handle_update().await; self.reconnecting = false;
match result {
ReconnectResult::Connected(client) => {
println!("Presence: reconnect succeeded.");
self.client = Some(client);
self.push_presence().await;
} }
ReconnectResult::Failed => {
eprintln!("Presence: all reconnect attempts exhausted.");
}
}
}
_ = heartbeat.tick() => {
self.push_presence().await;
} }
} }
} }
} }
async fn handle_connect(&mut self, responder: oneshot::Sender<bool>) { async fn cmd_connect(&mut self, resp: oneshot::Sender<bool>) {
if self.client.is_some() { if self.client.is_some() {
let _ = responder.send(true); let _ = resp.send(true);
return; return;
} }
println!("Actor: Connecting to Discord..."); match try_connect_once().await {
let mut dc_client = DiscordIpcClient::new("1032772293220384808"); Some(client) => {
match dc_client.connect() { self.client = Some(client);
Ok(()) => { self.reconnecting = false;
self.client = Some(dc_client); self.push_presence().await;
println!("Actor: Connected successfully."); let _ = resp.send(true);
self.handle_update().await;
let _ = responder.send(true);
} }
Err(e) => { None => {
eprintln!("Failed to create Discord client: {:?}", e); eprintln!("Presence: initial connect failed; spawning reconnect task.");
let _ = responder.send(false); let _ = resp.send(false);
self.spawn_reconnect_task();
} }
} }
} }
async fn handle_disconnect(&mut self, responder: oneshot::Sender<()>) { async fn cmd_disconnect(&mut self, resp: oneshot::Sender<()>) {
if let Some(mut client) = self.client.take() { if let Some(mut client) = self.client.take() {
println!("Actor: Disconnecting..."); let _ = client.clear_activity().await;
let _ = client.clear_activity().map_err(|e| e.to_string()); println!("Presence: disconnected.");
let _ = client.close().map_err(|e| e.to_string());
println!("Actor: Disconnected successfully.");
} }
let _ = responder.send(()); let _ = resp.send(());
} }
async fn handle_update(&mut self) { async fn push_presence(&mut self) {
if let Some(client) = self.client.as_mut() { let client = match self.client.as_mut() {
let mut assets = Assets::new() Some(c) => c,
None => return,
};
let mut builder = ActivityBuilder::new()
.state(&self.data.state)
.details(&self.data.details)
.start_timestamp(self.start_timestamp as u64)
.large_image(&self.data.large_image_key) .large_image(&self.data.large_image_key)
.large_text(&self.data.large_image_text); .large_text(&self.data.large_image_text);
if let Some(key) = &self.data.small_image_key { if let Some(key) = &self.data.small_image_key {
assets = assets.small_image(key); builder = builder.small_image(key);
} }
if let Some(text) = &self.data.small_image_text { if let Some(text) = &self.data.small_image_text {
assets = assets.small_text(text); builder = builder.small_text(text);
} }
let activity = Activity::new() if let Some(btn) = &self.data.dynamic_button {
.state(&self.data.state) builder = builder.button(&btn.label, &btn.url);
.details(&self.data.details) } else {
.timestamps(Timestamps::new().start(self.start_timestamp)) builder = builder.button(
.assets(assets)
.buttons(vec![
Button::new(
"Download the Launcher", "Download the Launcher",
"https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest", "https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest",
), );
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"), }
]); builder = builder.button("Join EZPPFarm", "https://ez-pp.farm/discord");
if let Err(e) = client.set_activity(activity).map_err(|e| e.to_string()) { let activity = builder.build();
eprintln!("Failed to set activity, disconnecting: {:?}", e);
if let Some(mut client) = self.client.take() { /* println!("Presence: updating activity: {:?}", activity); */
let _ = client.clear_activity(); if let Err(e) = client.set_activity(&activity).await {
let _ = client.close(); eprintln!("Presence: set_activity failed ({:?}); will reconnect.", e);
self.client = None;
self.spawn_reconnect_task();
} }
} }
fn spawn_reconnect_task(&mut self) {
if self.reconnecting {
return;
}
self.reconnecting = true;
let tx = self.reconnect_tx.clone();
tokio::spawn(async move {
const MAX_ATTEMPTS: u32 = 5;
for attempt in 1..=MAX_ATTEMPTS {
let delay = Duration::from_secs(5 * attempt as u64);
println!(
"Presence: reconnect attempt {}/{} in {:?}…",
attempt, MAX_ATTEMPTS, delay
);
tokio::time::sleep(delay).await;
if let Some(client) = try_connect_once().await {
let _ = tx.send(ReconnectResult::Connected(client)).await;
return;
}
}
let _ = tx.send(ReconnectResult::Failed).await;
});
}
}
async fn try_connect_once() -> Option<AsyncDiscordIpcClient> {
match AsyncDiscordIpcClient::new("1032772293220384808").await {
Ok(mut client) => match client.connect().await {
Ok(_) => {
println!("Presence: IPC connected.");
Some(client)
}
Err(e) => {
eprintln!("Presence: IPC connect error: {:?}", e);
None
}
},
Err(e) => {
eprintln!("Presence: failed to create client: {:?}", e);
None
} }
} }
} }
static PRESENCE_TX: Lazy<mpsc::Sender<PresenceCommand>> = Lazy::new(|| { static PRESENCE_TX: Lazy<mpsc::Sender<PresenceCommand>> = Lazy::new(|| {
let (tx, rx) = mpsc::channel(10); let (tx, rx) = mpsc::channel(32);
let mut actor = PresenceActor::new(rx); let mut actor = PresenceActor::new(rx);
tokio::spawn(async move { actor.run().await }); tokio::spawn(async move { actor.run().await });
tx tx
}); });
pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> = Lazy::new(|| { pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> =
StdMutex::new(PresenceData { Lazy::new(|| StdMutex::new(PresenceData::default()));
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
})
});
pub async fn connect() -> bool { pub async fn connect() -> bool {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@@ -182,14 +256,8 @@ pub async fn connect() -> bool {
pub async fn disconnect() { pub async fn disconnect() {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
if PRESENCE_TX if PRESENCE_TX.send(PresenceCommand::Disconnect(tx)).await.is_ok() {
.send(PresenceCommand::Disconnect(tx)) let _ = tokio::time::timeout(Duration::from_secs(2), rx).await;
.await
.is_ok()
{
let _ = rx.await;
} else {
println!("Could not send disconnect command; actor may not be running.");
} }
} }
@@ -206,30 +274,73 @@ pub async fn has_presence() -> bool {
} }
pub fn update_status(state: Option<&str>, details: Option<&str>, large_image_key: Option<&str>) { pub fn update_status(state: Option<&str>, details: Option<&str>, large_image_key: Option<&str>) {
let mut data = PRESENCE_DATA.lock().unwrap(); let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
if let Some(s) = state { if let Some(s) = state {
data.state = s.to_string(); guard.state = s.to_string();
} }
if let Some(d) = details { if let Some(d) = details {
data.details = d.to_string(); guard.details = d.to_string();
} }
if let Some(img) = large_image_key { if let Some(img) = large_image_key {
data.large_image_key = img.to_string(); guard.large_image_key = img.to_string();
} }
let data_clone = data.clone(); guard.clone()
};
let tx = PRESENCE_TX.clone(); let tx = PRESENCE_TX.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data_clone)).await; let _ = tx.send(PresenceCommand::UpdateData(data)).await;
}); });
} }
pub fn update_user(username: Option<&str>, id: Option<&str>) { pub fn update_user(username: Option<&str>, id: Option<&str>) {
let mut data = PRESENCE_DATA.lock().unwrap(); let data = {
data.small_image_key = id.map(|id_str| format!("https://a.ez-pp.farm/{}", id_str)); let mut guard = PRESENCE_DATA.lock().unwrap();
data.small_image_text = username.map(|s| s.to_string()); guard.small_image_key = id.map(|s| format!("https://a.ez-pp.farm/{}", s));
let data_clone = data.clone(); guard.small_image_text = username.map(str::to_string);
guard.clone()
};
let tx = PRESENCE_TX.clone(); let tx = PRESENCE_TX.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data_clone)).await; let _ = tx.send(PresenceCommand::UpdateData(data)).await;
}); });
} }
pub fn set_button(label: &str, url: &str) {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.dynamic_button = Some(PresenceButton {
label: label.to_string(),
url: url.to_string(),
});
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn clear_button() {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.dynamic_button = None;
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn handle_run_event(_app: &AppHandle, event: &RunEvent) {
if let RunEvent::ExitRequested { .. } | RunEvent::Exit = event {
tauri::async_runtime::block_on(async {
disconnect().await;
});
}
}
+9
View File
@@ -0,0 +1,9 @@
// state.rs
use crate::config::Config;
use std::path::PathBuf;
use std::sync::Mutex;
pub struct AppState {
pub config: Mutex<Config>,
pub opened_files: Mutex<Vec<PathBuf>>,
}
+41 -3
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "EZPPLauncher", "productName": "EZPPLauncher",
"version": "3.1.1", "version": "3.2.3",
"identifier": "farm.EZPPFarm.Launcher", "identifier": "farm.EZPPFarm.Launcher",
"mainBinaryName": "ezpplauncher", "mainBinaryName": "ezpplauncher",
"build": { "build": {
@@ -16,19 +16,45 @@
"title": "EZPPLauncher", "title": "EZPPLauncher",
"width": 1000, "width": 1000,
"height": 650, "height": 650,
"minWidth": 1000,
"minHeight": 650,
"decorations": false, "decorations": false,
"resizable": false, "resizable": false,
"maximizable": false "maximizable": false,
"shadow": true
} }
], ],
"withGlobalTauri": true, "withGlobalTauri": true,
"security": { "security": {
"csp": null "csp": {
"default-src": "'self' tauri: customprotocol: asset:",
"script-src": "'self' 'unsafe-inline' asset: http://asset.localhost https://asset.localhost https://analytics.ez-pp.farm",
"style-src": "'self' 'unsafe-inline' asset: http://asset.localhost https://asset.localhost",
"img-src": "'self' asset: http://asset.localhost https://asset.localhost blob: data: https://assets.ppy.sh https://a.ez-pp.farm https://git.ez-pp.farm https://osu.direct",
"connect-src": "'self' ipc: http://ipc.localhost https://ez-pp.farm https://api.ez-pp.farm https://c.ez-pp.farm https://analytics.ez-pp.farm https://git.ez-pp.farm https://osu.direct https://osu.ppy.sh",
"media-src": "'self' asset: http://asset.localhost https://asset.localhost blob:",
"font-src": [
"'self'",
"asset:",
"blob:",
"data:",
"http://asset.localhost",
"https://asset.localhost"
]
},
"assetProtocol": {
"enable": true,
"scope": [
"$HOME/.ezpplauncher/themes/**"
]
}
} }
}, },
"bundle": { "bundle": {
"publisher": "EZPPFarm", "publisher": "EZPPFarm",
"copyright": "EZPPFarm", "copyright": "EZPPFarm",
"shortDescription": "osu! launcher for EZPPFarm",
"longDescription": "EZPPLauncher is a custom osu! launcher for the EZPPFarm server, featuring automatic updates, ingame login, stat tracking and launcher theming.",
"homepage": "https://ez-pp.farm", "homepage": "https://ez-pp.farm",
"license": "GPL-3.0", "license": "GPL-3.0",
"active": true, "active": true,
@@ -36,6 +62,17 @@
"nsis", "nsis",
"appimage" "appimage"
], ],
"fileAssociations": [
{
"ext": [
"ezpplauncher-theme"
],
"name": "EZPPLauncher Theme",
"description": "EZPPLauncher Theme File",
"role": "Viewer",
"mimeType": "application/x-ezpplauncher-theme"
}
],
"windows": { "windows": {
"webviewInstallMode": { "webviewInstallMode": {
"silent": true, "silent": true,
@@ -44,6 +81,7 @@
"nsis": { "nsis": {
"installMode": "currentUser", "installMode": "currentUser",
"compression": "lzma", "compression": "lzma",
"installerIcon": "icons/icon.ico",
"installerHooks": "./nsis/nsis-hooks.nsh" "installerHooks": "./nsis/nsis-hooks.nsh"
} }
}, },
+4 -1
View File
@@ -4,6 +4,9 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@import "@fontsource-variable/sora";
@import "@fontsource/space-mono";
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(95.36% 0.017 256.57); --background: oklch(95.36% 0.017 256.57);
@@ -41,7 +44,7 @@
--sidebar-ring: oklch(60.7% 0.226 299.68); --sidebar-ring: oklch(60.7% 0.226 299.68);
--font-sans: --font-sans:
"Sora", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Sora Variable", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji"; "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: --font-mono:
"Space Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Space Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

+1 -1
View File
@@ -3,7 +3,7 @@ import type {
EZPPUserInfoResponse, EZPPUserInfoResponse,
EZPPUserResponse, EZPPUserResponse,
EZPPUSerStatusResponse, EZPPUSerStatusResponse,
} from '@/types'; } from '$lib/types';
import { betterFetch } from '@better-fetch/fetch'; import { betterFetch } from '@better-fetch/fetch';
const BANCHO_ENDPOINT = 'https://c.ez-pp.farm/'; const BANCHO_ENDPOINT = 'https://c.ez-pp.farm/';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Release } from '@/types'; import type { Release } from '$lib/types';
import { betterFetch } from '@better-fetch/fetch'; import { betterFetch } from '@better-fetch/fetch';
import semver from 'semver'; import semver from 'semver';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { StreamsResult } from '@/types'; import type { StreamsResult } from '$lib/types';
import { betterFetch } from '@better-fetch/fetch'; import { betterFetch } from '@better-fetch/fetch';
const API_ENDPOINT = 'https://osu.ppy.sh/api/'; const API_ENDPOINT = 'https://osu.ppy.sh/api/';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/components/ui/button/index.js'; import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { buttonVariants } from '$lib/components/ui/button/index.js'; import { buttonVariants } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from 'bits-ui';
import AlertDialogOverlay from './alert-dialog-overlay.svelte';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import AlertDialogOverlay from './alert-dialog-overlay.svelte';
import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils.js';
import type { WithElementRef } from 'bits-ui'; import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils.js';
import type { WithElementRef } from 'bits-ui'; import type { WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
+4 -4
View File
@@ -1,12 +1,12 @@
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
import Title from './alert-dialog-title.svelte';
import Action from './alert-dialog-action.svelte'; import Action from './alert-dialog-action.svelte';
import Cancel from './alert-dialog-cancel.svelte'; import Cancel from './alert-dialog-cancel.svelte';
import Content from './alert-dialog-content.svelte';
import Description from './alert-dialog-description.svelte';
import Footer from './alert-dialog-footer.svelte'; import Footer from './alert-dialog-footer.svelte';
import Header from './alert-dialog-header.svelte'; import Header from './alert-dialog-header.svelte';
import Overlay from './alert-dialog-overlay.svelte'; import Overlay from './alert-dialog-overlay.svelte';
import Content from './alert-dialog-content.svelte'; import Title from './alert-dialog-title.svelte';
import Description from './alert-dialog-description.svelte'; import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
const Root = AlertDialogPrimitive.Root; const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger; const Trigger = AlertDialogPrimitive.Trigger;
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { reduceAnimations } from '$lib/userSettings';
//TODO: maybe dynamic background images fetched from ezpp? //TODO: maybe dynamic background images fetched from ezpp?
const COUNT = 60; const COUNT = 60;
@@ -7,7 +9,8 @@
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
} }
const streaks = Array.from({ length: COUNT }).map((_, i) => { const streaks = Array.from({ length: COUNT })
.map((_, i) => {
const depth = rand(0, 2); const depth = rand(0, 2);
const scale = 0.6 + depth * 0.8; const scale = 0.6 + depth * 0.8;
@@ -17,8 +20,8 @@
const duration = distance / speed; const duration = distance / speed;
return { return {
width: rand(180, 320) * scale, width: rand(300, 320) * scale,
height: rand(16, 32) * scale, height: 32 * scale,
x: (i / COUNT) * 200 - 50, x: (i / COUNT) * 200 - 50,
y: ((i * 137.5) % 100) - 20, y: ((i * 137.5) % 100) - 20,
opacity: 0.25 + depth * 0.65, opacity: 0.25 + depth * 0.65,
@@ -26,22 +29,23 @@
delay: rand(0, duration), delay: rand(0, duration),
scale, scale,
}; };
}); })
.sort((a, b) => a.scale - b.scale);
</script> </script>
<div class="absolute bg blur-[2px]"> <div class="absolute bg blur-[2px]">
<div <div
class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50" class="relative h-full w-full from-0% to-90% bg-gradient-to-b from-transparent to-black z-50"
></div> ></div>
{#each streaks as s} {#each streaks as s, index (index)}
<div <div
class="streak drop-shadow-lg" class="streak drop-shadow-lg transition-opacity duration-1000"
style=" style="
width:{s.width}px; width:{s.width}px;
height:{s.height}px; height:{s.height}px;
left:{s.x}%; left:{s.x}%;
top:{s.y}%; top:{s.y}%;
opacity:{s.opacity}; opacity:{$reduceAnimations ? 0 : s.opacity};
animation-duration:{s.duration}s; animation-duration:{s.duration}s;
animation-delay:-{s.delay}s; animation-delay:-{s.delay}s;
" "
@@ -54,7 +58,12 @@
position: fixed; position: fixed;
inset: 0; inset: 0;
overflow: hidden; overflow: hidden;
background: linear-gradient(180deg, var(--color-primary-300) 0%, var(--color-primary-600) 40%, var(--color-primary-900) 100%); background: linear-gradient(
180deg,
var(--color-primary-300) 0%,
var(--color-primary-600) 40%,
var(--color-primary-900) 100%
);
} }
.streak { .streak {
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { Avatar as AvatarPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
+2 -2
View File
@@ -1,6 +1,6 @@
import Root from './avatar.svelte';
import Image from './avatar-image.svelte';
import Fallback from './avatar-fallback.svelte'; import Fallback from './avatar-fallback.svelte';
import Image from './avatar-image.svelte';
import Root from './avatar.svelte';
export { export {
Root, Root,
+2 -2
View File
@@ -22,9 +22,9 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { cn } from '$lib/utils.js';
import type { WithElementRef } from 'bits-ui'; import type { WithElementRef } from 'bits-ui';
import type { HTMLAnchorAttributes } from 'svelte/elements'; import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -42,7 +42,7 @@
this={href ? 'a' : 'span'} this={href ? 'a' : 'span'}
bind:this={ref} bind:this={ref}
{href} {href}
class={cn(badgeVariants({ variant, className }))} class={cn(badgeVariants({ variant }), className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
+2 -2
View File
@@ -53,13 +53,13 @@
</script> </script>
{#if href} {#if href}
<a bind:this={ref} class={cn(buttonVariants({ variant, size, className }))} {href} {...restProps}> <a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
{@render children?.()} {@render children?.()}
</a> </a>
{:else} {:else}
<button <button
bind:this={ref} bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))} class={cn(buttonVariants({ variant, size }), className)}
{type} {type}
{...restProps} {...restProps}
> >
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; import { cn } from '$lib/utils.js';
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 { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),
+1
View File
@@ -1,4 +1,5 @@
import Root from './checkbox.svelte'; import Root from './checkbox.svelte';
export { export {
Root, Root,
// //
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Ellipsis from 'lucide-svelte/icons/ellipsis'; import Ellipsis from '@lucide/svelte/icons/ellipsis';
import Play from 'lucide-svelte/icons/play'; import Play from '@lucide/svelte/icons/play';
const prop: { const prop: {
downloading: boolean; downloading: boolean;
@@ -17,7 +17,7 @@
</script> </script>
<button <button
class="group flex items-center h-12 rounded-full bg-primary-300 disabled:bg-primary-300/70 shadow-lg shadow-primary/25 transition-all not-disabled:hover:shadow-primary/40 not-disabled:hover:brightness-110 not-disabled:active:scale-[0.98] cursor-pointer w-fit" class="group flex items-center h-12 rounded-full bg-primary disabled:bg-primary/70 shadow-lg shadow-primary/25 transition-all not-disabled:hover:shadow-primary/40 not-disabled:hover:brightness-110 not-disabled:active:scale-[0.98] cursor-pointer w-fit"
disabled={prop.disabled} disabled={prop.disabled}
onclick={prop.onClick} onclick={prop.onClick}
> >
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui'; import { cn } from '$lib/utils.js';
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 { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
let { let {
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { let {
ref = $bindable(null), ref = $bindable(null),

Some files were not shown because too many files have changed in this diff Show More