diff --git a/.gitignore b/.gitignore index 28b197fb..4c99d205 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ target/ .DS_Store/ .opencode/ -opencode.json \ No newline at end of file +opencode.json + +__pycache__/ +.directory \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index d2cfe067..dc597761 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,18 +33,35 @@ cargo test | Frontend | `src/` | Vue 3 + TypeScript | | Backend | `src-tauri/src/` | Rust (Tauri v2) | -Communication is exclusively via Tauri `invoke()` commands – there is no HTTP API between the two halves. +Communication is exclusively via Tauri `invoke()` commands – there is no HTTP API between the two halves. The frontend API layer (`src/api/clients/internal.ts`) routes all HTTP calls through `invoke("api_request")`, so the Rust backend handles actual network requests. ### Frontend layout (`src/`) - `main.ts` → `bootstrap/applicationBootstrap.ts` (mounts correct Vue component based on `?window=` URL param) - Multiple window types: `main` (default), `network`, `customization` – each is a different root component -- `features/` – domain folders: `auth/`, `clients/`, `download/`, `friends/`, `marketplace/`, `presets/`, `social/` -- `api/` – Tauri invoke wrappers -- `services/` – shared services (i18n, etc.) -- `shared/` – reusable components/composables +- `features/` – domain modules: `auth/`, `chat/`, `clients/`, `download/`, `friends/`, `marketplace/`, `presets/`, `social/` +- `api/` – Tauri invoke wrappers (`ApiClient` class, re-exports `apiGet`/`apiPost`/etc.) +- `services/` – business logic services (i18n, settings, theme, chat, updater, etc.) +- `shared/` – reusable components, composables, types, utils +- `components/` – top-level UI components (`core/`, `customization/`, `features/`, `modals/`, `presets/`, `settings/`) +- `composables/` – app-level composables (`useAppInit`, `useAppHandlers`, `useDownloadSpeedMonitor`, `useIrcChat`) +- `views/` – page-level view components (16 views mapped in `router/views.ts`) +- `windows/` – separate window root components (`CustomizationWindow.vue`, `NetworkWindow.vue`) +- `layouts/` – layout components (`Sidebar.vue`, `Titlebar.vue`, forms, modals) +- `utils/` – top-level utilities (`settings.ts`, `tabs.ts`) +- `assets/` – static assets (CSS, images, icons, videos) - `config.ts` – API URL initialisation; must call `initializeApiUrl()` before using `getApiUrl()` +### Router + +The app uses a **custom lightweight router** (not vue-router) implemented with Vue refs in `router/index.ts`. API: `push()`, `replace()`, `back()`, `canGoBack()`, `clearHistory()`. Route-to-component mapping is in `router/views.ts` with `tabOrder` for transition direction. + +Supported routes: `home`, `news`, `settings`, `about`, `customization`, `custom_clients`, `app_logs`, `account`, `login`, `register`, `verify`, `friends`, `user-profile`, `marketplace`, `network_debug`, `chat`. + +### State management + +There are no Pinia/Vuex stores. State is managed through **services** (singletons) and **composables** (reactive state). The `@stores` alias is defined in config but `src/stores/` does not exist. + ### Path aliases (configured in both `vite.config.ts` and `tsconfig.json`) ``` @@ -55,19 +72,34 @@ Communication is exclusively via Tauri `invoke()` commands – there is no HTTP @services → src/services/ @layouts → src/layouts/ @router → src/router/index.ts (bare alias) -@stores → src/stores/ +@stores → src/stores/ (alias defined but directory does not exist) +@components → src/components/ ``` ### Backend layout (`src-tauri/src/`) -- `lib.rs` – app entry, Tauri builder, all `invoke_handler` registrations -- `commands/` – Tauri command handlers (clients, irc, presets, settings, updater, utils, network, report) +- `main.rs` – binary entry point; parses CLI args, loads `.env`, calls `prepare_startup()` then `run()` +- `lib.rs` – library crate entry; Tauri builder, `invoke_handler!` macro (120+ commands), `setup` hook, `on_window_event` +- `commands/` – Tauri command handlers: + - `clients.rs` – client launch, download, mods, logs, shortcuts + - `irc.rs` – IRC connect/disconnect/send (`IrcState` managed by Tauri) + - `mod_builds.rs` – mod build CRUD (create, update, delete, export, import) + - `network.rs` – `api_request`, network history + - `presets.rs` – preset CRUD + - `report.rs` – network report generation/export + - `settings.rs` – accounts, favorites, flags, settings, telemetry + - `updater.rs` – check/download/install updates + - `utils.rs` – data folder, base64, version, Discord RPC, tray, launch history - `core/` – business logic - - `clients/` – client manager - - `network/` – downloader, servers, analytics, API - - `platform/` – OS-specific code (Windows message boxes, DPI) - - `storage/` – persistent state (settings, accounts, favorites, presets, custom clients, flags) - - `utils/` – globals, logging macros, helpers, process, hashing, archive, Discord RPC + - `clients/` – client manager, custom clients, log checker, agent overlay verification + - `network/` – downloader, servers (health checks), API client, cache, server ads + - `platform/` – OS-specific code (Windows WebView2, Linux WebKitGTK, message boxes, DPI) + - `storage/` – persistent state (settings, accounts, favorites, flags, presets, custom clients, mod builds, launch history) + - `utils/` – globals, logging macros, helpers, process, hashing, archive, Discord RPC, DPI, taskbar, CLI args, module tags + - `state.rs` – `AppState`, `ClientState`, `CustomClientsState` (Tauri managed state) + - `app_runtime.rs` – `StartupRuntime`, tray menu parsing, deep link parsing/deduplication +- `build.rs` – build script; captures git hash/branch/body, sets `DEVELOPMENT` env var +- `tests/` – Rust unit tests (7 modules: clients_command, data, manager, runtime, settings, updater, utils_command) **Adding a new Tauri command**: implement in `commands/.rs`, then register it in the `invoke_handler!` macro in `lib.rs`. @@ -100,18 +132,30 @@ Env vars are parsed at Rust startup via `parse_env_bool()` in `core/utils/global ## Toolchain quirks - **TypeScript is strict**: `strict`, `noUnusedLocals`, `noUnusedParameters` are all enabled. Unused vars cause build failures unless prefixed with `_`. -- **ESLint targets `.ts`, `.tsx`, `.vue`** – `.js` files in `src/` are also linted. -- **Prettier**: 4-space indent, trailing commas (`es5`). No `tailwind.config.js` – Tailwind v4 config lives in CSS via `@import "tailwindcss"` and `@plugin "daisyui"`. +- **ESLint** uses flat config (`eslint.config.cjs`) targeting `.ts`, `.tsx`, `.vue`, `.js` files. Unused vars require `_` prefix. +- **Prettier**: 4-space indent, trailing commas (`es5`). See `.prettierrc.yaml`. +- **Tailwind CSS v4** config lives in CSS via `@import "tailwindcss"` and `@plugin "daisyui"` – no `tailwind.config.js`. - **daisyUI v5** is the component library (see `.github/instructions/daisyui.instructions.md` for full class reference). Use daisyUI semantic color names (`bg-primary`, `text-base-content`, etc.) instead of Tailwind hardcoded colors so themes work. - **Monaco editor** loads from CDN (`cdn.jsdelivr.net/npm/monaco-editor@0.55/min/vs`) – not bundled locally. - **`daisyui` is excluded from Vite `optimizeDeps`** – do not add it back. - **Rust `profile.dev`**: incremental builds, `opt-level=0`, `debug=1`. Dependencies are compiled at `opt-level=2` for reasonable dev performance. +- **i18n**: 5 languages supported (English, Polish, Russian, Ukrainian, Chinese Simplified) via `vue-i18n` in `services/i18n/`. --- ## CI / Build -- CI triggers on push to `main` or `dev` branches. +### Workflows + +- **`build.yml`** – triggers on push to `main` or `dev` branches, or manual `workflow_dispatch`. + - `check-flags` job: parses commit message for `[skip ci]`/`skip-ci`/`skip_ci` and `release`/`build macos` keywords. + - `build` job (matrix: ubuntu-22.04 + windows-latest): installs deps, builds Tauri bundles, uploads artifacts. + - `build-macos` job (conditional): builds universal binary (`universal-apple-darwin`) with aarch64 + x86_64 targets. + - `create-release` job: creates GitHub prerelease tagged `build-` with all artifacts. +- **`cleanup-artifacts.yml`** – runs weekly (Sunday 2 AM UTC) or manually; keeps only the 5 newest build artifacts. + +### Build details + - Commit messages containing `[skip ci]`, `skip-ci`, or `skip_ci` skip the build job. - Commit messages containing `release` or `build macos` trigger the macOS universal binary build (otherwise macOS is skipped). - Artifacts: `.msi`, NSIS `.exe`, portable `.exe` (Windows); `.AppImage`, `.deb`, `.rpm` (Linux); `.dmg` (macOS, conditional). @@ -133,15 +177,17 @@ Files updated: `package.json`, `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.jso ## Scripts -| Script | Purpose | -| ---------------------------------- | -------------------------------------------------------------- | -| `scripts/bump_version.py` | Bump version across all config files | -| `scripts/serve_mock_release.py` | Serve mock update JSON on `localhost:8000` for updater testing | -| `scripts/new_client.py` | Add new client entry (interactive menu or CLI args) | -| `scripts/md5.py` | Compute MD5 hash of a file (interactive or CLI) | -| `scripts/scripts_gui.py` | Web GUI with buttons – opens browser at localhost:8765 | -| `scripts/remove_releases.py` | Delete old GitHub releases (dry-run by default) | -| `scripts/remove_unused_actions.py` | Delete unused GH Actions runs | +| Script | Purpose | +| ---------------------------------- | ------------------------------------------------------------------------ | +| `scripts/bump_version.py` | Bump version across all config files | +| `scripts/serve_mock_release.py` | Serve mock update JSON on `localhost:8000` for updater testing | +| `scripts/new_client.py` | Add new client entry (interactive menu or CLI args) | +| `scripts/new_client.cjs` | Node.js equivalent of new_client.py (CLI-only) | +| `scripts/md5.py` | Compute MD5 hash of a file (interactive or CLI) | +| `scripts/md5.cjs` | Node.js equivalent of md5.py (CLI-only) | +| `scripts/scripts_gui.py` | Web GUI with buttons – opens browser at localhost:8765 | +| `scripts/remove_releases.py` | Delete old GitHub releases (dry-run by default, needs `GITHUB_TOKEN`) | +| `scripts/remove_unused_actions.py` | Delete unused GH Actions runs (dry-run by default, needs `GITHUB_TOKEN`) | --- @@ -157,9 +203,24 @@ The app registers the `collapseloader://` URI scheme. Supported actions: `verify --- +## Tauri plugins + +The app uses these Tauri plugins: `opener`, `notification`, `dialog`, `fs`, `deep-link`, `single-instance` (with deep-link feature). + +--- + ## Windows-only behaviour - Junction points (not symlinks) are used on Windows to share `resourcepacks` and `shaderpacks` across clients. - `junction` crate is a Windows-only dependency. - DPI scaling helper (`core/utils/dpi.rs`) runs a background process on Windows when configured. +- Taskbar progress indicator (`core/utils/taskbar.rs`) via Windows COM integration. - WebView2 is required; the app prompts to install it if missing. + +--- + +## Linux-only behaviour + +- WebKitGTK dependency checks at startup (`core/platform/linux.rs`). +- `zbus` crate used for Linux-specific D-Bus integration. +- `mold` linker used in CI for faster builds. diff --git a/package-lock.json b/package-lock.json index 0b62bb65..5ccbf5f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,50 +1,48 @@ { "name": "collapseloader", - "version": "1.2.1", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "collapseloader", - "version": "1.2.1", + "version": "1.2.2", "license": "GPL-3.0-only", "dependencies": { "@guolao/vue-monaco-editor": "1.6.0", - "@lucide/vue": "1.24.0", + "@lucide/vue": "1.38.0", "@stomp/stompjs": "^7.3.0", - "@supabase/supabase-js": "^2.110.5", + "@supabase/supabase-js": "^2.112.4", "@tauri-apps/api": "2.11.1", - "@tauri-apps/plugin-dialog": "2.7.1", + "@tauri-apps/plugin-dialog": "2.7.2", "@tauri-apps/plugin-fs": "2.5.1", "@tauri-apps/plugin-notification": "2.3.3", "@tauri-apps/plugin-opener": "2.5.4", - "axios": "1.18.1", + "axios": "1.20.0", "gsap": "3.15.0", - "monaco-editor": "^0.55.1", + "monaco-editor": "^0.56.0", "tls": "^0.0.1", - "vue": "3.5.39", - "vue-i18n": "11.4.6" + "vue": "3.5.42", + "vue-i18n": "11.4.10" }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.2", - "@tailwindcss/vite": "^4.3.2", + "@tailwindcss/postcss": "^4.3.3", + "@tailwindcss/vite": "^4.3.3", "@tauri-apps/cli": "2.11.4", - "@types/node": "26.1.1", - "@typescript-eslint/eslint-plugin": "8.64.0", - "@typescript-eslint/parser": "8.64.0", + "@types/node": "26.4.0", "@vitejs/plugin-vue": "6.0.8", "axios-mock-adapter": "^2.1.0", - "daisyui": "^5.6.18", - "eslint": "^10.7.0", - "eslint-plugin-vue": "10.9.2", - "jsdom": "^29.1.1", - "prettier": "3.9.5", - "tailwindcss": "4.3.2", - "typescript": "^5.8.3", - "vite": "^8.1.4", - "vitest": "^4.1.10", + "daisyui": "^5.7.22", + "eslint": "^10.9.1", + "eslint-plugin-vue": "10.10.0", + "jsdom": "^30.0.1", + "prettier": "3.9.6", + "tailwindcss": "4.3.3", + "typescript": "~5.8.3", + "vite": "^8.2.2", + "vitest": "^4.1.11", "vue-eslint-parser": "^10.4.1", - "vue-tsc": "3.3.7" + "vue-tsc": "3.3.11" } }, "node_modules/@alloc/quick-lru": { @@ -61,56 +59,38 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.13.0 || >=24.0.0" } }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -130,12 +110,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -145,9 +125,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -171,9 +151,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", "dev": true, "funding": [ { @@ -191,9 +171,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -215,9 +195,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", - "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", "dev": true, "funding": [ { @@ -231,8 +211,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -266,9 +246,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", - "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.10.tgz", + "integrity": "sha512-xBja6gaAaH2R2c7eNyl0TY4dhnnZ2uhj+KXpLdEQ6M/wuk9bYFZM8wY0ykw3VO4TgEJ56KGlerXS/9KBKVR/Cg==", "dev": true, "funding": [ { @@ -310,44 +290,10 @@ "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -363,6 +309,19 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", @@ -389,9 +348,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -569,14 +528,14 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz", - "integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==", + "version": "11.4.10", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.10.tgz", + "integrity": "sha512-+yJ74JRWVJokdgG9zYNMyTSzeNV3O9T4vVxk8PvLFHmI+R/BYA//cITh7vhRK37hWLZ4/kTcKcUz1dlWOpypIg==", "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.4.6", - "@intlify/message-compiler": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/devtools-types": "11.4.10", + "@intlify/message-compiler": "11.4.10", + "@intlify/shared": "11.4.10" }, "engines": { "node": ">= 22" @@ -586,13 +545,13 @@ } }, "node_modules/@intlify/devtools-types": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz", - "integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==", + "version": "11.4.10", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.10.tgz", + "integrity": "sha512-xZxzZsAuu6/0zoLRVQWdpXWe5Kjl0LnWpjlQA3r9u9FbLYMhapqt7IwkgQyn0Tm2GUNAqhj9eZiUmYOrB024BQ==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/core-base": "11.4.10", + "@intlify/shared": "11.4.10" }, "engines": { "node": ">= 22" @@ -602,12 +561,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz", - "integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==", + "version": "11.4.10", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.10.tgz", + "integrity": "sha512-oUB/scz2EJENXDiUJ7JjZffOrH8UIZ1BuZeHvonbi5fWLavLt04aivuk2OIByOZA0tsci1bkeeQRmwhb5M8Imw==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.4.6", + "@intlify/shared": "11.4.10", "source-map-js": "^1.0.2" }, "engines": { @@ -618,9 +577,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz", - "integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==", + "version": "11.4.10", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.10.tgz", + "integrity": "sha512-FeImVdPeoSHTm3NBFFZHv0eRP9gQ3F4lj2puDBX5Kw7iiM1uJW6JTf39ian0K/17pbXCI3ef5i9RVsRrALqI6Q==", "license": "MIT", "engines": { "node": ">= 22" @@ -662,9 +621,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -679,9 +638,9 @@ } }, "node_modules/@lucide/vue": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.24.0.tgz", - "integrity": "sha512-5bNPX0G2YEWdUlBYk7pE8SgDg/f1mkIFpJ9vtE44pW/cwRz7Ioc0tOTESoVJAPvxIELSmYekX+XXIJMjsswNIg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.38.0.tgz", + "integrity": "sha512-65f+77mbqXaBwi3I33JujaUpyOMvQfBc5sNmKS5NEc7PRbV/cDkuCcWSu/hrp0sEPhv8MDPDUdOVCtag06iDGg==", "license": "ISC", "peerDependencies": { "vue": ">=3.0.1" @@ -696,39 +655,37 @@ "state-local": "^1.0.6" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", "cpu": [ "arm64" ], @@ -743,9 +700,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", "cpu": [ "arm64" ], @@ -760,9 +717,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", "cpu": [ "x64" ], @@ -777,9 +734,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", "cpu": [ "x64" ], @@ -794,9 +751,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", "cpu": [ "arm" ], @@ -811,13 +768,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -828,13 +788,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -845,13 +808,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -862,13 +828,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -879,13 +848,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -896,13 +868,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -913,9 +888,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", "cpu": [ "arm64" ], @@ -929,29 +904,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", - "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", "cpu": [ "arm64" ], @@ -966,9 +922,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", "cpu": [ "x64" ], @@ -1003,9 +959,9 @@ "license": "Apache-2.0" }, "node_modules/@supabase/auth-js": { - "version": "2.110.5", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.5.tgz", - "integrity": "sha512-QSlI5CNeEefHP95/GbeMhNgr8aHEHiXFh8c1IWthYyI9ZzBwEigYzJFCw4Ff+GkL8OK9tArBmGGv6rpg5zLXSw==", + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.112.4.tgz", + "integrity": "sha512-z8DesgwLzKM5PiT0yNmJU8VJyh1zAhYi+20Z7drdJQLXg/wWW4yGt/un+He5ERYUo94Vz66t5aeyr1DIDemI5A==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1015,9 +971,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.110.5", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.110.5.tgz", - "integrity": "sha512-nuQuoIoEGT8ukrwr6THlY5v50bSMJ2rqti1FFsGyDaC98POLmcFQVFFh23PVPhRsKWUxrFc0MpmEoHKa7CY4mg==", + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.112.4.tgz", + "integrity": "sha512-DQ0aVH8wSQAccVqNoEkec62qCu2QRNyoGN53RqsVZ1k6F1zq4/v8scrlR6LNT2RJmT97apiTmORijPVhErCS2g==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1027,15 +983,15 @@ } }, "node_modules/@supabase/phoenix": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.4.tgz", - "integrity": "sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", + "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", "license": "MIT" }, "node_modules/@supabase/postgrest-js": { - "version": "2.110.5", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.110.5.tgz", - "integrity": "sha512-eObfgBjxLPzFakwMpUmsv8nuNPOhoDhzzN8cwvEvGkxkmuRIZcOCYH9+DRbpKnxh7tgsBZW+KWtaZI2j98b2/g==", + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.112.4.tgz", + "integrity": "sha512-uaubtPSeg2TR4wrtfQoQWgkTAe+a0qWX2KhmwvTfNl5mGN9+U7owiJt6abk3o/V6O899PSRD1yzxs5RlF4xTug==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1045,12 +1001,12 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.110.5", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.110.5.tgz", - "integrity": "sha512-VtOZxw5jfrc37KgIfFdp9SOFJjtvJ0qU+gT8CPQjL7cqy4DJlaTXacw5aBBogLZksqAI0YN67qhlDaMKFwfOuw==", + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.112.4.tgz", + "integrity": "sha512-vZ+j079SKrM0Xiq7MJCvQKLDpaH2kfKfLY68xuQE1sqsCsMmx1CyrDBJHsxZ3cX01VOs5SI9igmoZAF3BmdZxw==", "license": "MIT", "dependencies": { - "@supabase/phoenix": "0.4.4", + "@supabase/phoenix": "0.4.5", "tslib": "2.8.1" }, "engines": { @@ -1058,9 +1014,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.110.5", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.110.5.tgz", - "integrity": "sha512-7GkOZlrYknVGVPyijoDbu5OXGvQ2oQ6dkU0juAkIMPZ9QgtmJ8IPrxe76zGSbmzbaC1GdKw7pqeXEi2HT67sbA==", + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.112.4.tgz", + "integrity": "sha512-lQ0JemuTlMIXVKgSci1qez8yPnM5hyDngeAfEBjZS2Om4D+Cus0EE5BE6glFobrxdyii1OF4UzWfF0zcQgDq5A==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -1071,65 +1027,73 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.110.5", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.110.5.tgz", - "integrity": "sha512-cAO1Nm+CCogRNVXN93bBkh0vjOdLM5e6J9gB/cHV9Lqni/gEmN2HJFrnn4NI33GYIfmlh4Wbm6siH+XXRgpexA==", + "version": "2.112.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.112.4.tgz", + "integrity": "sha512-UiCX1udlFY1fQQrO7Z3GU7obQsju0w5Vk9mOOwalfo/+Gy+tahWVenSSuu5E/GTy/q//HxvGv2IrCdW66/61kw==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.110.5", - "@supabase/functions-js": "2.110.5", - "@supabase/postgrest-js": "2.110.5", - "@supabase/realtime-js": "2.110.5", - "@supabase/storage-js": "2.110.5" + "@supabase/auth-js": "2.112.4", + "@supabase/functions-js": "2.112.4", + "@supabase/postgrest-js": "2.112.4", + "@supabase/realtime-js": "2.112.4", + "@supabase/storage-js": "2.112.4" }, "engines": { "node": ">=22.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -1144,9 +1108,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -1161,9 +1125,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -1178,9 +1142,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -1195,9 +1159,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -1212,13 +1176,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1229,13 +1196,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1246,13 +1216,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1263,13 +1236,16 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1280,9 +1256,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1310,9 +1286,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -1327,9 +1303,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -1344,29 +1320,29 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", - "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "postcss": "^8.5.15", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -1471,6 +1447,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1488,6 +1467,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1505,6 +1487,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1522,6 +1507,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1539,6 +1527,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1600,9 +1591,9 @@ } }, "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", - "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.2.tgz", + "integrity": "sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==", "license": "MIT OR Apache-2.0", "dependencies": { "@tauri-apps/api": "^2.11.0" @@ -1635,17 +1626,6 @@ "@tauri-apps/api": "^2.11.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1686,9 +1666,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1702,540 +1682,173 @@ "license": "MIT", "optional": true }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", - "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/type-utils": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", - "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", - "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "dependencies": { + "@volar/source-map": "2.4.28" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", - "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", - "dev": true, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", - "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", - "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.64.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", - "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.64.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", - "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", - "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.28" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", - "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", - "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.28", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", - "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.39", + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -2260,29 +1873,29 @@ "license": "MIT" }, "node_modules/@vue/compiler-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", - "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", - "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.39", - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39", + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.15", + "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, @@ -2293,13 +1906,13 @@ "license": "MIT" }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", - "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" } }, "node_modules/@vue/devtools-api": { @@ -2309,9 +1922,9 @@ "license": "MIT" }, "node_modules/@vue/language-core": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.7.tgz", - "integrity": "sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.11.tgz", + "integrity": "sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2325,59 +1938,57 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", - "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.39" + "@vue/shared": "3.5.42" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", - "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", - "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/runtime-core": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", - "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39" - }, - "peerDependencies": { - "vue": "3.5.39" + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" } }, "node_modules/@vue/shared": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", - "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", "license": "MIT" }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -2450,13 +2061,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -2503,16 +2114,16 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/call-bind-apply-helpers": { @@ -2606,9 +2217,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.6.18", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.6.18.tgz", - "integrity": "sha512-6y9rboRQl8fosnusy/5pg11yp+H0LtK3YdtfS4Vf2QVnaYOKasNdYm6hDxeiT6bwZPdl4WRPzVIlIApdo2ssBg==", + "version": "5.7.22", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.22.tgz", + "integrity": "sha512-pYii+2NCN0+OIL6Lpuc054LK0QJeNv+GRK9UzN1GRmjdYhYO6WFl0QLoj5oPcitPD7o+okiiI0SKFkn641TxmA==", "dev": true, "license": "MIT", "funding": { @@ -2629,6 +2240,21 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2680,9 +2306,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -2703,9 +2329,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "dev": true, "license": "MIT", "dependencies": { @@ -2748,9 +2374,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -2795,9 +2421,9 @@ } }, "node_modules/eslint": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", - "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", "dev": true, "license": "MIT", "workspaces": [ @@ -2807,7 +2433,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -2831,7 +2457,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2854,18 +2480,18 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz", - "integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", + "@eslint-community/eslint-utils": "^4.9.1", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^7.1.0", - "semver": "^7.6.3", - "xml-name-validator": "^4.0.0" + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2905,19 +2531,6 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", @@ -2930,16 +2543,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/espree": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", @@ -2958,19 +2561,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3028,9 +2618,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3121,9 +2711,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -3148,16 +2738,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3337,9 +2927,9 @@ } }, "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3428,39 +3018,39 @@ } }, "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", + "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^3.2.3" }, "peerDependenciesMeta": { "canvas": { @@ -3468,16 +3058,6 @@ } } }, - "node_modules/jsdom/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3666,6 +3246,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3687,6 +3270,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3708,6 +3294,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3729,6 +3318,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3801,9 +3393,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3869,13 +3461,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -3885,12 +3477,12 @@ } }, "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", "license": "MIT", "dependencies": { - "dompurify": "3.2.7", + "dompurify": "3.4.8", "marked": "14.0.0" } }, @@ -3908,9 +3500,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -3946,15 +3538,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -4060,9 +3655,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -4073,9 +3668,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -4092,7 +3687,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4101,9 +3696,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -4125,9 +3720,9 @@ } }, "node_modules/prettier": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", - "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -4170,13 +3765,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4186,21 +3781,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.4", - "@rolldown/binding-darwin-arm64": "1.1.4", - "@rolldown/binding-darwin-x64": "1.1.4", - "@rolldown/binding-freebsd-x64": "1.1.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", - "@rolldown/binding-linux-arm64-gnu": "1.1.4", - "@rolldown/binding-linux-arm64-musl": "1.1.4", - "@rolldown/binding-linux-ppc64-gnu": "1.1.4", - "@rolldown/binding-linux-s390x-gnu": "1.1.4", - "@rolldown/binding-linux-x64-gnu": "1.1.4", - "@rolldown/binding-linux-x64-musl": "1.1.4", - "@rolldown/binding-openharmony-arm64": "1.1.4", - "@rolldown/binding-wasm32-wasi": "1.1.4", - "@rolldown/binding-win32-arm64-msvc": "1.1.4", - "@rolldown/binding-win32-x64-msvc": "1.1.4" + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" } }, "node_modules/saxes": { @@ -4217,9 +3812,9 @@ } }, "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -4282,9 +3877,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -4296,9 +3891,9 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, @@ -4324,9 +3919,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -4351,9 +3946,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -4361,22 +3956,22 @@ } }, "node_modules/tldts": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", - "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.2" + "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", - "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", "dev": true, "license": "MIT" }, @@ -4386,9 +3981,9 @@ "integrity": "sha512-GzHpG+hwupY8VMR6rYsnAhTHqT/97zT45PG8WD5eTT1lq+dFE0nN+1PYpsoBcHJgSmTz5ceK2Cv88IkPmIPOtQ==" }, "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4411,19 +4006,6 @@ "node": ">=20" } }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4458,13 +4040,13 @@ } }, "node_modules/undici": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", - "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { @@ -4492,16 +4074,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "bin": { @@ -4518,7 +4100,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -4569,20 +4151,293 @@ } } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -4610,12 +4465,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -4660,23 +4515,23 @@ } }, "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", "dev": true, "license": "MIT" }, "node_modules/vue": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", - "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-sfc": "3.5.39", - "@vue/runtime-dom": "3.5.39", - "@vue/server-renderer": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" }, "peerDependencies": { "typescript": "*" @@ -4711,28 +4566,15 @@ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/vue-i18n": { - "version": "11.4.6", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz", - "integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==", + "version": "11.4.10", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.10.tgz", + "integrity": "sha512-Lp+BjOxqzOY87DS6Z8KrQrpiTr9IN/Lt4kZEilwyXG2Wrx+AcU6IVsAW92HNXtVcn1HFFPV6ty41p9e/qDpyvg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/devtools-types": "11.4.6", - "@intlify/shared": "11.4.6", + "@intlify/core-base": "11.4.10", + "@intlify/devtools-types": "11.4.10", + "@intlify/shared": "11.4.10", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -4746,14 +4588,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.7.tgz", - "integrity": "sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.11.tgz", + "integrity": "sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.3.7" + "@vue/language-core": "3.3.11" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -4775,16 +4617,6 @@ "node": ">=18" } }, - "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -4806,18 +4638,18 @@ } }, "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.11.0", + "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/which": { @@ -4864,13 +4696,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { diff --git a/package.json b/package.json index 5db0cce0..98142bfd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "collapseloader", "private": true, - "version": "1.2.2", + "version": "1.3.0", "author": "dest4590", "license": "GPL-3.0-only", "maintainers": [ @@ -25,40 +25,38 @@ }, "dependencies": { "@guolao/vue-monaco-editor": "1.6.0", - "@lucide/vue": "1.24.0", + "@lucide/vue": "1.38.0", "@stomp/stompjs": "^7.3.0", - "@supabase/supabase-js": "^2.110.5", + "@supabase/supabase-js": "^2.112.4", "@tauri-apps/api": "2.11.1", - "@tauri-apps/plugin-dialog": "2.7.1", + "@tauri-apps/plugin-dialog": "2.7.2", "@tauri-apps/plugin-fs": "2.5.1", "@tauri-apps/plugin-notification": "2.3.3", "@tauri-apps/plugin-opener": "2.5.4", - "axios": "1.18.1", + "axios": "1.20.0", "gsap": "3.15.0", - "monaco-editor": "^0.55.1", + "monaco-editor": "^0.56.0", "tls": "^0.0.1", - "vue": "3.5.39", - "vue-i18n": "11.4.6" + "vue": "3.5.42", + "vue-i18n": "11.4.10" }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.2", - "@tailwindcss/vite": "^4.3.2", + "@tailwindcss/postcss": "^4.3.3", + "@tailwindcss/vite": "^4.3.3", "@tauri-apps/cli": "2.11.4", - "@types/node": "26.1.1", - "@typescript-eslint/eslint-plugin": "8.64.0", - "@typescript-eslint/parser": "8.64.0", + "@types/node": "26.4.0", "@vitejs/plugin-vue": "6.0.8", "axios-mock-adapter": "^2.1.0", - "daisyui": "^5.6.18", - "eslint": "^10.7.0", - "eslint-plugin-vue": "10.9.2", - "jsdom": "^29.1.1", - "prettier": "3.9.5", - "tailwindcss": "4.3.2", - "typescript": "^5.8.3", - "vite": "^8.1.4", - "vitest": "^4.1.10", + "daisyui": "^5.7.22", + "eslint": "^10.9.1", + "eslint-plugin-vue": "10.10.0", + "jsdom": "^30.0.1", + "prettier": "3.9.6", + "tailwindcss": "4.3.3", + "typescript": "~5.8.3", + "vite": "^8.2.2", + "vitest": "^4.1.11", "vue-eslint-parser": "^10.4.1", - "vue-tsc": "3.3.7" + "vue-tsc": "3.3.11" } } diff --git a/scripts/clients/gui_template.html b/scripts/clients/gui_template.html new file mode 100644 index 00000000..508a4be2 --- /dev/null +++ b/scripts/clients/gui_template.html @@ -0,0 +1,593 @@ + + + + + + CollapseLoader Scripts + + + +
+

CollapseLoader Scripts

+ +
+
+ MD5 Hash +
+
New Client
+
+ +
+ +
+ + +
+
+
Computing...
+ +
+ +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + +
+ + + + +
+ +
+
+ + + + +
+
Processing...
+ +
+
+ + + + diff --git a/scripts/clients/md5.cjs b/scripts/clients/md5.cjs new file mode 100644 index 00000000..23af9364 --- /dev/null +++ b/scripts/clients/md5.cjs @@ -0,0 +1,36 @@ +// md5.cjs – computes MD5 hash of a .jar file +// Usage: node md5.cjs + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); + +function usage() { + console.error("Usage: node md5.cjs "); + process.exit(1); +} + +const [, , target] = process.argv; +if (!target) usage(); + +const filePath = path.resolve(target); +if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + process.exit(1); +} +if (path.extname(filePath).toLowerCase() !== ".jar") { + console.error("Error: file must have .jar extension"); + process.exit(1); +} + +const hash = crypto.createHash("md5"); +const stream = fs.createReadStream(filePath); +stream.on("error", (err) => { + console.error("Read error:", err.message); + process.exit(1); +}); +stream.on("data", (chunk) => hash.update(chunk)); +stream.on("end", () => { + const digest = hash.digest("hex"); + console.log(`MD5(${path.basename(filePath)}) = ${digest}`); +}); diff --git a/scripts/md5.py b/scripts/clients/md5.py old mode 100755 new mode 100644 similarity index 100% rename from scripts/md5.py rename to scripts/clients/md5.py diff --git a/scripts/clients/new_client.cjs b/scripts/clients/new_client.cjs new file mode 100644 index 00000000..e0d7526a --- /dev/null +++ b/scripts/clients/new_client.cjs @@ -0,0 +1,243 @@ +const fs = require("fs"); +const crypto = require("crypto"); +const path = require("path"); +const https = require("https"); + +const VERSION_MANIFEST_URL = + "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; +const FALLBACK_VERSIONS = ["1.21.4", "1.21.8", "1.21.10", "1.21.11"]; + +function fetchVersions() { + return new Promise((resolve) => { + https + .get( + VERSION_MANIFEST_URL, + { headers: { "User-Agent": "CollapseLoader-Script/1.0" } }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + try { + const json = JSON.parse(data); + const releases = json.versions + .filter((v) => v.type === "release") + .map((v) => v.id); + resolve(releases); + } catch { + resolve(FALLBACK_VERSIONS); + } + }); + } + ) + .on("error", () => resolve(FALLBACK_VERSIONS)); + }); +} + +async function main() { + const args = process.argv.slice(2); + const showVersions = args.includes("--versions"); + + if (showVersions) { + console.log("Fetching Minecraft versions from Mojang..."); + const versions = await fetchVersions(); + console.log(`\nAvailable release versions (${versions.length}):\n`); + versions.forEach((v, i) => console.log(` ${i + 1}) ${v}`)); + process.exit(0); + } + + const filePath = args.find( + (a) => + !a.startsWith("--") && + !a.includes("\\") && + !a.includes("/") && + a.endsWith(".jar") + ); + const version = args.find( + (a) => /^\d+\.\d+/.test(a) && !a.includes("\\") && !a.includes("/") + ); + const clientType = + args.find((a) => ["default", "fabric", "forge"].includes(a)) || + "default"; + const cdnRoot = + args.find((a) => a.includes("\\") || a.includes("/")) || "E:\\hf-cdn"; + const extraFlags = args.filter( + (a) => + !a.startsWith("--") && + !a.includes("\\") && + !a.includes("/") && + !a.endsWith(".jar") && + !["default", "fabric", "forge"].includes(a) && + !/^\d+\.\d+/.test(a) + ); + + if (!filePath || !version) { + console.error( + "Usage: node scripts/new_client.cjs [default|fabric|forge] [flags...] [cdn-root]" + ); + console.error( + " node scripts/new_client.cjs --versions # list available versions" + ); + console.error("Flags: kotlin, satin, sodium"); + console.error( + 'Example: node scripts/new_client.cjs "E:\\hf-cdn\\clients\\fabric\\jars\\lambda.jar" "1.21.11" fabric kotlin' + ); + process.exit(1); + } + + const KOTLIN_DEP = { + md5_hash: "964103287b72e606de845420d1a8cc57", + name: "fabric-language-kotlin-1.13.8+kotlin.2.3.0", + size: 7, + }; + const SATIN_DEP = { + md5_hash: "2cf1534f9e818bd567837979444557e9", + name: "satin-3.0.0-alpha.1", + size: 0, + }; + const SODIUM_DEP = { + md5_hash: "28922a78d1876ee062e3265f10abcc46", + name: "sodium-fabric-0.6.13+mc1.21.4", + size: 1, + }; + + const BARITONE_DEPS = { + "1.21.4": [ + { + md5_hash: "0f8e922606f64c422cafafc0ad887c0e", + name: "baritone-api-fabric-1.13.1", + size: 2, + }, + { + md5_hash: "56cc7fc0294adc92cbbefe6f456d8f68", + name: "baritone-standalone-fabric-1.13.1", + size: 1, + }, + { + md5_hash: "015e00b79c6ae76881373d367b88a565", + name: "baritone-unoptimized-fabric-1.13.1", + size: 2, + }, + ], + }; + + const FABRIC_BASE_DEPS = { + "1.21.4": [ + { + md5_hash: "128a8d042180e7c92567342e21a21a6d", + name: "fabric-api-0.119.4+1.21.4", + size: 2, + }, + ], + "1.21.8": [ + { + md5_hash: "85d76d57a7b5bb7043ea815133d2f6ba", + name: "fabric-api-0.136.1+1.21.8", + size: 2, + }, + ], + "1.21.10": [ + { + md5_hash: "c9ebf1b300d813310d18115a7cc03f99", + name: "fabric-api-0.138.4+1.21.10", + size: 2, + }, + ], + "1.21.11": [ + { + md5_hash: "e2a72b6c6aa2c6c4f74541394858c86a", + name: "fabric-api-0.140.2+1.21.11", + size: 2, + }, + ], + }; + + const MAIN_CLASSES = { + default: "net.minecraft.client.main.Main", + fabric: "net.fabricmc.loader.launch.knot.KnotClient", + forge: "net.minecraft.launchwrapper.Launch", + }; + + const JSON_FILES = { + default: path.join(cdnRoot, "static", "clients.json"), + fabric: path.join(cdnRoot, "static", "fabric-clients.json"), + forge: path.join(cdnRoot, "static", "forge-clients.json"), + }; + + const FILENAME_PREFIXES = { + default: "", + fabric: "fabric/", + forge: "forge/", + }; + + const jsonPath = JSON_FILES[clientType]; + const existing = fs.existsSync(jsonPath) + ? JSON.parse(fs.readFileSync(jsonPath, "utf8")) + : []; + + const file = path.basename(filePath); + const filename = FILENAME_PREFIXES[clientType] + file; + + if (existing.find((c) => c.filename === filename)) { + console.error( + `Client "${filename}" already exists in ${path.basename(jsonPath)}` + ); + process.exit(1); + } + + const buf = fs.readFileSync(filePath); + const md5 = crypto.createHash("md5").update(buf).digest("hex"); + const size = Math.round(fs.statSync(filePath).size / 1024 / 1024); + const nextId = + existing.length > 0 ? Math.max(...existing.map((c) => c.id)) + 1 : 1; + + const entry = { + client_type: clientType, + created_at: new Date().toISOString(), + downloads: 0, + filename, + id: nextId, + launches: 0, + main_class: MAIN_CLASSES[clientType], + md5_hash: md5, + name: path.basename(file, ".jar"), + show: true, + size, + version, + working: true, + }; + + if (clientType === "fabric") { + const deps = [...(FABRIC_BASE_DEPS[version] || [])]; + if (extraFlags.includes("kotlin")) deps.push(KOTLIN_DEP); + if (extraFlags.includes("satin")) deps.push(SATIN_DEP); + if (extraFlags.includes("sodium")) deps.push(SODIUM_DEP); + if (extraFlags.includes("baritone")) { + if (BARITONE_DEPS[version]) { + for (const dep of BARITONE_DEPS[version]) { + deps.push(dep); + } + } else { + console.warn( + `Warning: baritone requested but no definition for version ${version}` + ); + } + } + entry.dependencies = deps; + if (extraFlags.length) + console.log(`Extra deps: ${extraFlags.join(", ")}`); + } + if (clientType === "forge") { + entry.dependencies = []; + } + + existing.unshift(entry); + fs.mkdirSync(path.dirname(jsonPath), { recursive: true }); + fs.writeFileSync(jsonPath, JSON.stringify(existing, null, 2)); + + console.log( + `Added "${entry.name}" (id=${entry.id}) to ${path.basename(jsonPath)}` + ); + console.log(`md5: ${md5} | size: ${size}MB | version: ${version}`); +} + +main(); diff --git a/scripts/new_client.py b/scripts/clients/new_client.py old mode 100755 new mode 100644 similarity index 52% rename from scripts/new_client.py rename to scripts/clients/new_client.py index 5f678c80..9ba3e68e --- a/scripts/new_client.py +++ b/scripts/clients/new_client.py @@ -9,6 +9,8 @@ import json import hashlib import os +import re +import subprocess import sys from datetime import datetime, timezone from pathlib import Path @@ -19,14 +21,11 @@ SODIUM_DEP = {"md5_hash": "28922a78d1876ee062e3265f10abcc46", "name": "sodium-fabric-0.6.13+mc1.21.4", "size": 1} BARITONE_DEPS = { - "1.21.11": {"md5_hash": "dbd83c7de8426f2facdc73f0a3a1da48", "name": "baritone-1.21.11", "size": 2}, -} - -FABRIC_BASE_DEPS = { - "1.21.4": [{"md5_hash": "128a8d042180e7c92567342e21a21a6d", "name": "fabric-api-0.119.4+1.21.4", "size": 2}], - "1.21.8": [{"md5_hash": "85d76d57a7b5bb7043ea815133d2f6ba", "name": "fabric-api-0.136.1+1.21.8", "size": 2}], - "1.21.10": [{"md5_hash": "c9ebf1b300d813310d18115a7cc03f99", "name": "fabric-api-0.138.4+1.21.10", "size": 2}], - "1.21.11": [{"md5_hash": "e2a72b6c6aa2c6c4f74541394858c86a", "name": "fabric-api-0.140.2+1.21.11", "size": 2}], + "1.21.4": [ + {"md5_hash": "0f8e922606f64c422cafafc0ad887c0e", "name": "baritone-api-fabric-1.13.1", "size": 2}, + {"md5_hash": "56cc7fc0294adc92cbbefe6f456d8f68", "name": "baritone-standalone-fabric-1.13.1", "size": 1}, + {"md5_hash": "015e00b79c6ae76881373d367b88a565", "name": "baritone-unoptimized-fabric-1.13.1", "size": 2}, + ], } MAIN_CLASSES = { @@ -44,6 +43,12 @@ } CDN_ROOT = os.environ.get("CDN_ROOT", "/media/w1xced/Disk/hf-cdn") +HF_VERSIONS_URL = "https://huggingface.co/api/datasets/Collapsecdn/collapsecdn/tree/main/misc/minecraft-versions" +FALLBACK_VERSIONS: dict[str, list[str]] = { + "default": ["1.8.9", "1.16.5"], + "fabric": ["1.21.4", "1.21.8", "1.21.11"], + "forge": ["1.8.9"], +} def compute_md5(filepath: Path) -> str: @@ -113,8 +118,110 @@ def pick_client_type() -> str | None: return CLIENT_TYPES[choice - 1] -def pick_version() -> str | None: - versions = ["1.21.4", "1.21.8", "1.21.10", "1.21.11"] +def _sort_versions(versions: list[str]) -> list[str]: + def to_num(v: str) -> int: + parts = v.split(".") + return sum(int(p) * (1000 ** (2 - i)) for i, p in enumerate(parts)) + return sorted(versions, key=to_num) + + +FABRIC_API_RE = re.compile(r"^fabric-api-([0-9.]+\+\d+\.\d+\.\d+)\.jar$") + + +def scan_local_deps(cdn_root: str) -> dict: + """Scan {cdn_root}/clients/fabric/deps/jars/ and return parsed deps with MD5.""" + deps_dir = os.path.join(cdn_root, "clients", "fabric", "deps", "jars") + result: dict[str, dict] = {} + if not os.path.isdir(deps_dir): + return result + for fname in sorted(os.listdir(deps_dir)): + if not fname.endswith(".jar"): + continue + fpath = os.path.join(deps_dir, fname) + if not os.path.isfile(fpath): + continue + md5 = compute_md5(Path(fpath)) + size_mb = round(os.path.getsize(fpath) / 1024 / 1024) + result[fname] = { + "md5_hash": md5, + "name": fname.replace(".jar", ""), + "size": size_mb, + } + return result + + +def _find_dep(local_deps: dict, keyword: str) -> dict | None: + """Find a dep by keyword in local scanned deps.""" + for fname, info in local_deps.items(): + if keyword.lower() in fname.lower(): + return {"md5_hash": info["md5_hash"], "name": info["name"], "size": info["size"]} + return None + + +def pick_fabric_api(cdn_root: str, mc_version: str) -> dict | None: + """Let user pick a fabric-api version from local deps, auto-compute MD5.""" + local = scan_local_deps(cdn_root) + api_deps = {k: v for k, v in local.items() if FABRIC_API_RE.match(k)} + if not api_deps: + print("No fabric-api jars found in CDN deps folder.") + return None + + matching = {k: v for k, v in api_deps.items() if f"+{mc_version}.jar" in k} + if not matching: + print(f"\nNo fabric-api for MC {mc_version}. Available:") + matching = api_deps + + items = sorted(matching.items(), key=lambda x: x[1]["name"]) + print("\nFabric API version:\n") + for i, (_, info) in enumerate(items, 1): + print(f" {i}) {info['name']} (md5: {info['md5_hash'][:12]}...)") + print(f"\n 0) None (skip fabric-api)\n") + + try: + choice = int(input("Select fabric-api: ")) + except (ValueError, EOFError): + return None + + if choice == 0 or choice > len(items): + return None + _, info = items[choice - 1] + return {"md5_hash": info["md5_hash"], "name": info["name"], "size": info["size"]} + + +def fetch_cdn_versions() -> dict[str, list[str]]: + """Parse jar filenames from HuggingFace CDN to get available versions per type.""" + try: + result = subprocess.run( + ["curl", "-s", "--max-time", "10", HF_VERSIONS_URL], + capture_output=True, text=True, timeout=15, + ) + if result.returncode == 0 and result.stdout: + data = json.loads(result.stdout) + map_fabric: set[str] = set() + map_forge: set[str] = set() + for item in data: + if item.get("type") == "file" and item.get("path"): + filename = item["path"].rsplit("/", 1)[-1] + m = re.match(r"^(fabric|forge)_(.+)\.jar$", filename) + if m: + kind, ver = m.group(1), m.group(2) + if kind == "fabric": + map_fabric.add(ver) + elif kind == "forge": + map_forge.add(ver) + return { + "default": FALLBACK_VERSIONS["default"], + "fabric": _sort_versions(list(map_fabric)) if map_fabric else FALLBACK_VERSIONS["fabric"], + "forge": _sort_versions(list(map_forge)) if map_forge else FALLBACK_VERSIONS["forge"], + } + except Exception: + pass + return {**FALLBACK_VERSIONS} + + +def pick_version(client_type: str = "fabric") -> str | None: + cdn = fetch_cdn_versions() + versions = cdn.get(client_type, cdn.get("fabric", [])) print("\nMinecraft version:\n") for i, v in enumerate(versions, 1): print(f" {i}) {v}") @@ -182,7 +289,7 @@ def main(): print("Cancelled.") sys.exit(0) - version = pick_version() + version = pick_version(client_type) if not version: print("Cancelled.") sys.exit(0) @@ -221,21 +328,38 @@ def main(): } if client_type == "fabric": - deps = list(FABRIC_BASE_DEPS.get(version, [])) + deps = [] + fabric_api = pick_fabric_api(CDN_ROOT, version) + if fabric_api: + deps.append(fabric_api) + if len(sys.argv) > 4: flags = [a.lower() for a in sys.argv[4:] if not os.sep in a and "/" not in a] else: flags = pick_flags() + local = scan_local_deps(CDN_ROOT) if "kotlin" in flags: - deps.append(KOTLIN_DEP) + dep = _find_dep(local, "kotlin") + if dep: deps.append(dep) + else: deps.append(KOTLIN_DEP) if "satin" in flags: - deps.append(SATIN_DEP) + dep = _find_dep(local, "satin") + if dep: deps.append(dep) + else: deps.append(SATIN_DEP) if "sodium" in flags: - deps.append(SODIUM_DEP) + dep = _find_dep(local, "sodium") + if dep: deps.append(dep) + else: deps.append(SODIUM_DEP) if "baritone" in flags: - if version in BARITONE_DEPS: - deps.append(BARITONE_DEPS[version]) + known_names = {d["name"] for d in BARITONE_DEPS.get(version, [])} + local_baritone = [v for k, v in local.items() if "baritone" in k.lower() and v["name"] in known_names] + if local_baritone: + for dep in local_baritone: + deps.append({"md5_hash": dep["md5_hash"], "name": dep["name"], "size": dep["size"]}) + elif version in BARITONE_DEPS: + for dep in BARITONE_DEPS[version]: + deps.append(dep) else: print(f"Warning: baritone not available for {version}") diff --git a/scripts/clients/scripts_gui.py b/scripts/clients/scripts_gui.py new file mode 100644 index 00000000..67bdf5ae --- /dev/null +++ b/scripts/clients/scripts_gui.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 + +import hashlib +import json +import os +import re + +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path +import webbrowser + +CDN_ROOT = os.environ.get("CDN_ROOT", "/media/w1xced/disk/collapsecdn") + +FALLBACK_VERSIONS: dict[str, list[str]] = { + "default": ["1.8.9", "1.16.5"], + "fabric": ["1.21.4", "1.21.8", "1.21.11"], + "forge": ["1.8.9"], +} + +FABRIC_API_RE = re.compile(r"^fabric-api-([0-9.]+\+\d+\.\d+\.\d+)\.jar$") +OTHER_DEP_RE = re.compile(r"^(.+)\.jar$") + +KOTLIN_DEP = {"md5_hash": "964103287b72e606de845420d1a8cc57", "name": "fabric-language-kotlin-1.13.8+kotlin.2.3.0", "size": 7} +SATIN_DEP = {"md5_hash": "2cf1534f9e818bd567837979444557e9", "name": "satin-3.0.0-alpha.1", "size": 0} +SODIUM_DEP = {"md5_hash": "28922a78d1876ee062e3265f10abcc46", "name": "sodium-fabric-0.6.13+mc1.21.4", "size": 1} +BARITONE_DEPS = { + "1.21.4": [ + {"md5_hash": "0f8e922606f64c422cafafc0ad887c0e", "name": "baritone-api-fabric-1.13.1", "size": 2}, + {"md5_hash": "56cc7fc0294adc92cbbefe6f456d8f68", "name": "baritone-standalone-fabric-1.13.1", "size": 1}, + {"md5_hash": "015e00b79c6ae76881373d367b88a565", "name": "baritone-unoptimized-fabric-1.13.1", "size": 2}, + ], +} +FABRIC_BASE_DEPS = { + "1.21.4": [{"md5_hash": "128a8d042180e7c92567342e21a21a6d", "name": "fabric-api-0.119.4+1.21.4", "size": 2}], + "1.21.8": [{"md5_hash": "85d76d57a7b5bb7043ea815133d2f6ba", "name": "fabric-api-0.136.1+1.21.8", "size": 2}], + "1.21.10": [{"md5_hash": "c9ebf1b300d813310d18115a7cc03f99", "name": "fabric-api-0.138.4+1.21.10", "size": 2}], + "1.21.11": [{"md5_hash": "e2a72b6c6aa2c6c4f74541394858c86a", "name": "fabric-api-0.140.2+1.21.11", "size": 2}], +} +MAIN_CLASSES = { + "default": "net.minecraft.client.main.Main", + "fabric": "net.fabricmc.loader.launch.knot.KnotClient", + "forge": "net.minecraft.launchwrapper.Launch", +} +FILENAMES = {"default": "clients.json", "fabric": "fabric-clients.json", "forge": "forge-clients.json"} +DEFAULT_VIAVERSION = "5.9.1" +VIA_VERSIONS = ["5.3.0", "5.7.1", "5.9.1", "5.11.0"] +DEFAULT_JAVA_VERSION = "8" +JAVA_VERSIONS = ["8", "21"] + +HTML_TEMPLATE_PATH = Path(__file__).parent / "gui_template.html" + + +def _sort_versions(versions: list[str]) -> list[str]: + def to_num(v: str) -> int: + parts = v.split(".") + return sum(int(p) * (1000 ** (2 - i)) for i, p in enumerate(parts)) + return sorted(versions, key=to_num) + + +def compute_md5(filepath): + h = hashlib.md5() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +def scan_cdn_client_versions(cdn_root: str) -> dict[str, list[str]]: + result: dict[str, list[str]] = {t: [] for t in FALLBACK_VERSIONS} + mv_dir = os.path.join(cdn_root, "misc", "minecraft-versions") + if not os.path.isdir(mv_dir): + return {"default": FALLBACK_VERSIONS["default"], "fabric": FALLBACK_VERSIONS["fabric"], "forge": FALLBACK_VERSIONS["forge"]} + for client_type in ["fabric", "forge"]: + versions: set[str] = set() + for fname in os.listdir(mv_dir): + if not fname.endswith(".jar"): + continue + m = re.match(r"^" + re.escape(client_type) + r"_(.+)\.jar$", fname) + if m: + versions.add(m.group(1)) + result[client_type] = _sort_versions(list(versions)) if versions else FALLBACK_VERSIONS.get(client_type, []) + result["default"] = FALLBACK_VERSIONS["default"] + return result + + +def _find_dep(local_other: dict, keyword: str) -> dict | None: + for fname, info in local_other.items(): + if keyword.lower() in fname.lower(): + return {"md5_hash": info["md5_hash"], "name": info["name"], "size": info["size"]} + return None + + +def _find_file_by_name(name: str) -> str | None: + base = CDN_ROOT + search_dirs = [ + os.path.join(base, "clients", "fabric", "deps", "jars"), + os.path.join(base, "misc", "minecraft-versions"), + os.path.join(base, "clients", "fabric"), + base, + ] + for d in search_dirs: + if not os.path.isdir(d): + continue + for root, dirs, files in os.walk(d): + if name in files: + return os.path.join(root, name) + return None + + +def scan_local_deps(cdn_root: str) -> dict: + deps_dir = os.path.join(cdn_root, "clients", "fabric", "deps", "jars") + result = {"fabric_api": {}, "other": {}} + if not os.path.isdir(deps_dir): + return result + for fname in sorted(os.listdir(deps_dir)): + if not fname.endswith(".jar"): + continue + fpath = os.path.join(deps_dir, fname) + if not os.path.isfile(fpath): + continue + md5 = compute_md5(fpath) + size_mb = round(os.path.getsize(fpath) / 1024 / 1024) + m_api = FABRIC_API_RE.match(fname) + if m_api: + api_ver = m_api.group(1) + parts = api_ver.split("+") + result["fabric_api"][fname] = { + "md5_hash": md5, + "name": fname.replace(".jar", ""), + "size": size_mb, + "api_version": parts[0] if parts else "", + "mc_version": parts[1] if len(parts) > 1 else "", + } + else: + m_other = OTHER_DEP_RE.match(fname) + if m_other: + result["other"][fname] = { + "md5_hash": md5, + "name": fname.replace(".jar", ""), + "size": size_mb, + } + return result + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, format, *args): + print(f"[SERVER] {format % args}") + + def _json(self, code, data): + body = json.dumps(data).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _read_body(self): + length = int(self.headers.get("Content-Length", 0)) + return json.loads(self.rfile.read(length)) if length else {} + + def do_GET(self): + if self.path == "/" or self.path == "/index.html": + versions = scan_cdn_client_versions(CDN_ROOT) + deps = scan_local_deps(CDN_ROOT) + payload = json.dumps({"versions": versions, "deps": deps, "viaversions": VIA_VERSIONS, "default_viaversion": DEFAULT_VIAVERSION, "javaversions": JAVA_VERSIONS, "default_java_version": DEFAULT_JAVA_VERSION}).replace("\\", "\\\\").replace("'", "\\'") + html = HTML_TEMPLATE_PATH.read_text(encoding="utf-8") + body = html.replace("CDN_ROOT_PLACEHOLDER", CDN_ROOT).replace("/*__INIT_DATA__*/", f"window.__INIT={payload};").encode() + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self.send_error(404) + + def do_POST(self): + try: + if self.path == "/api/load": + data = self._read_body() + root = data.get("cdn_root", CDN_ROOT) + versions = scan_cdn_client_versions(root) + deps = scan_local_deps(root) + self._json(200, {"versions": versions, "deps": deps}) + + elif self.path == "/api/deps": + data = self._read_body() + root = data.get("cdn_root", CDN_ROOT) + deps = scan_local_deps(root) + self._json(200, {"deps": deps}) + + elif self.path == "/api/md5": + data = self._read_body() + filepath = data.get("path", "").strip() + if not filepath: + self._json(200, {"error": "No file path provided"}) + return + if not os.path.isfile(filepath): + found = _find_file_by_name(filepath) + if found: + filepath = found + else: + self._json(200, {"error": f"File not found: {filepath}"}) + return + digest = compute_md5(filepath) + self._json(200, {"name": os.path.basename(filepath), "hash": digest}) + + elif self.path == "/api/client": + data = self._read_body() + jar = data.get("jar", "").strip() + version = data.get("version", "1.21.11") + client_type = data.get("client_type", "fabric") + flags = data.get("flags", []) + viaversion = data.get("viaversion", DEFAULT_VIAVERSION) + if viaversion not in VIA_VERSIONS: + viaversion = DEFAULT_VIAVERSION + java_version = data.get("java_version", DEFAULT_JAVA_VERSION) + if java_version not in JAVA_VERSIONS: + java_version = DEFAULT_JAVA_VERSION + cdn_root = data.get("cdn_root", CDN_ROOT) + + if not jar: + self._json(200, {"error": "No JAR file specified"}) + return + + json_file = FILENAMES.get(client_type, "clients.json") + json_path = os.path.join(cdn_root, "static", json_file) + + existing = [] + if os.path.exists(json_path): + with open(json_path, "r", encoding="utf-8") as f: + existing = json.load(f) + + filename = jar if client_type == "default" else f"{client_type}/{jar}" + if any(c.get("filename") == filename for c in existing): + self._json(200, {"error": f'Client "{filename}" already exists in {json_file}'}) + return + + md5 = compute_md5(jar) if os.path.isfile(jar) else "unknown" + size_mb = 0 + if os.path.isfile(jar): + size_mb = round(os.path.getsize(jar) / 1024 / 1024) + + next_id = max((c.get("id", 0) for c in existing), default=0) + 1 + name = os.path.splitext(os.path.basename(jar))[0] + + entry = { + "client_type": client_type, + "created_at": datetime.now(timezone.utc).isoformat(), + "downloads": 0, + "filename": filename, + "id": next_id, + "launches": 0, + "main_class": MAIN_CLASSES.get(client_type, MAIN_CLASSES["default"]), + "md5_hash": md5, + "name": name, + "show": True, + "size": size_mb, + "version": version, + "working": True, + } + + if client_type == "default" and viaversion: + entry["viaversion"] = viaversion + if client_type == "default" and version == "1.8.9": + entry["java_version"] = java_version + + if client_type == "fabric": + deps = [] + fabric_api = data.get("fabric_api") + if fabric_api and fabric_api.get("md5_hash"): + deps.append({ + "md5_hash": fabric_api["md5_hash"], + "name": fabric_api["name"], + "size": fabric_api.get("size", 0), + }) + local = scan_local_deps(cdn_root) + local_other = local.get("other", {}) + if "kotlin" in flags: + dep = _find_dep(local_other, "kotlin") + if dep: deps.append(dep) + else: deps.append(KOTLIN_DEP) + if "satin" in flags: + dep = _find_dep(local_other, "satin") + if dep: deps.append(dep) + else: deps.append(SATIN_DEP) + if "sodium" in flags: + dep = _find_dep(local_other, "sodium") + if dep: deps.append(dep) + else: deps.append(SODIUM_DEP) + if "baritone" in flags: + known_names = {d["name"] for d in BARITONE_DEPS.get(version, [])} + local_baritone = [v for k, v in local_other.items() if "baritone" in k.lower() and v["name"] in known_names] + if local_baritone: + for dep in local_baritone: + deps.append(dep) + elif version in BARITONE_DEPS: + for dep in BARITONE_DEPS[version]: + deps.append(dep) + entry["dependencies"] = deps + elif client_type == "forge": + entry["dependencies"] = [] + + existing.insert(0, entry) + os.makedirs(os.path.dirname(json_path), exist_ok=True) + with open(json_path, "w", encoding="utf-8") as f: + json.dump(existing, f, indent=2, ensure_ascii=False) + + self._json(200, {"name": name, "id": next_id, "md5": md5, "size": size_mb}) + else: + self.send_error(404) + except Exception as e: + self._json(500, {"error": str(e)}) + + +def main(): + port = 8765 + server = HTTPServer(("127.0.0.1", port), Handler) + url = f"http://127.0.0.1:{port}" + print(f"Starting GUI at {url}") + print(f"CDN root: {CDN_ROOT}") + + webbrowser.open(url) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nStopped.") + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/scripts/md5.cjs b/scripts/md5.cjs deleted file mode 100644 index ae764641..00000000 --- a/scripts/md5.cjs +++ /dev/null @@ -1,36 +0,0 @@ -// md5.cjs – computes MD5 hash of a .jar file -// Usage: node md5.cjs - -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); - -function usage() { - console.error('Usage: node md5.cjs '); - process.exit(1); -} - -const [, , target] = process.argv; -if (!target) usage(); - -const filePath = path.resolve(target); -if (!fs.existsSync(filePath)) { - console.error(`File not found: ${filePath}`); - process.exit(1); -} -if (path.extname(filePath).toLowerCase() !== '.jar') { - console.error('Error: file must have .jar extension'); - process.exit(1); -} - -const hash = crypto.createHash('md5'); -const stream = fs.createReadStream(filePath); -stream.on('error', err => { - console.error('Read error:', err.message); - process.exit(1); -}); -stream.on('data', chunk => hash.update(chunk)); -stream.on('end', () => { - const digest = hash.digest('hex'); - console.log(`MD5(${path.basename(filePath)}) = ${digest}`); -}); diff --git a/scripts/new_client.cjs b/scripts/new_client.cjs deleted file mode 100644 index 82d19b1c..00000000 --- a/scripts/new_client.cjs +++ /dev/null @@ -1,107 +0,0 @@ -const fs = require('fs'); -const crypto = require('crypto'); -const path = require('path'); - -const filePath = process.argv[2]; -const version = process.argv[3]; -const clientType = process.argv[4] || 'default'; -const extraFlags = process.argv.slice(5).filter(a => !a.includes('\\') && !a.includes('/')); -const cdnRoot = process.argv.slice(5).find(a => a.includes('\\') || a.includes('/')) || 'E:\\hf-cdn'; - -if (!filePath || !version) { - console.error('Usage: node scripts/new_client.cjs [default|fabric|forge] [flags...] [cdn-root]'); - console.error('Flags: kotlin, satin, sodium'); - console.error('Example: node scripts/new_client.cjs "E:\\hf-cdn\\clients\\fabric\\jars\\lambda.jar" "1.21.11" fabric kotlin'); - process.exit(1); -} - -const KOTLIN_DEP = { md5_hash: '964103287b72e606de845420d1a8cc57', name: 'fabric-language-kotlin-1.13.8+kotlin.2.3.0', size: 7 }; -const SATIN_DEP = { md5_hash: '2cf1534f9e818bd567837979444557e9', name: 'satin-3.0.0-alpha.1', size: 0 }; -const SODIUM_DEP = { md5_hash: '28922a78d1876ee062e3265f10abcc46', name: 'sodium-fabric-0.6.13+mc1.21.4', size: 1 }; - -const BARITONE_DEPS = { - '1.21.11': { md5_hash: 'dbd83c7de8426f2facdc73f0a3a1da48', name: 'baritone-1.21.11', size: 2 } -}; - -const FABRIC_BASE_DEPS = { - '1.21.4': [{ md5_hash: '128a8d042180e7c92567342e21a21a6d', name: 'fabric-api-0.119.4+1.21.4', size: 2 }], - '1.21.8': [{ md5_hash: '85d76d57a7b5bb7043ea815133d2f6ba', name: 'fabric-api-0.136.1+1.21.8', size: 2 }], - '1.21.10': [{ md5_hash: 'c9ebf1b300d813310d18115a7cc03f99', name: 'fabric-api-0.138.4+1.21.10', size: 2 }], - '1.21.11': [{ md5_hash: 'e2a72b6c6aa2c6c4f74541394858c86a', name: 'fabric-api-0.140.2+1.21.11', size: 2 }], -}; - -const MAIN_CLASSES = { - default: 'net.minecraft.client.main.Main', - fabric: 'net.fabricmc.loader.launch.knot.KnotClient', - forge: 'net.minecraft.launchwrapper.Launch', -}; - -const JSON_FILES = { - default: path.join(cdnRoot, 'static', 'clients.json'), - fabric: path.join(cdnRoot, 'static', 'fabric-clients.json'), - forge: path.join(cdnRoot, 'static', 'forge-clients.json'), -}; - -const FILENAME_PREFIXES = { - default: '', - fabric: 'fabric/', - forge: 'forge/', -}; - -const jsonPath = JSON_FILES[clientType]; -const existing = fs.existsSync(jsonPath) ? JSON.parse(fs.readFileSync(jsonPath, 'utf8')) : []; - -const file = path.basename(filePath); -const filename = FILENAME_PREFIXES[clientType] + file; - -if (existing.find(c => c.filename === filename)) { - console.error(`Client "${filename}" already exists in ${path.basename(jsonPath)}`); - process.exit(1); -} - -const buf = fs.readFileSync(filePath); -const md5 = crypto.createHash('md5').update(buf).digest('hex'); -const size = Math.round(fs.statSync(filePath).size / 1024 / 1024); -const nextId = existing.length > 0 ? Math.max(...existing.map(c => c.id)) + 1 : 1; - -const entry = { - client_type: clientType, - created_at: new Date().toISOString(), - downloads: 0, - filename, - id: nextId, - launches: 0, - main_class: MAIN_CLASSES[clientType], - md5_hash: md5, - name: path.basename(file, '.jar'), - show: true, - size, - version, - working: true, -}; - -if (clientType === 'fabric') { - const deps = [...(FABRIC_BASE_DEPS[version] || [])]; - if (extraFlags.includes('kotlin')) deps.push(KOTLIN_DEP); - if (extraFlags.includes('satin')) deps.push(SATIN_DEP); - if (extraFlags.includes('sodium')) deps.push(SODIUM_DEP); - if (extraFlags.includes('baritone')) { - if (BARITONE_DEPS[version]) { - deps.push(BARITONE_DEPS[version]); - } else { - console.warn(`Warning: baritone requested but no definition for version ${version}`); - } - } - entry.dependencies = deps; - if (extraFlags.length) console.log(`Extra deps: ${extraFlags.join(', ')}`); -} -if (clientType === 'forge') { - entry.dependencies = []; -} - -existing.unshift(entry); -fs.mkdirSync(path.dirname(jsonPath), { recursive: true }); -fs.writeFileSync(jsonPath, JSON.stringify(existing, null, 2)); - -console.log(`Added "${entry.name}" (id=${entry.id}) to ${path.basename(jsonPath)}`); -console.log(`md5: ${md5} | size: ${size}MB | version: ${version}`); diff --git a/scripts/scripts_gui.py b/scripts/scripts_gui.py deleted file mode 100755 index efe7d22c..00000000 --- a/scripts/scripts_gui.py +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/env python3 -"""scripts_gui.py – Web GUI for CollapseLoader scripts (md5 + new_client) - -Opens a browser with clickable interface. -No dependencies – uses only Python stdlib. - -Usage: - python3 scripts/scripts_gui.py -""" - -import hashlib -import json -import os -import sys -import webbrowser -from datetime import datetime, timezone -from http.server import HTTPServer, BaseHTTPRequestHandler -from pathlib import Path -from urllib.parse import parse_qs, urlparse - -CDN_ROOT = os.environ.get("CDN_ROOT", "/media/w1xced/Disk/hf-cdn") - -KOTLIN_DEP = {"md5_hash": "964103287b72e606de845420d1a8cc57", "name": "fabric-language-kotlin-1.13.8+kotlin.2.3.0", "size": 7} -SATIN_DEP = {"md5_hash": "2cf1534f9e818bd567837979444557e9", "name": "satin-3.0.0-alpha.1", "size": 0} -SODIUM_DEP = {"md5_hash": "28922a78d1876ee062e3265f10abcc46", "name": "sodium-fabric-0.6.13+mc1.21.4", "size": 1} -BARITONE_DEPS = { - "1.21.11": {"md5_hash": "dbd83c7de8426f2facdc73f0a3a1da48", "name": "baritone-1.21.11", "size": 2}, -} -FABRIC_BASE_DEPS = { - "1.21.4": [{"md5_hash": "128a8d042180e7c92567342e21a21a6d", "name": "fabric-api-0.119.4+1.21.4", "size": 2}], - "1.21.8": [{"md5_hash": "85d76d57a7b5bb7043ea815133d2f6ba", "name": "fabric-api-0.136.1+1.21.8", "size": 2}], - "1.21.10": [{"md5_hash": "c9ebf1b300d813310d18115a7cc03f99", "name": "fabric-api-0.138.4+1.21.10", "size": 2}], - "1.21.11": [{"md5_hash": "e2a72b6c6aa2c6c4f74541394858c86a", "name": "fabric-api-0.140.2+1.21.11", "size": 2}], -} -MAIN_CLASSES = { - "default": "net.minecraft.client.main.Main", - "fabric": "net.fabricmc.loader.launch.knot.KnotClient", - "forge": "net.minecraft.launchwrapper.Launch", -} -FILENAMES = {"default": "clients.json", "fabric": "fabric-clients.json", "forge": "forge-clients.json"} - - -def compute_md5(filepath): - h = hashlib.md5() - with open(filepath, "rb") as f: - for chunk in iter(lambda: f.read(8192), b""): - h.update(chunk) - return h.hexdigest() - - -HTML = r""" - - - - -CollapseLoader Scripts - - - -
-

CollapseLoader Scripts

- -
-
MD5 Hash
-
New Client
-
- - -
- -
- - -
-
-
Computing...
- -
- - -
- -
- - -
- - - - - - - -
- -
- - - - -
- - - - -
-
Processing...
- -
-
- - - -""" - - -class Handler(BaseHTTPRequestHandler): - def log_message(self, format, *args): - pass - - def _json(self, code, data): - body = json.dumps(data).encode() - self.send_response(code) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def _read_body(self): - length = int(self.headers.get("Content-Length", 0)) - return json.loads(self.rfile.read(length)) if length else {} - - def do_GET(self): - if self.path == "/" or self.path == "/index.html": - body = HTML.replace("CDN_ROOT_PLACEHOLDER", CDN_ROOT).encode() - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - else: - self.send_error(404) - - def do_POST(self): - try: - if self.path == "/api/md5": - data = self._read_body() - filepath = data.get("path", "").strip() - if not filepath or not os.path.isfile(filepath): - self._json(200, {"error": f"File not found: {filepath}"}) - return - digest = compute_md5(filepath) - self._json(200, {"name": os.path.basename(filepath), "hash": digest}) - - elif self.path == "/api/client": - data = self._read_body() - jar = data.get("jar", "").strip() - version = data.get("version", "1.21.11") - client_type = data.get("client_type", "fabric") - flags = data.get("flags", []) - cdn_root = data.get("cdn_root", CDN_ROOT) - - if not jar: - self._json(200, {"error": "No JAR file specified"}) - return - - json_file = FILENAMES.get(client_type, "clients.json") - json_path = os.path.join(cdn_root, "static", json_file) - - existing = [] - if os.path.exists(json_path): - with open(json_path, "r", encoding="utf-8") as f: - existing = json.load(f) - - filename = jar if client_type == "default" else f"{client_type}/{jar}" - if any(c.get("filename") == filename for c in existing): - self._json(200, {"error": f'Client "{filename}" already exists in {json_file}'}) - return - - md5 = compute_md5(jar) if os.path.isfile(jar) else "unknown" - size_mb = 0 - if os.path.isfile(jar): - size_mb = round(os.path.getsize(jar) / 1024 / 1024) - - next_id = max((c.get("id", 0) for c in existing), default=0) + 1 - name = os.path.splitext(os.path.basename(jar))[0] - - entry = { - "client_type": client_type, - "created_at": datetime.now(timezone.utc).isoformat(), - "downloads": 0, - "filename": filename, - "id": next_id, - "launches": 0, - "main_class": MAIN_CLASSES.get(client_type, MAIN_CLASSES["default"]), - "md5_hash": md5, - "name": name, - "show": True, - "size": size_mb, - "version": version, - "working": True, - } - - if client_type == "fabric": - deps = list(FABRIC_BASE_DEPS.get(version, [])) - if "kotlin" in flags: deps.append(KOTLIN_DEP) - if "satin" in flags: deps.append(SATIN_DEP) - if "sodium" in flags: deps.append(SODIUM_DEP) - if "baritone" in flags and version in BARITONE_DEPS: - deps.append(BARITONE_DEPS[version]) - entry["dependencies"] = deps - elif client_type == "forge": - entry["dependencies"] = [] - - existing.insert(0, entry) - os.makedirs(os.path.dirname(json_path), exist_ok=True) - with open(json_path, "w", encoding="utf-8") as f: - json.dump(existing, f, indent=2, ensure_ascii=False) - - self._json(200, {"name": name, "id": next_id, "md5": md5, "size": size_mb}) - else: - self.send_error(404) - except Exception as e: - self._json(500, {"error": str(e)}) - - -def main(): - port = 8765 - server = HTTPServer(("127.0.0.1", port), Handler) - url = f"http://127.0.0.1:{port}" - print(f"Starting GUI at {url}") - webbrowser.open(url) - try: - server.serve_forever() - except KeyboardInterrupt: - print("\nStopped.") - server.server_close() - - -if __name__ == "__main__": - main() diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 808251c4..77dc9f1f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ "cipher", "cpubits", @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ascii" @@ -157,7 +157,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -186,13 +186,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -232,9 +232,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", @@ -265,6 +265,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -288,9 +294,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -306,9 +312,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", "zeroize", @@ -325,9 +331,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" dependencies = [ "async-channel", "async-task", @@ -368,13 +374,13 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] @@ -385,9 +391,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -419,7 +425,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cairo-sys-rs", "glib", "libc", @@ -440,9 +446,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.3" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] @@ -467,7 +473,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] @@ -482,9 +488,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -506,7 +512,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.23.5", + "uuid 1.26.0", ] [[package]] @@ -527,9 +533,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -585,7 +591,7 @@ checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" name = "collapseloader" version = "1.2.2" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "chrono", "colored", "discord-rich-presence", @@ -618,9 +624,9 @@ dependencies = [ "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-single-instance", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", - "uuid 1.23.5", + "uuid 1.26.0", "windows 0.62.2", "winreg 0.56.0", "zbus", @@ -633,7 +639,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -729,7 +735,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-graphics-types", "foreign-types", @@ -742,7 +748,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "libc", ] @@ -773,27 +779,27 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -840,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -888,7 +894,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -899,14 +905,14 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "dbus" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", @@ -946,7 +952,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -965,7 +971,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", "crypto-common 0.2.2", "ctutils", @@ -990,7 +996,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1004,7 +1010,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_repr", - "thiserror 2.0.18", + "thiserror 2.0.20", "uuid 0.8.2", ] @@ -1014,7 +1020,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -1022,13 +1028,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -1051,7 +1057,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1137,20 +1143,20 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "embed-resource" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "vswhom", "winreg 0.55.0", ] @@ -1194,7 +1200,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1227,16 +1233,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1253,9 +1258,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -1284,12 +1289,12 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", "zlib-rs", ] @@ -1317,13 +1322,13 @@ dependencies = [ [[package]] name = "foreign-types-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -1355,9 +1360,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1365,15 +1370,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1382,9 +1387,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -1401,32 +1406,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -1624,7 +1629,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "futures-channel", "futures-core", "futures-executor", @@ -1652,7 +1657,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1667,9 +1672,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gobject-sys" @@ -1731,7 +1736,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1816,9 +1821,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1826,9 +1831,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1836,9 +1841,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1855,18 +1860,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2109,9 +2114,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is-docker" @@ -2198,7 +2203,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.20", "walkdir", "windows-link 0.2.1", ] @@ -2213,7 +2218,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2241,7 +2246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2256,13 +2261,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2295,7 +2299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "160f2eade097f30263b548aae5deb12ad349c909baa710fa24b92c9090b2e006" dependencies = [ "scopeguard", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2304,7 +2308,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "serde", "unicode-segmentation", ] @@ -2341,9 +2345,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdbus-sys" @@ -2366,9 +2370,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -2408,23 +2412,25 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.16.3" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9ceaec84b54518262de7cf06b8b43e83c808349960f1610b21b0bfc9640f20" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" dependencies = [ "sha2 0.11.0", ] [[package]] name = "mac-notification-sys" -version = "0.6.12" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" dependencies = [ "cc", + "log", "objc2", "objc2-foundation", "time", + "uuid 1.26.0", ] [[package]] @@ -2485,11 +2491,21 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -2498,9 +2514,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -2513,8 +2529,8 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", + "thiserror 2.0.20", + "windows-sys 0.60.2", ] [[package]] @@ -2533,7 +2549,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "raw-window-handle", - "thiserror 2.0.18", + "thiserror 2.0.20", "versions", "wfd", "which", @@ -2546,7 +2562,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2590,9 +2606,9 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.17.0" +version = "4.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" dependencies = [ "futures-lite", "log", @@ -2642,10 +2658,10 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2664,7 +2680,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -2685,7 +2701,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -2696,7 +2712,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -2707,7 +2723,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -2718,7 +2734,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -2751,7 +2767,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2763,7 +2779,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2791,7 +2807,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -2814,7 +2830,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -2836,7 +2852,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2848,7 +2864,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-cloud-kit", @@ -2879,7 +2895,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-app-kit", @@ -2895,9 +2911,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.4.0" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +checksum = "ade3be4664bc1ef537ce133015f04c176b737815c2ba9fd60edf212d6e90dd55" dependencies = [ "dunce", "is-wsl", @@ -3064,7 +3080,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3101,13 +3117,13 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml 0.39.4", + "quick-xml", "serde", "time", ] @@ -3122,7 +3138,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -3131,11 +3147,11 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -3214,7 +3230,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -3243,27 +3259,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -3282,7 +3289,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -3305,7 +3312,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -3322,14 +3329,14 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3348,9 +3355,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -3413,7 +3420,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3424,34 +3431,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3461,9 +3468,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3590,18 +3597,18 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -3625,9 +3632,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -3651,7 +3658,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3674,9 +3681,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "same-file" @@ -3708,7 +3715,7 @@ dependencies = [ "serde", "serde_json", "url", - "uuid 1.23.5", + "uuid 1.26.0", ] [[package]] @@ -3725,9 +3732,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -3744,7 +3751,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3759,7 +3766,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3782,7 +3789,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cssparser", "derive_more", "log", @@ -3807,9 +3814,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3829,22 +3836,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -3855,14 +3862,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3873,13 +3880,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -3902,9 +3909,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", "bs58", @@ -3913,7 +3920,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.2.1", + "schemars 1.2.2", "serde_core", "serde_json", "serde_with_macros", @@ -3922,14 +3929,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3951,7 +3958,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4014,15 +4021,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -4059,7 +4066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4175,9 +4182,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -4201,7 +4219,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4225,7 +4243,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4259,7 +4277,7 @@ version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "core-foundation 0.10.1", "core-graphics", @@ -4295,13 +4313,13 @@ dependencies = [ [[package]] name = "tao-macros" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4351,7 +4369,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tray-icon", "url", @@ -4400,12 +4418,12 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.118", + "syn 2.0.119", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "url", - "uuid 1.23.5", + "uuid 1.26.0", "walkdir", ] @@ -4418,16 +4436,16 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -4453,7 +4471,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "tracing", "url", "windows-registry 0.5.3", @@ -4462,9 +4480,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" dependencies = [ "log", "raw-window-handle", @@ -4474,7 +4492,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", ] @@ -4497,8 +4515,8 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", "url", ] @@ -4510,13 +4528,13 @@ checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" dependencies = [ "log", "notify-rust", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_json", "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", "time", "url", ] @@ -4537,7 +4555,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", "windows 0.61.3", "zbus", @@ -4553,7 +4571,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin-deep-link", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "tracing", "windows-sys 0.60.2", @@ -4578,7 +4596,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", "webkit2gtk", "webview2-com", @@ -4641,11 +4659,11 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", "url", "urlpattern", - "uuid 1.23.5", + "uuid 1.26.0", "walkdir", ] @@ -4657,17 +4675,16 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] name = "tauri-winrt-notification" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" dependencies = [ - "quick-xml 0.37.5", - "thiserror 2.0.18", + "thiserror 2.0.20", "windows 0.61.3", "windows-version", ] @@ -4679,20 +4696,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "tendril" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ "new_debug_unreachable", - "utf-8", ] [[package]] @@ -4706,11 +4722,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -4721,25 +4737,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "js-sys", @@ -4758,9 +4774,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4787,9 +4803,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4802,9 +4818,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -4819,13 +4835,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -4840,13 +4856,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -4880,9 +4897,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -4890,7 +4907,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -4946,30 +4963,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -4992,7 +5009,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -5035,7 +5052,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5049,9 +5066,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" dependencies = [ "crossbeam-channel", "dirs", @@ -5065,8 +5082,8 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", + "thiserror 2.0.20", + "windows-sys 0.60.2", ] [[package]] @@ -5101,7 +5118,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5194,12 +5211,6 @@ dependencies = [ "url", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5217,9 +5228,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.5" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -5305,9 +5316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -5318,9 +5329,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5328,9 +5339,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5338,22 +5349,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -5373,9 +5384,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -5393,9 +5404,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf", "phf_codegen", @@ -5449,9 +5460,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -5478,7 +5489,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5487,7 +5498,7 @@ version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.20", "windows 0.61.3", "windows-core 0.61.2", ] @@ -5536,7 +5547,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5659,7 +5670,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5670,7 +5681,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6038,9 +6049,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -6062,7 +6073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6116,7 +6127,7 @@ dependencies = [ "sha2 0.10.9", "soup3", "tao-macros", - "thiserror 2.0.18", + "thiserror 2.0.20", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6167,15 +6178,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zbus" -version = "5.17.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" dependencies = [ "async-broadcast", "async-executor", @@ -6199,9 +6210,9 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "uuid 1.23.5", + "uuid 1.26.0", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -6209,14 +6220,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.17.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", "zbus_names", "zvariant", "zvariant_utils", @@ -6224,33 +6235,42 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.3" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant", ] +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6270,7 +6290,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -6310,7 +6330,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6342,15 +6362,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" @@ -6394,40 +6414,41 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.13.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.3", + "winnow 1.0.4", + "zcheapstr", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.13.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.5.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.118", - "winnow 1.0.3", + "syn 3.0.3", + "winnow 1.0.4", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 91234249..427636d0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "collapseloader" -version = "1.2.2" +version = "1.3.0" description = "CollapseLoader" authors = ["dest4590"] edition = "2021" @@ -19,8 +19,8 @@ tauri-plugin-opener = "2.5.4" tauri-plugin-notification = "2.3.3" tauri-plugin-deep-link = "2.4.9" tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" colored = "3.1.1" rand = "0.10.2" reqwest = { version = "0.13.4", features = [ @@ -31,26 +31,26 @@ reqwest = { version = "0.13.4", features = [ ] } semver = "1.0.28" zip = "8.6.0" -tokio = { version = "1.52.3", features = ["macros", "process"] } +tokio = { version = "1.53.1", features = ["macros", "process"] } opener = "0.8.5" -uuid = { version = "1.23.5", features = ["v4"] } +uuid = { version = "1.26.0", features = ["v4"] } chrono = { version = "0.4.45", features = ["serde"] } paste = "1.0.15" -open = "5.4.0" +open = "5.4.2" md5 = "0.8.1" discord-rich-presence = "1.1.0" -futures-util = "0.3.32" -base64 = "0.22.1" -tauri-plugin-dialog = "2.7.1" +futures-util = "0.3.34" +base64 = "0.23.1" +tauri-plugin-dialog = "2.7.2" dotenvy = "0.15.7" tauri-plugin-fs = "2.5.1" -thiserror = "2.0.18" +thiserror = "2.0.20" native-dialog = "0.9.7" sysinfo = "0.39.6" socket2 = "0.6.5" sha2 = "0.11.0" -regex = "1.13.0" -flate2 = "1.1.9" +regex = "1.13.1" +flate2 = "1.1.10" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.4" @@ -61,7 +61,7 @@ objc2-app-kit = { version = "0.3.2", features = [ objc2-foundation = { version = "0.3.2", features = ["NSString"] } [target.'cfg(target_os = "linux")'.dependencies] -zbus = { version = "5.17.0", default-features = false, features = ["tokio"] } +zbus = { version = "5.19.0", default-features = false, features = ["tokio"] } [target.'cfg(windows)'.dependencies] junction = "2.0.0" diff --git a/src-tauri/src/commands/clients.rs b/src-tauri/src/commands/clients.rs deleted file mode 100644 index b3612166..00000000 --- a/src-tauri/src/commands/clients.rs +++ /dev/null @@ -1,1350 +0,0 @@ -use crate::AppState; -use core::clients::{ - client::{Client, CLIENT_LOGS}, - manager::ClientManager, -}; -use tauri::{AppHandle, State}; - -use crate::commands::utils::refresh_tray_menu; -use crate::core::{ - clients::client::ClientType, - network::servers::{ServerConnectivityStatus, SERVERS}, - utils::helpers::emit_to_main_window, -}; -use crate::core::{ - clients::custom_clients::CustomClient, storage::custom_clients::CustomClientUpdate, - utils::globals::SKIP_AGENT_OVERLAY_VERIFICATION, -}; -use crate::core::{ - clients::{client::LaunchOptions, internal::agent_overlay::AgentOverlayManager}, - storage::common::JsonStorage, -}; -use crate::core::{ - storage::settings::SETTINGS, - utils::{discord_rpc, hashing::calculate_md5_hash, helpers::hide_main_window, logging}, -}; -use crate::{ - core::{self, storage::data::DATA}, - log_debug, log_error, log_info, log_warn, -}; - -use serde::Serialize; -use sysinfo::{MemoryRefreshKind, Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; - -use std::fs::File; -use std::io::Read; -use std::path::PathBuf; -use zip::ZipArchive; - -pub(crate) fn get_client_by_id( - id: u32, - manager: &std::sync::Arc>, -) -> Result { - manager - .lock() - .map_err(|_| "Failed to acquire lock on client manager".to_string())? - .clients - .iter() - .find(|c| c.id == id) - .cloned() - .ok_or_else(|| format!("Client with ID {id} not found")) -} - -#[derive(Debug, Clone, Serialize)] -pub struct ClientRamUsage { - pub client_id: u32, - pub is_running: bool, - pub process_count: usize, - pub pids: Vec, - pub total_memory_bytes: u64, - pub total_memory_mib: f64, - pub system_total_memory_bytes: u64, - pub system_total_memory_mib: f64, - pub system_memory_percent: f64, -} - -fn collect_client_ram_usage(client: &Client) -> ClientRamUsage { - let pids = crate::core::utils::process::find_processes_by_filename(&client.filename) - .into_iter() - .filter_map(|pid| pid.parse::().ok()) - .collect::>(); - - let mut system = System::new_with_specifics( - RefreshKind::nothing() - .with_memory(MemoryRefreshKind::nothing().with_ram()) - .with_processes(ProcessRefreshKind::nothing().with_memory()), - ); - system.refresh_memory(); - let _ = system.refresh_processes(ProcessesToUpdate::All, true); - - let total_memory_bytes = pids - .iter() - .filter_map(|pid| system.process(Pid::from_u32(*pid))) - .map(|process| process.memory()) - .sum::(); - - let system_total_memory_bytes = system.total_memory(); - let total_memory_mib = total_memory_bytes as f64 / 1024.0 / 1024.0; - let system_total_memory_mib = system_total_memory_bytes as f64 / 1024.0 / 1024.0; - let system_memory_percent = if system_total_memory_bytes > 0 { - (total_memory_bytes as f64 / system_total_memory_bytes as f64) * 100.0 - } else { - 0.0 - }; - - ClientRamUsage { - client_id: client.id, - is_running: !pids.is_empty(), - process_count: pids.len(), - pids, - total_memory_bytes, - total_memory_mib, - system_total_memory_bytes, - system_total_memory_mib, - system_memory_percent, - } -} - -fn with_client_manager( - state: &State<'_, AppState>, - operation: impl FnOnce(&mut ClientManager) -> Result, -) -> Result { - let mut manager = state - .clients - .manager - .lock() - .map_err(|_| "Failed to acquire lock on client manager".to_string())?; - - operation(&mut manager) -} - -fn with_custom_client_manager( - state: &State<'_, AppState>, - operation: impl FnOnce( - &mut crate::core::storage::custom_clients::CustomClientManager, - ) -> Result, -) -> Result { - let mut manager = state.custom_clients.lock(); - operation(&mut manager) -} - -fn refresh_tray_menu_after_client_change( - state: State<'_, AppState>, - result: Result<(), String>, -) -> Result<(), String> { - if result.is_ok() { - refresh_tray_menu(state); - } - result -} - -#[tauri::command] -pub fn get_app_logs() -> Vec { - logging::APP_LOGS - .lock() - .map(|logs| logs.clone()) - .unwrap_or_default() - .into() -} - -#[tauri::command] -pub async fn initialize_api(state: State<'_, AppState>) -> Result<(), String> { - let clients = ClientManager::fetch_clients().await.map_err(|e| { - log_error!("Failed to fetch clients: {}", e); - e.to_string() - })?; - - if clients.is_empty() { - log_warn!("Fetched client list is empty - this may indicate an API or network issue"); - return Err("Fetched client list is empty".to_string()); - } - - { - let mut manager = state - .clients - .manager - .lock() - .map_err(|_| "Failed to lock state".to_string())?; - - manager.clients = clients; - } - - let sync_enabled = crate::core::storage::settings::SETTINGS - .lock() - .map(|s| s.sync_client_settings.value) - .unwrap_or(false); - - if sync_enabled { - if let Err(e) = crate::core::storage::data::DATA - .sync_all_installed_clients() - .await - { - log_warn!("Failed to sync all clients on startup: {}", e); - } - } - - Ok(()) -} - -#[tauri::command] -pub fn initialize_rpc() -> Result<(), String> { - log_info!("Initializing Discord RPC"); - if let Err(e) = discord_rpc::initialize() { - log_error!("Failed to initialize Discord RPC: {}", e); - } - Ok(()) -} - -#[tauri::command] -pub async fn get_server_connectivity_status() -> ServerConnectivityStatus { - let servers = &SERVERS; - servers.wait_for_initial_check().await; - servers.connectivity_status.lock().unwrap().clone() -} - -#[tauri::command] -pub fn get_clients(state: State<'_, AppState>) -> Vec { - state - .clients - .manager - .lock() - .ok() - .map(|manager| manager.clients.clone()) - .unwrap_or_default() -} - -async fn verify_client_hash( - client: &Client, - jar_path: &std::path::Path, - app_handle: &AppHandle, - state: &State<'_, AppState>, -) -> Result<(), String> { - let hash_verify_enabled = SETTINGS.lock().map(|s| s.hash_verify.value).unwrap_or(true); - - if !hash_verify_enabled { - log_debug!( - "Hash verification disabled, skipping verification for client {}", - client.name - ); - return Ok(()); - } - - log_info!("Hash verification is enabled for client '{}'", client.name); - emit_to_main_window( - app_handle, - "client-hash-verification-start", - &serde_json::json!({ "id": client.id, "name": client.name }), - ); - - log_info!( - "Verifying MD5 hash for client {} before launch", - client.name - ); - - let current_hash = calculate_md5_hash(jar_path)?; - if current_hash == client.md5_hash { - log_info!( - "MD5 hash verification successful for client {}", - client.name - ); - emit_to_main_window( - app_handle, - "client-hash-verification-done", - &serde_json::json!({ "id": client.id, "name": client.name }), - ); - return Ok(()); - } - - log_warn!( - "Hash mismatch for client {}. Expected: {}, Got: {}. Redownloading...", - client.name, - client.md5_hash, - current_hash - ); - - emit_to_main_window( - app_handle, - "client-hash-verification-failed", - &serde_json::json!({ - "id": client.id, - "name": client.name, - "expected_hash": client.md5_hash, - "actual_hash": current_hash - }), - ); - - let _ = std::fs::remove_file(jar_path); - update_client_installed_status(client.id, false, state.clone())?; - - log_info!("Redownloading client: {} (ID: {})", client.name, client.id); - client - .download(&state.clients.manager) - .await - .map_err(|e| { - if e.contains("Hash verification failed") { - format!("Hash verification failed for {}: The downloaded file is corrupted. Please try downloading again.", client.name) - } else { - format!("Failed to redownload client {}: {}", client.name, e) - } - })?; - - emit_to_main_window( - app_handle, - "client-redownload-complete", - &serde_json::json!({ "id": client.id, "name": client.name }), - ); - - log_info!( - "Client {} redownloaded and verified successfully", - client.name - ); - Ok(()) -} - -async fn ensure_agent_overlay() -> Result<(), String> { - match AgentOverlayManager::verify_agent_overlay_files().await { - Ok(true) => Ok(()), - Ok(false) => { - if !*SKIP_AGENT_OVERLAY_VERIFICATION { - log_warn!("Agent/overlay files verification failed, attempting to download..."); - AgentOverlayManager::download_agent_overlay_files() - .await - .map_err(|e| format!("Failed to download required agent/overlay files: {e}")) - } else { - log_debug!("Agent/overlay files verification failed, but skipping download due to SKIP_AGENT_OVERLAY_VERIFICATION being enabled."); - Ok(()) - } - } - Err(e) => { - log_error!("Error verifying agent/overlay files: {}", e); - Ok(()) - } - } -} - -#[tauri::command] -pub async fn launch_client( - id: u32, - user_token: String, - app_handle: AppHandle, - state: State<'_, AppState>, -) -> Result<(), String> { - let client = get_client_by_id(id, &state.clients.manager)?; - let (_, jar_path) = client.get_launch_paths()?; - - if !jar_path.exists() { - log_warn!( - "Launch failed: Client '{}' is not installed at path: {}", - client.name, - jar_path.display() - ); - return Err(format!( - "Client {} is not installed. Please download it first.", - client.name - )); - } - - log_info!( - "Launching '{}' (ID: {}, Play Count: {})...", - client.name, - id, - client.launches - ); - - verify_client_hash(&client, &jar_path, &app_handle, &state).await?; - ensure_agent_overlay().await?; - - let sync_enabled = SETTINGS - .lock() - .map(|s| s.sync_client_settings.value) - .unwrap_or(false); - - if sync_enabled { - let client_base = std::path::Path::new(&client.filename) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or(&client.name) - .to_string(); - if let Err(e) = crate::core::storage::data::DATA - .ensure_client_synced(&client_base) - .await - { - if !e.contains("5") { - log_warn!("Failed to sync client {} before launch: {}", client_base, e); - } - } - } - - let minimize_on_launch = SETTINGS - .lock() - .map(|s| s.minimize_to_tray_on_launch.value) - .unwrap_or(false); - - if minimize_on_launch { - hide_main_window(&app_handle); - } - - let options = LaunchOptions::new(app_handle.clone(), user_token, false); - client.run(options, state.clients.manager.clone()).await -} - -#[tauri::command] -pub async fn get_running_client_ids(state: State<'_, AppState>) -> Result, String> { - let manager = state.clients.manager.clone(); - let handle = tokio::task::spawn_blocking(move || { - Client::get_running_clients(&manager) - .iter() - .map(|client| client.id) - .collect() - }); - - handle - .await - .map_err(|e| format!("Failed to get running client IDs: {}", e)) -} - -#[tauri::command] -pub async fn stop_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { - log_info!("Attempting to stop client with ID: {}", id); - let client = get_client_by_id(id, &state.clients.manager)?; - log_debug!("Found client '{}' to stop", client.name); - - let client_clone = client.clone(); - let handle = tokio::task::spawn_blocking(move || client_clone.stop()); - - handle - .await - .map_err(|e| format!("Stop client task error: {e}"))? -} - -#[tauri::command] -pub fn get_client_logs(id: u32) -> Vec { - CLIENT_LOGS - .lock() - .ok() - .and_then(|logs| logs.get(&id).cloned()) - .unwrap_or_default() -} - -#[tauri::command] -pub async fn download_client_only( - id: u32, - app_handle: AppHandle, - state: State<'_, AppState>, -) -> Result<(), String> { - let client = get_client_by_id(id, &state.clients.manager)?; - let state_clone = state.clients.manager.clone(); - let client_clone = client.clone(); - let client_download = async move { - client_clone.download(&state_clone).await.map_err(|e| { - if e.contains("Hash verification failed") { - let _ = update_client_installed_status(id, false, state.clone()); - format!( - "Hash verification failed for {}: The downloaded file is corrupted. Please try downloading again.", - client_clone.name - ) - } else { - e - } - }) - }; - - let requirements_download = client.download_requirements(&app_handle); - - tokio::try_join!(client_download, requirements_download)?; - - let sync_enabled = crate::core::storage::settings::SETTINGS - .lock() - .map(|s| s.sync_client_settings.value) - .unwrap_or(false); - - if sync_enabled { - let client_base = crate::core::storage::data::Data::get_filename(&client.filename); - if let Err(e) = crate::core::storage::data::DATA - .ensure_client_synced(&client_base) - .await - { - if e.contains("5") { - log_warn!( - "Failed to sync client {} after download: {}", - client_base, - e - ); - } - } - } - - Ok(()) -} - -#[tauri::command] -pub async fn reinstall_client( - id: u32, - app_handle: AppHandle, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!("Starting reinstall for client ID: {}", id); - let client = get_client_by_id(id, &state.clients.manager)?; - log_debug!("Found client '{}' for reinstall", client.name); - - let client_clone = client.clone(); - let manager = state.clients.manager.clone(); - let handle = tokio::task::spawn_blocking(move || -> Result<(), String> { - log_info!("Removing existing installation for '{}'", client_clone.name); - client_clone.remove_installation(&manager)?; - log_info!( - "Successfully removed existing installation for '{}'", - client_clone.name - ); - Ok(()) - }); - - handle - .await - .map_err(|e| format!("Reinstall task error: {e}"))??; - - update_client_installed_status(id, false, state.clone())?; - log_debug!( - "Updated installed status to false for client '{}'", - client.name - ); - - let download_result = client - .download(&state.clients.manager) - .await - .map_err(|e| { - if e.contains("Hash verification failed") { - format!( - "Hash verification failed for {}: The downloaded file is corrupted. Please try again.", - client.name - ) - } else { - log_error!("Client download failed during reinstall: {}", e); - e - } - }); - - if let Err(e) = download_result.as_ref() { - log_error!( - "Aborting reinstall for '{}' due to download failure: {}", - client.name, - e - ); - return Err(e.clone()); - } - - let result = client.download_requirements(&app_handle).await; - - if download_result.is_ok() && result.is_ok() { - log_info!( - "Client '{}' successfully installed with all requirements", - client.name - ); - } - - result -} - -#[tauri::command] -pub fn open_client_folder(id: u32, state: State<'_, AppState>) -> Result<(), String> { - log_info!("Attempting to open folder for client ID: {}", id); - let client = get_client_by_id(id, &state.clients.manager)?; - log_debug!("Found client '{}' to open folder", client.name); - - let client_dir_relative = DATA.get_as_folder(&client.filename); - - if !client_dir_relative.exists() { - log_warn!( - "Cannot open folder for client '{}', it does not exist at path: {}", - client.name, - client_dir_relative.display() - ); - return Err("Client folder does not exist".to_string()); - } - - let client_dir_absolute = client_dir_relative - .canonicalize() - .map_err(|e| format!("Failed to get absolute path: {e}"))?; - - log_debug!( - "Opening client folder at: {}", - client_dir_absolute.display() - ); - opener::open(&client_dir_absolute).map_err(|e| { - log_error!( - "Failed to open client folder at {}: {}", - client_dir_absolute.display(), - e - ); - format!( - "Failed to open client folder: {} at path {}", - e, - client_dir_absolute.display() - ) - }) -} - -#[tauri::command] -pub fn get_latest_client_logs(id: u32) -> Result { - log_debug!("Fetching latest logs for client ID: {}", id); - CLIENT_LOGS - .lock() - .map_err(|_| "Failed to acquire lock on client logs".to_string())? - .get(&id) - .map(|logs| logs.join("\n")) - .ok_or_else(|| "No logs found for this client".to_string()) -} - -#[tauri::command] -pub async fn get_client_ram_usage( - id: u32, - state: State<'_, AppState>, -) -> Result { - let client = get_client_by_id(id, &state.clients.manager)?; - - let client_clone = client.clone(); - tokio::task::spawn_blocking(move || collect_client_ram_usage(&client_clone)) - .await - .map_err(|e| format!("Failed to get client RAM usage: {e}")) -} - -#[tauri::command] -pub fn update_client_installed_status( - id: u32, - installed: bool, - state: State<'_, AppState>, -) -> Result<(), String> { - let result = with_client_manager(&state, |manager| { - if let Some(client) = manager.clients.iter_mut().find(|c| c.id == id) { - client.meta.installed = installed; - Ok(()) - } else { - Err("Client not found".to_string()) - } - }); - - refresh_tray_menu_after_client_change(state, result) -} - -#[tauri::command] -pub async fn delete_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { - let client = get_client_by_id(id, &state.clients.manager)?; - let manager = state.clients.manager.clone(); - let handle = tokio::task::spawn_blocking(move || client.remove_installation(&manager)); - - match handle.await { - Ok(result) => { - if result.is_ok() { - update_client_installed_status(id, false, state.clone())?; - } - result - } - Err(e) => Err(format!("Delete task error: {e}")), - } -} - -#[tauri::command] -pub fn increment_client_counter( - id: u32, - counter_type: String, - state: State<'_, AppState>, -) -> Result<(), String> { - let result = with_client_manager(&state, |manager| { - if let Some(client) = manager.clients.iter_mut().find(|c| c.id == id) { - match counter_type.as_str() { - "download" => { - client.downloads += 1; - } - "launch" => { - client.launches += 1; - } - _ => { - return Err(format!("Invalid counter type: {counter_type}")); - } - } - Ok(()) - } else { - Err("Client not found".to_string()) - } - }); - - refresh_tray_menu_after_client_change(state, result) -} - -#[tauri::command] -pub fn detect_main_class(file_path: String) -> Result { - log_info!("Attempting to detect main class from: {}", file_path); - - let file = File::open(&file_path).map_err(|e| format!("Failed to open file: {}", e))?; - let mut archive = ZipArchive::new(file).map_err(|e| format!("Failed to read jar: {}", e))?; - - let mut manifest = archive - .by_name("META-INF/MANIFEST.MF") - .map_err(|_| "MANIFEST.MF not found in jar".to_string())?; - - let mut content = String::new(); - manifest - .read_to_string(&mut content) - .map_err(|e| format!("Failed to read manifest: {}", e))?; - - for line in content.lines() { - if line.starts_with("Main-Class:") { - let main_class = line.replace("Main-Class:", "").trim().to_string(); - log_info!("Detected main class: {}", main_class); - return Ok(main_class); - } - } - - log_warn!("Main-Class attribute not found in manifest"); - Err("Main-Class attribute not found in manifest".to_string()) -} - -#[tauri::command] -pub fn get_custom_clients(state: State<'_, AppState>) -> Vec { - state.custom_clients.lock().clients.clone() -} - -#[tauri::command] -#[allow(clippy::too_many_arguments)] -pub fn add_custom_client( - name: String, - version: String, - filename: String, - file_path: String, - main_class: String, - java_path: Option, - java_args: Option, - client_type: ClientType, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!("Adding new custom client: '{}'", name); - let path_buf = PathBuf::from(file_path); - let mut custom_client = CustomClient::new(0, name, version, filename, path_buf, main_class); - custom_client.java_path = java_path; - custom_client.java_args = java_args; - custom_client.client_type = client_type; - - log_debug!("New custom client details: {:?}", custom_client); - with_custom_client_manager(&state, |manager| manager.add_client(custom_client)) -} - -#[tauri::command] -pub fn remove_custom_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { - log_info!("Removing custom client with ID: {}", id); - with_custom_client_manager(&state, |manager| manager.remove_client(id)) -} - -#[tauri::command] -#[allow(clippy::too_many_arguments)] -pub fn update_custom_client( - id: u32, - name: Option, - version: Option, - main_class: Option, - java_path: Option, - java_args: Option, - client_type: Option, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!("Updating custom client with ID: {}", id); - let updates = CustomClientUpdate { - name, - version, - main_class, - java_path, - java_args, - client_type, - }; - - log_debug!("Applying updates to custom client ID {}: {:?}", id, updates); - with_custom_client_manager(&state, |manager| manager.update_client(id, updates)) -} - -#[tauri::command] -pub async fn launch_custom_client( - id: u32, - user_token: String, - app_handle: AppHandle, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!("Attempting to launch custom client with ID: {}", id); - let custom_client = with_custom_client_manager(&state, |manager| { - let client = manager - .get_client_mut(id) - .ok_or_else(|| "Custom client not found".to_string())?; - - client.launches += 1; - log_debug!( - "Incremented launch count for custom client '{}' to {}", - client.name, - client.launches - ); - let client_clone = client.clone(); - manager.save_to_disk(); - - Ok(client_clone) - })?; - - custom_client.validate_file()?; - - log_debug!("Custom client file validated for '{}'", custom_client.name); - - log_info!("Launching custom client: {}", custom_client.name); - - let client = custom_client.to_client(); - - emit_to_main_window( - &app_handle, - "custom-client-launched", - &serde_json::json!({ - "name": custom_client.name - }), - ); - - let options = LaunchOptions::new(app_handle.clone(), user_token.clone(), true); - - let minimize_on_launch = { - let settings = SETTINGS.lock().unwrap(); - settings.minimize_to_tray_on_launch.value - }; - - if minimize_on_launch { - hide_main_window(&app_handle); - } - - client.run(options, state.clients.manager.clone()).await -} - -#[tauri::command] -pub async fn get_running_custom_client_ids() -> Vec { - let handle = tokio::task::spawn_blocking(|| { - CustomClient::get_running_custom_clients() - .iter() - .map(|client| client.id) - .collect() - }); - - handle.await.unwrap_or_else(|e| { - log_error!("Failed to get running custom client IDs: {}", e); - Vec::new() - }) -} - -#[tauri::command] -pub async fn install_mod_from_url( - id: u32, - url: String, - filename: String, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!( - "Installing mod for client {}: {} from {}", - id, - filename, - url - ); - - let client = get_client_by_id(id, &state.clients.manager)?; - - let mods_folder_relative = match client.client_type { - ClientType::Fabric | ClientType::Forge | ClientType::Default => { - let (folder, _) = client.get_launch_paths().map_err(|e| e.to_string())?; - let root = DATA.root_dir.lock().unwrap().clone(); - let relative = folder - .strip_prefix(&root) - .map_err(|_| "Client folder is outside of root directory".to_string())?; - relative.join("mods") - } - }; - - let mods_folder_str = mods_folder_relative - .to_str() - .ok_or_else(|| "Invalid mods folder path".to_string())?; - - DATA.download_to_folder(&url, mods_folder_str).await?; - - log_info!( - "Successfully installed mod: {} to {}", - filename, - mods_folder_str - ); - - Ok(()) -} - -#[tauri::command] -pub async fn list_installed_mods( - id: u32, - state: State<'_, AppState>, -) -> Result, String> { - let client = get_client_by_id(id, &state.clients.manager)?; - - let mods_folder = match client.client_type { - ClientType::Fabric | ClientType::Forge | ClientType::Default => { - let (folder, _) = client.get_launch_paths().map_err(|e| e.to_string())?; - folder.join("mods") - } - }; - - if !mods_folder.exists() { - return Ok(Vec::new()); - } - - let mut mods = Vec::new(); - let mut entries = tokio::fs::read_dir(mods_folder) - .await - .map_err(|e| format!("Failed to read mods directory: {}", e))?; - - while let Some(entry) = entries.next_entry().await.map_err(|e| e.to_string())? { - let path = entry.path(); - if path.is_file() { - if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { - if filename.ends_with(".jar") { - mods.push(filename.to_string()); - } - } - } - } - - Ok(mods) -} - -#[tauri::command] -pub async fn uninstall_mod( - id: u32, - filename: String, - state: State<'_, AppState>, -) -> Result<(), String> { - let client = get_client_by_id(id, &state.clients.manager)?; - - let mods_folder = match client.client_type { - ClientType::Fabric | ClientType::Forge | ClientType::Default => { - let (folder, _) = client.get_launch_paths().map_err(|e| e.to_string())?; - folder.join("mods") - } - }; - - let target_file = mods_folder.join(filename); - - if !target_file.exists() { - return Err("Mod file does not exist".to_string()); - } - - tokio::fs::remove_file(target_file) - .await - .map_err(|e| format!("Failed to delete mod file: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub async fn stop_custom_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { - log_info!("Attempting to stop custom client with ID: {}", id); - let custom_client = { - let manager = state.custom_clients.lock(); - - manager - .get_client(id) - .cloned() - .ok_or_else(|| "Custom client not found".to_string())? - }; - log_debug!("Found custom client '{}' to stop", custom_client.name); - - let client_clone = custom_client.clone(); - let handle = tokio::task::spawn_blocking(move || client_clone.stop()); - - handle - .await - .map_err(|e| format!("Stop custom client task error: {e}"))? -} - -#[tauri::command] -pub fn open_custom_client_folder(id: u32, state: State<'_, AppState>) -> Result<(), String> { - log_info!("Attempting to open folder for custom client ID: {}", id); - let manager = state.custom_clients.lock(); - let custom_client = manager - .get_client(id) - .cloned() - .ok_or_else(|| "Custom client not found".to_string())?; - drop(manager); - - let folder = custom_client - .file_path - .parent() - .ok_or_else(|| "Cannot determine client folder".to_string())? - .to_path_buf(); - - if !folder.exists() { - return Err("Custom client folder does not exist".to_string()); - } - - let folder_absolute = folder - .canonicalize() - .map_err(|e| format!("Failed to get absolute path: {e}"))?; - - opener::open(&folder_absolute).map_err(|e| format!("Failed to open folder: {e}")) -} - -#[tauri::command] -pub async fn list_installed_mods_custom( - id: u32, - state: State<'_, AppState>, -) -> Result, String> { - let custom_client = { - let manager = state.custom_clients.lock(); - manager - .get_client(id) - .cloned() - .ok_or_else(|| "Custom client not found".to_string())? - }; - - let mods_folder = custom_client - .file_path - .parent() - .ok_or_else(|| "Cannot determine client folder".to_string())? - .join("mods"); - - if !mods_folder.exists() { - return Ok(Vec::new()); - } - - let mut mods = Vec::new(); - let mut entries = tokio::fs::read_dir(mods_folder) - .await - .map_err(|e| format!("Failed to read mods directory: {e}"))?; - - while let Some(entry) = entries.next_entry().await.map_err(|e| e.to_string())? { - let path = entry.path(); - if path.is_file() { - if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { - if filename.ends_with(".jar") { - mods.push(filename.to_string()); - } - } - } - } - - Ok(mods) -} - -#[tauri::command] -pub async fn install_mod_for_custom_client( - id: u32, - url: String, - filename: String, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!( - "Installing mod for custom client {}: {} from {}", - id, - filename, - url - ); - - let custom_client = { - let manager = state.custom_clients.lock(); - manager - .get_client(id) - .cloned() - .ok_or_else(|| "Custom client not found".to_string())? - }; - - let mods_folder = custom_client - .file_path - .parent() - .ok_or_else(|| "Cannot determine client folder".to_string())? - .join("mods"); - - tokio::fs::create_dir_all(&mods_folder) - .await - .map_err(|e| format!("Failed to create mods folder: {e}"))?; - - let dest = mods_folder.join(&filename); - - let client = reqwest::Client::new(); - let response = client - .get(&url) - .header("User-Agent", "CollapseLoader-Reborn") - .send() - .await - .map_err(|e| format!("Failed to download mod: {e}"))?; - - if !response.status().is_success() { - return Err(format!( - "Download failed with status: {}", - response.status() - )); - } - - let bytes = response - .bytes() - .await - .map_err(|e| format!("Failed to read mod response: {e}"))?; - - tokio::fs::write(&dest, &bytes) - .await - .map_err(|e| format!("Failed to write mod file: {e}"))?; - - log_info!( - "Successfully installed mod: {} ({} bytes)", - filename, - bytes.len() - ); - Ok(()) -} - -#[tauri::command] -pub async fn uninstall_mod_custom( - id: u32, - filename: String, - state: State<'_, AppState>, -) -> Result<(), String> { - let custom_client = { - let manager = state.custom_clients.lock(); - manager - .get_client(id) - .cloned() - .ok_or_else(|| "Custom client not found".to_string())? - }; - - let mods_folder = custom_client - .file_path - .parent() - .ok_or_else(|| "Cannot determine client folder".to_string())? - .join("mods"); - - let target_file = mods_folder.join(filename); - - if !target_file.exists() { - return Err("Mod file does not exist".to_string()); - } - - tokio::fs::remove_file(target_file) - .await - .map_err(|e| format!("Failed to delete mod file: {}", e))?; - - Ok(()) -} - -#[tauri::command] -pub fn create_client_shortcut( - id: u32, - custom_id: Option, - is_custom: bool, - shortcut_name: Option, - icon_path: Option, - state: State<'_, AppState>, -) -> Result<(), String> { - let (client_name, _client_path) = if is_custom { - let cid = custom_id.unwrap_or(id); - let manager = state.custom_clients.lock(); - let c = manager - .get_client(cid) - .cloned() - .ok_or_else(|| "Custom client not found".to_string())?; - drop(manager); - (c.name.clone(), c.file_path.clone()) - } else { - let c = get_client_by_id(id, &state.clients.manager)?; - let (folder, _) = c.get_launch_paths()?; - (c.name.clone(), folder) - }; - - let display_name = shortcut_name - .filter(|s| !s.trim().is_empty()) - .unwrap_or_else(|| client_name.clone()); - - let exe_path = - std::env::current_exe().map_err(|e| format!("Failed to get executable path: {e}"))?; - - log_info!( - "Creating shortcut '{}' for client '{}' (exe: {})", - display_name, - client_name, - exe_path.display() - ); - - create_shortcut_platform( - &display_name, - &exe_path, - id, - custom_id, - is_custom, - icon_path.as_deref(), - ) -} - -#[cfg(target_os = "windows")] -fn create_shortcut_platform( - display_name: &str, - exe_path: &std::path::Path, - id: u32, - custom_id: Option, - is_custom: bool, - icon_path: Option<&str>, -) -> Result<(), String> { - let client_id = if is_custom { - custom_id.unwrap_or(id) - } else { - id - }; - let args = format!("collapseloader://launch-client/{}", client_id); - - let exe_str = exe_path.to_string_lossy().into_owned(); - let icon_str = icon_path.unwrap_or(&exe_str).to_string(); - let safe_name = sanitize_filename(display_name); - - let script = format!( - r#" -$desktop = [Environment]::GetFolderPath('Desktop'); -$lnk = Join-Path $desktop '{safe_name}.lnk'; -$exe = '{exe}'; -$ico = '{ico}'; -$ws = New-Object -ComObject WScript.Shell; -$s = $ws.CreateShortcut($lnk); -$s.TargetPath = $exe; -$s.Arguments = '{link_args}'; -$s.Description = 'Launch {desc} via CollapseLoader'; -$s.IconLocation = $ico; -$s.Save(); -Write-Output $lnk -"#, - safe_name = safe_name.replace('\'', "''"), - exe = exe_str.replace('\'', "''"), - ico = icon_str.replace('\'', "''"), - link_args = args, - desc = display_name.replace('\'', "''"), - ); - - let output = std::process::Command::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", &script]) - .output() - .map_err(|e| format!("Failed to run PowerShell: {e}"))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("PowerShell shortcut creation failed: {stderr}")); - } - - let lnk_out = String::from_utf8_lossy(&output.stdout).trim().to_string(); - log_info!("Shortcut created at: {}", lnk_out); - Ok(()) -} - -#[cfg(target_os = "linux")] -fn create_shortcut_platform( - display_name: &str, - exe_path: &std::path::Path, - id: u32, - custom_id: Option, - is_custom: bool, - icon_path: Option<&str>, -) -> Result<(), String> { - let client_id = if is_custom { - custom_id.unwrap_or(id) - } else { - id - }; - let deep_link = format!("collapseloader://launch-client/{}", client_id); - - let home = std::env::var("HOME").map_err(|_| "Cannot find HOME".to_string())?; - let desktop = std::path::PathBuf::from(&home).join("Desktop"); - - let target_dir = if desktop.exists() { - desktop - } else { - std::path::PathBuf::from(&home) - }; - let desktop_file = target_dir.join(format!("{}.desktop", sanitize_filename(display_name))); - - let icon_line = if let Some(ip) = icon_path { - format!("Icon={}", ip) - } else { - format!("Icon={}", exe_path.to_string_lossy()) - }; - - let content = format!( - "[Desktop Entry]\nVersion=1.0\nType=Application\nName={name}\nExec={exe} {link}\n{icon}\nTerminal=false\nComment=Launch {name} via CollapseLoader\n", - name = display_name, - exe = exe_path.to_string_lossy(), - link = deep_link, - icon = icon_line, - ); - - std::fs::write(&desktop_file, &content) - .map_err(|e| format!("Failed to write .desktop file: {e}"))?; - - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&desktop_file, std::fs::Permissions::from_mode(0o755)) - .map_err(|e| format!("Failed to set permissions: {e}"))?; - - log_info!("Desktop shortcut created at: {}", desktop_file.display()); - Ok(()) -} - -#[cfg(target_os = "macos")] -fn create_shortcut_platform( - display_name: &str, - _exe_path: &std::path::Path, - id: u32, - custom_id: Option, - is_custom: bool, - _icon_path: Option<&str>, -) -> Result<(), String> { - let client_id = if is_custom { - custom_id.unwrap_or(id) - } else { - id - }; - let deep_link = format!("collapseloader://launch-client/{}", client_id); - - let home = std::env::var("HOME").map_err(|_| "Cannot find HOME".to_string())?; - let desktop = std::path::PathBuf::from(&home).join("Desktop"); - let target_dir = if desktop.exists() { - desktop - } else { - std::path::PathBuf::from(&home) - }; - - let app_bundle = target_dir.join(format!("{}.app", sanitize_filename(display_name))); - let contents = app_bundle.join("Contents"); - let macos_dir = contents.join("MacOS"); - - std::fs::create_dir_all(&macos_dir) - .map_err(|e| format!("Failed to create .app bundle: {e}"))?; - - let plist = format!( - "\n\ - \n\ - \n\ - CFBundleName{name}\n\ - CFBundleExecutablelaunch\n\ - CFBundleIdentifiercom.collapseloader.shortcut.{id}\n\ - \n", - name = display_name, - id = client_id, - ); - std::fs::write(contents.join("Info.plist"), plist) - .map_err(|e| format!("Failed to write Info.plist: {e}"))?; - - let script = format!("#!/bin/sh\nopen '{}'\n", deep_link); - let script_path = macos_dir.join("launch"); - std::fs::write(&script_path, script) - .map_err(|e| format!("Failed to write launcher script: {e}"))?; - - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)) - .map_err(|e| format!("Failed to set permissions: {e}"))?; - - log_info!("macOS app shortcut created at: {}", app_bundle.display()); - Ok(()) -} - -fn sanitize_filename(name: &str) -> String { - name.chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', - c => c, - }) - .collect() -} diff --git a/src-tauri/src/commands/clients/custom.rs b/src-tauri/src/commands/clients/custom.rs new file mode 100644 index 00000000..e204f089 --- /dev/null +++ b/src-tauri/src/commands/clients/custom.rs @@ -0,0 +1,278 @@ +use super::with_custom_client_manager; +use crate::core::clients::client::{ClientType, LaunchOptions}; +use crate::core::clients::custom_clients::CustomClient; +use crate::core::storage::common::JsonStorage; +use crate::core::storage::custom_clients::CustomClientUpdate; +use crate::core::utils::helpers::{emit_to_main_window, hide_main_window}; +use crate::AppState; +use crate::{log_debug, log_error, log_info, log_warn}; +use std::path::PathBuf; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub fn get_custom_clients(state: State<'_, AppState>) -> Vec { + state.custom_clients.lock().clients.clone() +} + +#[tauri::command] +pub fn detect_custom_client(file_path: String) -> Result { + log_info!("Detecting custom client type for '{}'", file_path); + let path = std::path::PathBuf::from(&file_path); + let detected = crate::core::clients::client_detect::detect_client_type(&path) + .ok_or_else(|| { + format!( + "Could not detect client type for {}. Please configure manually.", + file_path + ) + })?; + + let client_type_str = match detected.client_type { + ClientType::Default => "default", + ClientType::Fabric => "fabric", + ClientType::Forge => "forge", + }; + + Ok(DetectResult { + main_class: detected.main_class, + client_type: client_type_str.to_string(), + confidence: detected.confidence, + reason: detected.reason, + }) +} + +#[derive(serde::Serialize)] +pub struct DetectResult { + pub main_class: String, + pub client_type: String, + pub confidence: u8, + pub reason: String, +} + +#[tauri::command] +#[allow(clippy::too_many_arguments)] +pub async fn add_custom_client( + name: String, + version: String, + filename: String, + file_path: String, + main_class: String, + java_path: Option, + java_args: Option, + libraries_path: Option, + natives_path: Option, + client_type: ClientType, + viaversion: Option, + java_version: Option, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!("Adding new custom client: '{}'", name); + let path_buf = PathBuf::from(&file_path); + let mut resolved_main_class = main_class.clone(); + let mut resolved_client_type = client_type.clone(); + + if resolved_main_class.trim().is_empty() || resolved_client_type == ClientType::Default + { + if let Some(detected) = + crate::core::clients::client_detect::detect_client_type(&path_buf) + { + log_info!( + "Auto-detected client: main_class={}, client_type={:?}, confidence={}", + detected.main_class, + detected.client_type, + detected.confidence + ); + if resolved_main_class.trim().is_empty() { + resolved_main_class = detected.main_class; + } + if resolved_client_type == ClientType::Default { + resolved_client_type = detected.client_type; + } + } + } + + let mut custom_client = CustomClient::new( + 0, + name, + version, + filename, + path_buf, + resolved_main_class, + ); + custom_client.java_path = java_path; + custom_client.java_args = java_args; + custom_client.libraries_path = libraries_path; + custom_client.natives_path = natives_path; + custom_client.client_type = resolved_client_type; + custom_client.viaversion = viaversion.filter(|v| !v.is_empty()); + custom_client.java_version = java_version.filter(|v| !v.is_empty()); + + log_debug!("New custom client details: {:?}", custom_client); + let sync_needed = + with_custom_client_manager(&state, |manager| manager.add_client(custom_client))?; + + if let Some(client_base) = sync_needed { + if let Err(e) = crate::core::storage::data::DATA + .ensure_client_synced(&client_base) + .await + { + log_warn!("Failed to ensure client sync for custom client: {}", e); + } + } + + Ok(()) +} + +#[tauri::command] +pub fn remove_custom_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { + log_info!("Removing custom client with ID: {}", id); + with_custom_client_manager(&state, |manager| manager.remove_client(id)) +} + +#[tauri::command] +#[allow(clippy::too_many_arguments)] +pub fn update_custom_client( + id: u32, + name: Option, + version: Option, + main_class: Option, + java_path: Option, + java_args: Option, + libraries_path: Option, + natives_path: Option, + client_type: Option, + viaversion: Option, + java_version: Option, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!("Updating custom client with ID: {}", id); + let updates = CustomClientUpdate { + name, + version, + main_class, + java_path, + java_args, + libraries_path, + natives_path, + client_type, + viaversion, + java_version, + }; + + log_debug!("Applying updates to custom client ID {}: {:?}", id, updates); + with_custom_client_manager(&state, |manager| manager.update_client(id, updates)) +} + +#[tauri::command] +pub async fn launch_custom_client( + id: u32, + user_token: String, + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!("Attempting to launch custom client with ID: {}", id); + let custom_client = with_custom_client_manager(&state, |manager| { + let client = manager + .get_client_mut(id) + .ok_or_else(|| "Custom client not found".to_string())?; + + client.launches += 1; + log_debug!( + "Incremented launch count for custom client '{}' to {}", + client.name, + client.launches + ); + let client_clone = client.clone(); + manager.save_to_disk(); + + Ok(client_clone) + })?; + + custom_client.validate_file()?; + + log_debug!("Custom client file validated for '{}'", custom_client.name); + + log_info!("Launching custom client: {}", custom_client.name); + + let client = custom_client.to_client(); + + emit_to_main_window( + &app_handle, + "custom-client-launched", + &serde_json::json!({ + "name": custom_client.name + }), + ); + + let options = LaunchOptions::new(app_handle.clone(), user_token.clone(), true); + + let minimize_on_launch = state.settings().minimize_to_tray_on_launch.value; + + if minimize_on_launch { + hide_main_window(&app_handle); + } + + client.run(options, state.clients.manager.clone()).await +} + +#[tauri::command] +pub async fn get_running_custom_client_ids() -> Vec { + let handle = tokio::task::spawn_blocking(|| { + CustomClient::get_running_custom_clients() + .iter() + .map(|client| client.id) + .collect() + }); + + handle.await.unwrap_or_else(|e| { + log_error!("Failed to get running custom client IDs: {}", e); + Vec::new() + }) +} + +#[tauri::command] +pub async fn stop_custom_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { + log_info!("Attempting to stop custom client with ID: {}", id); + let custom_client = { + let manager = state.custom_clients.lock(); + + manager + .get_client(id) + .cloned() + .ok_or_else(|| "Custom client not found".to_string())? + }; + log_debug!("Found custom client '{}' to stop", custom_client.name); + + let client_clone = custom_client.clone(); + let handle = tokio::task::spawn_blocking(move || client_clone.stop()); + + handle + .await + .map_err(|e| format!("Stop custom client task error: {e}"))? +} + +#[tauri::command] +pub fn open_custom_client_folder(id: u32, state: State<'_, AppState>) -> Result<(), String> { + log_info!("Attempting to open folder for custom client ID: {}", id); + let manager = state.custom_clients.lock(); + let custom_client = manager + .get_client(id) + .cloned() + .ok_or_else(|| "Custom client not found".to_string())?; + drop(manager); + + let folder = custom_client + .file_path + .parent() + .ok_or_else(|| "Cannot determine client folder".to_string())? + .to_path_buf(); + + if !folder.exists() { + return Err("Custom client folder does not exist".to_string()); + } + + let folder_absolute = folder + .canonicalize() + .map_err(|e| format!("Failed to get absolute path: {e}"))?; + + opener::open(&folder_absolute).map_err(|e| format!("Failed to open folder: {e}")) +} diff --git a/src-tauri/src/commands/clients/general.rs b/src-tauri/src/commands/clients/general.rs new file mode 100644 index 00000000..d039c0e1 --- /dev/null +++ b/src-tauri/src/commands/clients/general.rs @@ -0,0 +1,574 @@ +use super::{get_client_by_id, refresh_tray_menu_after_client_change, with_client_manager}; +use crate::core::clients::client::{Client, LaunchOptions, CLIENT_LOGS}; +use crate::core::clients::internal::agent_overlay::AgentOverlayManager; +use crate::core::clients::internal::titlebar_branding::TitlebarBrandingManager; +use crate::core::clients::manager::ClientManager; +use crate::core::network::servers::{ServerConnectivityStatus, SERVERS}; +use crate::core::storage::data::DATA; +use crate::core::utils::{ + discord_rpc, + globals::SKIP_AGENT_OVERLAY_VERIFICATION, + hashing::calculate_md5_hash, + helpers::{emit_to_main_window, hide_main_window}, + logging, +}; +use crate::AppState; +use crate::{log_debug, log_error, log_info, log_warn}; + +use std::fs::File; +use std::io::Read; +use zip::ZipArchive; + +use tauri::{AppHandle, State}; + +#[tauri::command] +pub fn get_app_logs() -> Vec { + logging::APP_LOGS + .lock() + .map(|logs| logs.clone()) + .unwrap_or_default() + .into() +} + +#[tauri::command] +pub async fn initialize_api(state: State<'_, AppState>) -> Result<(), String> { + let clients = ClientManager::fetch_clients().await.map_err(|e| { + log_error!("Failed to fetch clients: {}", e); + e.to_string() + })?; + + if clients.is_empty() { + log_warn!("Fetched client list is empty - this may indicate an API or network issue"); + return Err("Fetched client list is empty".to_string()); + } + + { + let mut manager = state + .clients + .manager + .lock() + .map_err(|_| "Failed to lock state".to_string())?; + + manager.clients = clients; + } + + let sync_enabled = state.settings().sync_client_settings.value; + + if sync_enabled { + if let Err(e) = crate::core::storage::data::DATA + .sync_all_installed_clients() + .await + { + log_warn!("Failed to sync all clients on startup: {}", e); + } + } + + Ok(()) +} + +#[tauri::command] +pub fn initialize_rpc() -> Result<(), String> { + log_info!("Initializing Discord RPC"); + if let Err(e) = discord_rpc::initialize() { + log_error!("Failed to initialize Discord RPC: {}", e); + } + Ok(()) +} + +#[tauri::command] +pub async fn get_server_connectivity_status() -> ServerConnectivityStatus { + let servers = &SERVERS; + servers.wait_for_initial_check().await; + servers.connectivity_status.lock().unwrap().clone() +} + +#[tauri::command] +pub fn get_clients(state: State<'_, AppState>) -> Vec { + state + .clients + .manager + .lock() + .ok() + .map(|manager| manager.clients.clone()) + .unwrap_or_default() +} + +async fn verify_client_hash( + client: &Client, + jar_path: &std::path::Path, + app_handle: &AppHandle, + state: &State<'_, AppState>, +) -> Result<(), String> { + let hash_verify_enabled = state.settings().hash_verify.value; + + if !hash_verify_enabled { + log_debug!( + "Hash verification disabled, skipping verification for client {}", + client.name + ); + return Ok(()); + } + + log_info!("Hash verification is enabled for client '{}'", client.name); + emit_to_main_window( + app_handle, + "client-hash-verification-start", + &serde_json::json!({ "id": client.id, "name": client.name }), + ); + + log_info!( + "Verifying MD5 hash for client {} before launch", + client.name + ); + + let current_hash = calculate_md5_hash(jar_path)?; + if current_hash == client.md5_hash { + log_info!( + "MD5 hash verification successful for client {}", + client.name + ); + emit_to_main_window( + app_handle, + "client-hash-verification-done", + &serde_json::json!({ "id": client.id, "name": client.name }), + ); + return Ok(()); + } + + log_warn!( + "Hash mismatch for client {}. Expected: {}, Got: {}. Redownloading...", + client.name, + client.md5_hash, + current_hash + ); + + emit_to_main_window( + app_handle, + "client-hash-verification-failed", + &serde_json::json!({ + "id": client.id, + "name": client.name, + "expected_hash": client.md5_hash, + "actual_hash": current_hash + }), + ); + + let _ = std::fs::remove_file(jar_path); + update_client_installed_status(client.id, false, state.clone())?; + + log_info!("Redownloading client: {} (ID: {})", client.name, client.id); + client + .download(&state.clients.manager) + .await + .map_err(|e| { + if e.contains("Hash verification failed") { + format!("Hash verification failed for {}: The downloaded file is corrupted. Please try downloading again.", client.name) + } else { + format!("Failed to redownload client {}: {}", client.name, e) + } + })?; + + emit_to_main_window( + app_handle, + "client-redownload-complete", + &serde_json::json!({ "id": client.id, "name": client.name }), + ); + + log_info!( + "Client {} redownloaded and verified successfully", + client.name + ); + Ok(()) +} + +async fn ensure_agent_overlay() -> Result<(), String> { + match AgentOverlayManager::verify_agent_overlay_files().await { + Ok(true) => Ok(()), + Ok(false) => { + if !*SKIP_AGENT_OVERLAY_VERIFICATION { + log_warn!("Agent/overlay files verification failed, attempting to download..."); + AgentOverlayManager::download_agent_overlay_files() + .await + .map_err(|e| format!("Failed to download required agent/overlay files: {e}")) + } else { + log_debug!("Agent/overlay files verification failed, but skipping download due to SKIP_AGENT_OVERLAY_VERIFICATION being enabled."); + Ok(()) + } + } + Err(e) => { + log_error!("Error verifying agent/overlay files: {}", e); + Ok(()) + } + } +} + +async fn ensure_titlebar_branding() { + match TitlebarBrandingManager::verify_titlebar_file().await { + Ok(true) => {} + Ok(false) => { + if let Err(e) = TitlebarBrandingManager::download_titlebar_file().await { + log_warn!("Titlebar branding not available: {}", e); + } + } + Err(e) => { + log_warn!("Error verifying titlebar branding: {}", e); + } + } +} + +#[tauri::command] +pub async fn launch_client( + id: u32, + user_token: String, + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result<(), String> { + let client = get_client_by_id(id, &state.clients.manager)?; + let (_, jar_path) = client.get_launch_paths()?; + + if !jar_path.exists() { + log_warn!( + "Launch failed: Client '{}' is not installed at path: {}", + client.name, + jar_path.display() + ); + return Err(format!( + "Client {} is not installed. Please download it first.", + client.name + )); + } + + log_info!( + "Launching '{}' (ID: {}, Play Count: {})...", + client.name, + id, + client.launches + ); + + verify_client_hash(&client, &jar_path, &app_handle, &state).await?; + if !client.meta.is_custom { + ensure_agent_overlay().await?; + ensure_titlebar_branding().await; + } + + let sync_enabled = state.settings().sync_client_settings.value; + + if sync_enabled { + let client_base = std::path::Path::new(&client.filename) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(&client.name) + .to_string(); + if let Err(e) = crate::core::storage::data::DATA + .ensure_client_synced(&client_base) + .await + { + if !e.contains("5") { + log_warn!("Failed to sync client {} before launch: {}", client_base, e); + } + } + } + + let minimize_on_launch = state.settings().minimize_to_tray_on_launch.value; + + if minimize_on_launch { + hide_main_window(&app_handle); + } + + let options = LaunchOptions::new(app_handle.clone(), user_token, false); + client.run(options, state.clients.manager.clone()).await +} + +#[tauri::command] +pub async fn get_running_client_ids(state: State<'_, AppState>) -> Result, String> { + let manager = state.clients.manager.clone(); + let handle = tokio::task::spawn_blocking(move || { + Client::get_running_clients(&manager) + .iter() + .map(|client| client.id) + .collect() + }); + + handle + .await + .map_err(|e| format!("Failed to get running client IDs: {}", e)) +} + +#[tauri::command] +pub async fn stop_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { + log_info!("Attempting to stop client with ID: {}", id); + let client = get_client_by_id(id, &state.clients.manager)?; + log_debug!("Found client '{}' to stop", client.name); + + let client_clone = client.clone(); + let handle = tokio::task::spawn_blocking(move || client_clone.stop()); + + handle + .await + .map_err(|e| format!("Stop client task error: {e}"))? +} + +#[tauri::command] +pub fn get_client_logs(id: u32) -> Vec { + CLIENT_LOGS + .lock() + .ok() + .and_then(|logs| logs.get(&id).cloned()) + .unwrap_or_default() +} + +#[tauri::command] +pub async fn download_client_only( + id: u32, + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result<(), String> { + let client = get_client_by_id(id, &state.clients.manager)?; + let sync_enabled = state.settings().sync_client_settings.value; + let state_clone = state.clients.manager.clone(); + let client_clone = client.clone(); + let client_download = async move { + client_clone.download(&state_clone).await.map_err(|e| { + if e.contains("Hash verification failed") { + let _ = update_client_installed_status(id, false, state.clone()); + format!( + "Hash verification failed for {}: The downloaded file is corrupted. Please try downloading again.", + client_clone.name + ) + } else { + e + } + }) + }; + + let requirements_download = client.download_requirements(&app_handle); + + tokio::try_join!(client_download, requirements_download)?; + + if sync_enabled { + let client_base = crate::core::storage::data::Data::get_filename(&client.filename); + if let Err(e) = crate::core::storage::data::DATA + .ensure_client_synced(&client_base) + .await + { + if e.contains("5") { + log_warn!( + "Failed to sync client {} after download: {}", + client_base, + e + ); + } + } + } + + Ok(()) +} + +#[tauri::command] +pub async fn reinstall_client( + id: u32, + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!("Starting reinstall for client ID: {}", id); + let client = get_client_by_id(id, &state.clients.manager)?; + log_debug!("Found client '{}' for reinstall", client.name); + + let client_clone = client.clone(); + let manager = state.clients.manager.clone(); + let handle = tokio::task::spawn_blocking(move || -> Result<(), String> { + log_info!("Removing existing installation for '{}'", client_clone.name); + client_clone.remove_installation(&manager)?; + log_info!( + "Successfully removed existing installation for '{}'", + client_clone.name + ); + Ok(()) + }); + + handle + .await + .map_err(|e| format!("Reinstall task error: {e}"))??; + + update_client_installed_status(id, false, state.clone())?; + log_debug!( + "Updated installed status to false for client '{}'", + client.name + ); + + let download_result = client + .download(&state.clients.manager) + .await + .map_err(|e| { + if e.contains("Hash verification failed") { + format!( + "Hash verification failed for {}: The downloaded file is corrupted. Please try again.", + client.name + ) + } else { + log_error!("Client download failed during reinstall: {}", e); + e + } + }); + + if let Err(e) = download_result.as_ref() { + log_error!( + "Aborting reinstall for '{}' due to download failure: {}", + client.name, + e + ); + return Err(e.clone()); + } + + let result = client.download_requirements(&app_handle).await; + + if download_result.is_ok() && result.is_ok() { + log_info!( + "Client '{}' successfully installed with all requirements", + client.name + ); + } + + result +} + +#[tauri::command] +pub fn open_client_folder(id: u32, state: State<'_, AppState>) -> Result<(), String> { + log_info!("Attempting to open folder for client ID: {}", id); + let client = get_client_by_id(id, &state.clients.manager)?; + log_debug!("Found client '{}' to open folder", client.name); + + let client_dir_relative = DATA.get_as_folder(&client.filename); + + if !client_dir_relative.exists() { + log_warn!( + "Cannot open folder for client '{}', it does not exist at path: {}", + client.name, + client_dir_relative.display() + ); + return Err("Client folder does not exist".to_string()); + } + + let client_dir_absolute = client_dir_relative + .canonicalize() + .map_err(|e| format!("Failed to get absolute path: {e}"))?; + + log_debug!( + "Opening client folder at: {}", + client_dir_absolute.display() + ); + opener::open(&client_dir_absolute).map_err(|e| { + log_error!( + "Failed to open client folder at {}: {}", + client_dir_absolute.display(), + e + ); + format!( + "Failed to open client folder: {} at path {}", + e, + client_dir_absolute.display() + ) + }) +} + +#[tauri::command] +pub fn get_latest_client_logs(id: u32) -> Result { + log_debug!("Fetching latest logs for client ID: {}", id); + CLIENT_LOGS + .lock() + .map_err(|_| "Failed to acquire lock on client logs".to_string())? + .get(&id) + .map(|logs| logs.join("\n")) + .ok_or_else(|| "No logs found for this client".to_string()) +} + +#[tauri::command] +pub fn update_client_installed_status( + id: u32, + installed: bool, + state: State<'_, AppState>, +) -> Result<(), String> { + let result = with_client_manager(&state, |manager| { + if let Some(client) = manager.clients.iter_mut().find(|c| c.id == id) { + client.meta.installed = installed; + Ok(()) + } else { + Err("Client not found".to_string()) + } + }); + + refresh_tray_menu_after_client_change(state, result) +} + +#[tauri::command] +pub async fn delete_client(id: u32, state: State<'_, AppState>) -> Result<(), String> { + let client = get_client_by_id(id, &state.clients.manager)?; + let manager = state.clients.manager.clone(); + let handle = tokio::task::spawn_blocking(move || client.remove_installation(&manager)); + + match handle.await { + Ok(result) => { + if result.is_ok() { + update_client_installed_status(id, false, state.clone())?; + } + result + } + Err(e) => Err(format!("Delete task error: {e}")), + } +} + +#[tauri::command] +pub fn increment_client_counter( + id: u32, + counter_type: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let result = with_client_manager(&state, |manager| { + if let Some(client) = manager.clients.iter_mut().find(|c| c.id == id) { + match counter_type.as_str() { + "download" => { + client.downloads += 1; + } + "launch" => { + client.launches += 1; + } + _ => { + return Err(format!("Invalid counter type: {counter_type}")); + } + } + Ok(()) + } else { + Err("Client not found".to_string()) + } + }); + + refresh_tray_menu_after_client_change(state, result) +} + +#[tauri::command] +pub fn detect_main_class(file_path: String) -> Result { + log_info!("Attempting to detect main class from: {}", file_path); + + let file = File::open(&file_path).map_err(|e| format!("Failed to open file: {}", e))?; + let mut archive = ZipArchive::new(file).map_err(|e| format!("Failed to read jar: {}", e))?; + + let mut manifest = archive + .by_name("META-INF/MANIFEST.MF") + .map_err(|_| "MANIFEST.MF not found in jar".to_string())?; + + let mut content = String::new(); + manifest + .read_to_string(&mut content) + .map_err(|e| format!("Failed to read manifest: {}", e))?; + + for line in content.lines() { + if line.starts_with("Main-Class:") { + let main_class = line.replace("Main-Class:", "").trim().to_string(); + log_info!("Detected main class: {}", main_class); + return Ok(main_class); + } + } + + log_warn!("Main-Class attribute not found in manifest"); + Err("Main-Class attribute not found in manifest".to_string()) +} diff --git a/src-tauri/src/commands/clients/mod.rs b/src-tauri/src/commands/clients/mod.rs new file mode 100644 index 00000000..889bc168 --- /dev/null +++ b/src-tauri/src/commands/clients/mod.rs @@ -0,0 +1,66 @@ +pub mod custom; +pub mod general; +pub mod mods; +pub mod ram; +pub mod shortcuts; + +pub use custom::*; +pub use general::*; +pub use mods::*; +pub use ram::*; +pub use shortcuts::*; + +pub(crate) fn get_client_by_id( + id: u32, + manager: &std::sync::Arc>, +) -> Result { + manager + .lock() + .map_err(|_| "Failed to acquire lock on client manager".to_string())? + .clients + .iter() + .find(|c| c.id == id) + .cloned() + .ok_or_else(|| format!("Client with ID {id} not found")) +} + +pub(crate) fn with_client_manager( + state: &tauri::State<'_, crate::AppState>, + operation: impl FnOnce(&mut crate::core::clients::manager::ClientManager) -> Result, +) -> Result { + let mut manager = state + .clients + .manager + .lock() + .map_err(|_| "Failed to acquire lock on client manager".to_string())?; + operation(&mut manager) +} + +pub(crate) fn with_custom_client_manager( + state: &tauri::State<'_, crate::AppState>, + operation: impl FnOnce( + &mut crate::core::storage::custom_clients::CustomClientManager, + ) -> Result, +) -> Result { + let mut manager = state.custom_clients.lock(); + operation(&mut manager) +} + +pub(crate) fn refresh_tray_menu_after_client_change( + state: tauri::State<'_, crate::AppState>, + result: Result<(), String>, +) -> Result<(), String> { + if result.is_ok() { + crate::commands::utils::refresh_tray_menu(state); + } + result +} + +pub(crate) fn sanitize_filename(name: &str) -> String { + name.chars() + .map(|c| match c { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', + c => c, + }) + .collect() +} diff --git a/src-tauri/src/commands/clients/mods.rs b/src-tauri/src/commands/clients/mods.rs new file mode 100644 index 00000000..3dd3fd43 --- /dev/null +++ b/src-tauri/src/commands/clients/mods.rs @@ -0,0 +1,219 @@ +use crate::core::storage::data::DATA; +use crate::log_info; +use crate::AppState; +use tauri::State; + +use std::path::PathBuf; + +fn get_mods_folder_for_client( + client: &crate::core::clients::client::Client, +) -> Result { + let (folder, _) = client.get_launch_paths().map_err(|e| e.to_string())?; + Ok(folder.join("mods")) +} + +fn get_mods_folder_for_custom_client( + custom_client: &crate::core::clients::custom_clients::CustomClient, +) -> Result { + let parent = custom_client + .file_path + .parent() + .ok_or_else(|| "Cannot determine client folder".to_string())?; + + // Fabric/Forge clients already have the jar inside a "mods/" subdirectory, + // so we should NOT append "mods" again to avoid creating a "mods/mods" path. + if parent.ends_with("mods") { + Ok(parent.to_path_buf()) + } else { + Ok(parent.join("mods")) + } +} + +async fn list_jar_files(mods_folder: &std::path::Path) -> Result, String> { + if !mods_folder.exists() { + return Ok(Vec::new()); + } + let mut mods = Vec::new(); + let mut entries = tokio::fs::read_dir(mods_folder) + .await + .map_err(|e| format!("Failed to read mods directory: {e}"))?; + while let Some(entry) = entries.next_entry().await.map_err(|e| e.to_string())? { + let path = entry.path(); + if path.is_file() { + if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { + if filename.ends_with(".jar") { + mods.push(filename.to_string()); + } + } + } + } + Ok(mods) +} + +async fn remove_mod_file(mods_folder: &std::path::Path, filename: &str) -> Result<(), String> { + let target_file = mods_folder.join(filename); + if !target_file.exists() { + return Err("Mod file does not exist".to_string()); + } + tokio::fs::remove_file(target_file) + .await + .map_err(|e| format!("Failed to delete mod file: {e}")) +} + +#[tauri::command] +pub async fn install_mod_from_url( + id: u32, + url: String, + filename: String, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!( + "Installing mod for client {}: {} from {}", + id, + filename, + url + ); + + let client = super::get_client_by_id(id, &state.clients.manager)?; + + let mods_folder_relative = { + let (folder, _) = client.get_launch_paths().map_err(|e| e.to_string())?; + let root = DATA.root_dir.lock().unwrap().clone(); + let relative = folder + .strip_prefix(&root) + .map_err(|_| "Client folder is outside of root directory".to_string())?; + relative.join("mods") + }; + + let mods_folder_str = mods_folder_relative + .to_str() + .ok_or_else(|| "Invalid mods folder path".to_string())?; + + DATA.download_to_folder(&url, mods_folder_str).await?; + + log_info!( + "Successfully installed mod: {} to {}", + filename, + mods_folder_str + ); + + Ok(()) +} + +#[tauri::command] +pub async fn list_installed_mods( + id: u32, + state: State<'_, AppState>, +) -> Result, String> { + let client = super::get_client_by_id(id, &state.clients.manager)?; + let mods_folder = get_mods_folder_for_client(&client)?; + list_jar_files(&mods_folder).await +} + +#[tauri::command] +pub async fn uninstall_mod( + id: u32, + filename: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let client = super::get_client_by_id(id, &state.clients.manager)?; + let mods_folder = get_mods_folder_for_client(&client)?; + remove_mod_file(&mods_folder, &filename).await +} + +#[tauri::command] +pub async fn install_mod_for_custom_client( + id: u32, + url: String, + filename: String, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!( + "Installing mod for custom client {}: {} from {}", + id, + filename, + url + ); + + let custom_client = { + let manager = state.custom_clients.lock(); + manager + .get_client(id) + .cloned() + .ok_or_else(|| "Custom client not found".to_string())? + }; + + let mods_folder = get_mods_folder_for_custom_client(&custom_client)?; + + tokio::fs::create_dir_all(&mods_folder) + .await + .map_err(|e| format!("Failed to create mods folder: {e}"))?; + + let dest = mods_folder.join(&filename); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .header("User-Agent", "CollapseLoader-Reborn") + .send() + .await + .map_err(|e| format!("Failed to download mod: {e}"))?; + + if !response.status().is_success() { + return Err(format!( + "Download failed with status: {}", + response.status() + )); + } + + let bytes = response + .bytes() + .await + .map_err(|e| format!("Failed to read mod response: {e}"))?; + + tokio::fs::write(&dest, &bytes) + .await + .map_err(|e| format!("Failed to write mod file: {e}"))?; + + log_info!( + "Successfully installed mod: {} ({} bytes)", + filename, + bytes.len() + ); + Ok(()) +} + +#[tauri::command] +pub async fn list_installed_mods_custom( + id: u32, + state: State<'_, AppState>, +) -> Result, String> { + let custom_client = { + let manager = state.custom_clients.lock(); + manager + .get_client(id) + .cloned() + .ok_or_else(|| "Custom client not found".to_string())? + }; + + let mods_folder = get_mods_folder_for_custom_client(&custom_client)?; + list_jar_files(&mods_folder).await +} + +#[tauri::command] +pub async fn uninstall_mod_custom( + id: u32, + filename: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let custom_client = { + let manager = state.custom_clients.lock(); + manager + .get_client(id) + .cloned() + .ok_or_else(|| "Custom client not found".to_string())? + }; + + let mods_folder = get_mods_folder_for_custom_client(&custom_client)?; + remove_mod_file(&mods_folder, &filename).await +} diff --git a/src-tauri/src/commands/clients/ram.rs b/src-tauri/src/commands/clients/ram.rs new file mode 100644 index 00000000..5f218811 --- /dev/null +++ b/src-tauri/src/commands/clients/ram.rs @@ -0,0 +1,73 @@ +use super::get_client_by_id; +use crate::core::clients::client::Client; +use crate::AppState; +use serde::Serialize; +use sysinfo::{MemoryRefreshKind, Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; +use tauri::State; + +#[derive(Debug, Clone, Serialize)] +pub struct ClientRamUsage { + pub client_id: u32, + pub is_running: bool, + pub process_count: usize, + pub pids: Vec, + pub total_memory_bytes: u64, + pub total_memory_mib: f64, + pub system_total_memory_bytes: u64, + pub system_total_memory_mib: f64, + pub system_memory_percent: f64, +} + +fn collect_client_ram_usage(client: &Client) -> ClientRamUsage { + let pids = crate::core::utils::process::find_processes_by_filename(&client.filename) + .into_iter() + .filter_map(|pid| pid.parse::().ok()) + .collect::>(); + + let mut system = System::new_with_specifics( + RefreshKind::nothing() + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes(ProcessRefreshKind::nothing().with_memory()), + ); + system.refresh_memory(); + let _ = system.refresh_processes(ProcessesToUpdate::All, true); + + let total_memory_bytes = pids + .iter() + .filter_map(|pid| system.process(Pid::from_u32(*pid))) + .map(|process| process.memory()) + .sum::(); + + let system_total_memory_bytes = system.total_memory(); + let total_memory_mib = total_memory_bytes as f64 / 1024.0 / 1024.0; + let system_total_memory_mib = system_total_memory_bytes as f64 / 1024.0 / 1024.0; + let system_memory_percent = if system_total_memory_bytes > 0 { + (total_memory_bytes as f64 / system_total_memory_bytes as f64) * 100.0 + } else { + 0.0 + }; + + ClientRamUsage { + client_id: client.id, + is_running: !pids.is_empty(), + process_count: pids.len(), + pids, + total_memory_bytes, + total_memory_mib, + system_total_memory_bytes, + system_total_memory_mib, + system_memory_percent, + } +} + +#[tauri::command] +pub async fn get_client_ram_usage( + id: u32, + state: State<'_, AppState>, +) -> Result { + let client = get_client_by_id(id, &state.clients.manager)?; + let client_clone = client.clone(); + tokio::task::spawn_blocking(move || collect_client_ram_usage(&client_clone)) + .await + .map_err(|e| format!("Failed to get client RAM usage: {e}")) +} diff --git a/src-tauri/src/commands/clients/shortcuts.rs b/src-tauri/src/commands/clients/shortcuts.rs new file mode 100644 index 00000000..a36334d2 --- /dev/null +++ b/src-tauri/src/commands/clients/shortcuts.rs @@ -0,0 +1,214 @@ +use super::{get_client_by_id, sanitize_filename}; +use crate::log_info; +use crate::AppState; +use tauri::State; + +fn resolve_client_id(id: u32, custom_id: Option, is_custom: bool) -> u32 { + if is_custom { + custom_id.unwrap_or(id) + } else { + id + } +} + +#[tauri::command] +pub fn create_client_shortcut( + id: u32, + custom_id: Option, + is_custom: bool, + shortcut_name: Option, + icon_path: Option, + state: State<'_, AppState>, +) -> Result<(), String> { + let (client_name, _client_path) = if is_custom { + let cid = custom_id.unwrap_or(id); + let manager = state.custom_clients.lock(); + let c = manager + .get_client(cid) + .cloned() + .ok_or_else(|| "Custom client not found".to_string())?; + drop(manager); + (c.name.clone(), c.file_path.clone()) + } else { + let c = get_client_by_id(id, &state.clients.manager)?; + let (folder, _) = c.get_launch_paths()?; + (c.name.clone(), folder) + }; + + let display_name = shortcut_name + .filter(|s| !s.trim().is_empty()) + .unwrap_or_else(|| client_name.clone()); + + let exe_path = + std::env::current_exe().map_err(|e| format!("Failed to get executable path: {e}"))?; + + log_info!( + "Creating shortcut '{}' for client '{}' (exe: {})", + display_name, + client_name, + exe_path.display() + ); + + create_shortcut_platform( + &display_name, + &exe_path, + id, + custom_id, + is_custom, + icon_path.as_deref(), + ) +} + +#[cfg(target_os = "windows")] +fn create_shortcut_platform( + display_name: &str, + exe_path: &std::path::Path, + id: u32, + custom_id: Option, + is_custom: bool, + icon_path: Option<&str>, +) -> Result<(), String> { + let client_id = resolve_client_id(id, custom_id, is_custom); + let args = format!("collapseloader://launch-client/{}", client_id); + + let exe_str = exe_path.to_string_lossy().into_owned(); + let icon_str = icon_path.unwrap_or(&exe_str).to_string(); + let safe_name = sanitize_filename(display_name); + + let script = format!( + r#" +$desktop = [Environment]::GetFolderPath('Desktop'); +$lnk = Join-Path $desktop '{safe_name}.lnk'; +$exe = '{exe}'; +$ico = '{ico}'; +$ws = New-Object -ComObject WScript.Shell; +$s = $ws.CreateShortcut($lnk); +$s.TargetPath = $exe; +$s.Arguments = '{link_args}'; +$s.Description = 'Launch {desc} via CollapseLoader'; +$s.IconLocation = $ico; +$s.Save(); +Write-Output $lnk +"#, + safe_name = safe_name.replace('\'', "''"), + exe = exe_str.replace('\'', "''"), + ico = icon_str.replace('\'', "''"), + link_args = args, + desc = display_name.replace('\'', "''"), + ); + + let output = std::process::Command::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", &script]) + .output() + .map_err(|e| format!("Failed to run PowerShell: {e}"))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("PowerShell shortcut creation failed: {stderr}")); + } + + let lnk_out = String::from_utf8_lossy(&output.stdout).trim().to_string(); + log_info!("Shortcut created at: {}", lnk_out); + Ok(()) +} + +#[cfg(target_os = "linux")] +fn create_shortcut_platform( + display_name: &str, + exe_path: &std::path::Path, + id: u32, + custom_id: Option, + is_custom: bool, + icon_path: Option<&str>, +) -> Result<(), String> { + let client_id = resolve_client_id(id, custom_id, is_custom); + let deep_link = format!("collapseloader://launch-client/{}", client_id); + + let home = std::env::var("HOME").map_err(|_| "Cannot find HOME".to_string())?; + let desktop = std::path::PathBuf::from(&home).join("Desktop"); + + let target_dir = if desktop.exists() { + desktop + } else { + std::path::PathBuf::from(&home) + }; + let desktop_file = target_dir.join(format!("{}.desktop", sanitize_filename(display_name))); + + let icon_line = if let Some(ip) = icon_path { + format!("Icon={}", ip) + } else { + format!("Icon={}", exe_path.to_string_lossy()) + }; + + let content = format!( + "[Desktop Entry]\nVersion=1.0\nType=Application\nName={name}\nExec={exe} {link}\n{icon}\nTerminal=false\nComment=Launch {name} via CollapseLoader\n", + name = display_name, + exe = exe_path.to_string_lossy(), + link = deep_link, + icon = icon_line, + ); + + std::fs::write(&desktop_file, &content) + .map_err(|e| format!("Failed to write .desktop file: {e}"))?; + + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&desktop_file, std::fs::Permissions::from_mode(0o755)) + .map_err(|e| format!("Failed to set permissions: {e}"))?; + + log_info!("Desktop shortcut created at: {}", desktop_file.display()); + Ok(()) +} + +#[cfg(target_os = "macos")] +fn create_shortcut_platform( + display_name: &str, + _exe_path: &std::path::Path, + id: u32, + custom_id: Option, + is_custom: bool, + _icon_path: Option<&str>, +) -> Result<(), String> { + let client_id = resolve_client_id(id, custom_id, is_custom); + let deep_link = format!("collapseloader://launch-client/{}", client_id); + + let home = std::env::var("HOME").map_err(|_| "Cannot find HOME".to_string())?; + let desktop = std::path::PathBuf::from(&home).join("Desktop"); + let target_dir = if desktop.exists() { + desktop + } else { + std::path::PathBuf::from(&home) + }; + + let app_bundle = target_dir.join(format!("{}.app", sanitize_filename(display_name))); + let contents = app_bundle.join("Contents"); + let macos_dir = contents.join("MacOS"); + + std::fs::create_dir_all(&macos_dir) + .map_err(|e| format!("Failed to create .app bundle: {e}"))?; + + let plist = format!( + "\n\ + \n\ + \n\ + CFBundleName{name}\n\ + CFBundleExecutablelaunch\n\ + CFBundleIdentifiercom.collapseloader.shortcut.{id}\n\ + \n", + name = display_name, + id = client_id, + ); + std::fs::write(contents.join("Info.plist"), plist) + .map_err(|e| format!("Failed to write Info.plist: {e}"))?; + + let script = format!("#!/bin/sh\nopen '{}'\n", deep_link); + let script_path = macos_dir.join("launch"); + std::fs::write(&script_path, script) + .map_err(|e| format!("Failed to write launcher script: {e}"))?; + + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)) + .map_err(|e| format!("Failed to set permissions: {e}"))?; + + log_info!("macOS app shortcut created at: {}", app_bundle.display()); + Ok(()) +} diff --git a/src-tauri/src/commands/mod_builds.rs b/src-tauri/src/commands/mod_builds.rs index 1da5bc61..dcf49fe4 100644 --- a/src-tauri/src/commands/mod_builds.rs +++ b/src-tauri/src/commands/mod_builds.rs @@ -1,43 +1,54 @@ -use crate::core::storage::mod_builds::{ModBuild, MOD_BUILDS}; +use crate::core::storage::mod_builds::ModBuild; +use crate::AppState; +use tauri::State; #[tauri::command] -pub async fn get_all_mod_builds() -> Result, String> { - let builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn get_all_mod_builds(state: State<'_, AppState>) -> Result, String> { + let builds = state.mod_builds(); Ok(builds.get_all().to_vec()) } #[tauri::command] -pub async fn get_mod_build(id: String) -> Result, String> { - let builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn get_mod_build( + id: String, + state: State<'_, AppState>, +) -> Result, String> { + let builds = state.mod_builds(); Ok(builds.get(&id).cloned()) } #[tauri::command] -pub async fn create_mod_build(build: ModBuild) -> Result { - let mut builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn create_mod_build( + build: ModBuild, + state: State<'_, AppState>, +) -> Result { + let mut builds = state.mod_builds(); let created = build.clone(); builds.create(build); Ok(created) } #[tauri::command] -pub async fn update_mod_build(build: ModBuild) -> Result { - let mut builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn update_mod_build( + build: ModBuild, + state: State<'_, AppState>, +) -> Result { + let mut builds = state.mod_builds(); let updated = build.clone(); builds.update(build); Ok(updated) } #[tauri::command] -pub async fn delete_mod_build(id: String) -> Result<(), String> { - let mut builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn delete_mod_build(id: String, state: State<'_, AppState>) -> Result<(), String> { + let mut builds = state.mod_builds(); builds.delete(&id); Ok(()) } #[tauri::command] -pub async fn export_mod_build(id: String) -> Result { - let builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn export_mod_build(id: String, state: State<'_, AppState>) -> Result { + let builds = state.mod_builds(); builds .get(&id) .cloned() @@ -45,8 +56,11 @@ pub async fn export_mod_build(id: String) -> Result { } #[tauri::command] -pub async fn import_mod_build(build: ModBuild) -> Result { - let mut builds = MOD_BUILDS.lock().map_err(|e| e.to_string())?; +pub async fn import_mod_build( + build: ModBuild, + state: State<'_, AppState>, +) -> Result { + let mut builds = state.mod_builds(); let imported = build.clone(); builds.create(build); Ok(imported) diff --git a/src-tauri/src/commands/network.rs b/src-tauri/src/commands/network.rs index 6d8f13c5..32b1b8fe 100644 --- a/src-tauri/src/commands/network.rs +++ b/src-tauri/src/commands/network.rs @@ -1,7 +1,7 @@ -use crate::core::network::create_client; +use crate::core::network::get_api_client; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; use std::sync::{Mutex, OnceLock}; -use std::time::Duration; use tauri::{AppHandle, Emitter}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -21,13 +21,13 @@ pub struct NetworkRequest { pub error_message: Option, } -static NETWORK_HISTORY: OnceLock>> = OnceLock::new(); +static NETWORK_HISTORY: OnceLock>> = OnceLock::new(); -fn get_history() -> &'static Mutex> { - NETWORK_HISTORY.get_or_init(|| Mutex::new(Vec::new())) +fn get_history() -> &'static Mutex> { + NETWORK_HISTORY.get_or_init(|| Mutex::new(VecDeque::new())) } -fn with_network_history(operation: impl FnOnce(&mut Vec) -> R) -> R { +fn with_network_history(operation: impl FnOnce(&mut VecDeque) -> R) -> R { let mut history = get_history().lock().unwrap(); operation(&mut history) } @@ -61,8 +61,7 @@ pub async fn api_request( body: Option, app_handle: AppHandle, ) -> Result { - static API_CLIENT: OnceLock = OnceLock::new(); - let client = API_CLIENT.get_or_init(|| create_client(Duration::from_secs(30))); + let client = get_api_client(); let start = std::time::Instant::now(); let id = uuid::Uuid::new_v4().to_string(); @@ -236,14 +235,16 @@ pub fn clear_network_history() { #[tauri::command] pub fn get_network_history() -> Result, String> { - Ok(with_network_history(|history| history.clone())) + Ok(with_network_history(|history| { + history.iter().cloned().collect() + })) } fn save_request_history(rec: NetworkRequest) { with_network_history(|history| { - history.push(rec); + history.push_back(rec); if history.len() > 1000 { - history.remove(0); + history.pop_front(); } }); } diff --git a/src-tauri/src/commands/presets.rs b/src-tauri/src/commands/presets.rs index 92afc23f..ba310625 100644 --- a/src-tauri/src/commands/presets.rs +++ b/src-tauri/src/commands/presets.rs @@ -1,6 +1,7 @@ -use crate::core::storage::presets::{ThemePreset, PRESET_MANAGER}; -use crate::{log_debug, log_info, log_warn}; +use crate::core::storage::presets::ThemePreset; +use crate::{log_debug, log_info, log_warn, AppState}; use chrono::Utc; +use tauri::State; use uuid::Uuid; #[derive(Clone, serde::Deserialize)] @@ -124,31 +125,29 @@ pub struct UpdatePresetInput { } #[tauri::command] -pub fn get_all_presets() -> Result, String> { +pub fn get_all_presets(state: State<'_, AppState>) -> Result, String> { log_debug!("Fetching all theme presets"); - PRESET_MANAGER - .lock() - .map(|p| p.get_all_presets()) - .map_err(|e| { - log_warn!("Failed to get presets: {}", e); - "Failed to get presets".to_string() - }) + let presets = state.presets(); + Ok(presets.get_all_presets()) } #[tauri::command] -pub fn get_preset(id: String) -> Result, String> { +pub fn get_preset(state: State<'_, AppState>, id: String) -> Result, String> { log_debug!("Fetching theme preset with ID: {}", id); - let preset_manager = PRESET_MANAGER.lock().unwrap(); + let preset_manager = state.presets(); Ok(preset_manager.get_preset(&id).cloned()) } #[tauri::command] -pub fn create_preset(input: CreatePresetInput) -> Result { +pub fn create_preset( + state: State<'_, AppState>, + input: CreatePresetInput, +) -> Result { log_info!( "Creating new theme preset with name: '{}'", input.preset.name ); - let mut preset_manager = PRESET_MANAGER.lock().unwrap(); + let mut preset_manager = state.presets(); let preset = build_preset( Uuid::new_v4().to_string(), @@ -165,9 +164,12 @@ pub fn create_preset(input: CreatePresetInput) -> Result { } #[tauri::command] -pub fn update_preset(input: UpdatePresetInput) -> Result { +pub fn update_preset( + state: State<'_, AppState>, + input: UpdatePresetInput, +) -> Result { log_info!("Updating theme preset with ID: {}", input.id); - let mut preset_manager = PRESET_MANAGER.lock().unwrap(); + let mut preset_manager = state.presets(); if !preset_manager.preset_exists(&input.id) { log_warn!("Update failed: Preset with ID '{}' not found", input.id); @@ -188,16 +190,20 @@ pub fn update_preset(input: UpdatePresetInput) -> Result { } #[tauri::command] -pub fn delete_preset(id: String) -> Result<(), String> { +pub fn delete_preset(state: State<'_, AppState>, id: String) -> Result<(), String> { log_info!("Deleting theme preset with ID: {}", id); - let mut preset_manager = PRESET_MANAGER.lock().unwrap(); + let mut preset_manager = state.presets(); preset_manager.delete_preset(&id) } #[tauri::command] -pub fn duplicate_preset(id: String, new_name: String) -> Result { +pub fn duplicate_preset( + state: State<'_, AppState>, + id: String, + new_name: String, +) -> Result { log_info!("Duplicating theme preset with ID: {} as '{}'", id, new_name); - let mut preset_manager = PRESET_MANAGER.lock().unwrap(); + let mut preset_manager = state.presets(); let existing_preset = preset_manager.get_preset(&id).ok_or_else(|| { log_warn!("Duplication failed: Preset with ID '{}' not found", id); diff --git a/src-tauri/src/commands/report/helpers.rs b/src-tauri/src/commands/report/helpers.rs new file mode 100644 index 00000000..a7ebb0ba --- /dev/null +++ b/src-tauri/src/commands/report/helpers.rs @@ -0,0 +1,303 @@ +use crate::commands::report::NetworkReport; + +pub(crate) fn get_local_ip() -> String { + std::net::UdpSocket::bind("0.0.0.0:0") + .and_then(|s| { + s.connect("8.8.8.8:80")?; + s.local_addr() + }) + .map(|addr| addr.ip().to_string()) + .unwrap_or_else(|_| "Unknown".to_string()) +} + +pub(crate) fn get_hostname() -> String { + if let Ok(name) = std::env::var("COMPUTERNAME") { + return name; + } + if let Ok(name) = std::env::var("HOSTNAME") { + return name; + } + if let Ok(name) = std::fs::read_to_string("/etc/hostname") { + return name.trim().to_string(); + } + "Unknown".to_string() +} + +pub(crate) fn get_proxy_env_vars() -> Option> { + let mut proxies = std::collections::HashMap::new(); + for key in &["http_proxy", "https_proxy", "all_proxy", "no_proxy"] { + if let Ok(val) = + std::env::var(key.to_lowercase()).or_else(|_| std::env::var(key.to_uppercase())) + { + proxies.insert(key.to_string(), val); + } + } + + if proxies.is_empty() { + None + } else { + Some(proxies) + } +} + +pub(crate) async fn get_local_dns_servers() -> Vec { + tokio::task::spawn_blocking(|| { + let mut dns_servers = Vec::new(); + + #[cfg(target_os = "windows")] + { + if let Ok(output) = std::process::Command::new("powershell") + .args([ + "-NoProfile", + "-Command", + "(Get-DnsClientServerAddress -AddressFamily IPv4).ServerAddresses", + ]) + .output() + { + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let ip = line.trim(); + if !ip.is_empty() { + dns_servers.push(ip.to_string()); + } + } + } + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(content) = std::fs::read_to_string("/etc/resolv.conf") { + for line in content.lines() { + if line.starts_with("nameserver ") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() > 1 { + dns_servers.push(parts[1].to_string()); + } + } + } + } + } + + dns_servers.dedup(); + dns_servers + }) + .await + .unwrap_or_default() +} + +pub(crate) fn push_line(buffer: &mut String, args: std::fmt::Arguments<'_>) { + use std::fmt::Write; + + let _ = buffer.write_fmt(args); + let _ = buffer.write_char('\n'); +} + +pub(crate) fn write_report_header(buffer: &mut String, report: &NetworkReport) { + push_line( + buffer, + format_args!("=================================================="), + ); + push_line( + buffer, + format_args!(" NETWORK DIAGNOSTIC REPORT "), + ); + push_line( + buffer, + format_args!("=================================================="), + ); + push_line( + buffer, + format_args!( + "Generated at: {} (Timestamp: {})", + report.date, report.timestamp + ), + ); + push_line(buffer, format_args!("")); +} + +pub(crate) fn write_environment_section(buffer: &mut String, report: &NetworkReport) { + push_line(buffer, format_args!("--- SYSTEM & APP ENVIRONMENT ---")); + push_line( + buffer, + format_args!("Hostname: {}", report.system_network.hostname), + ); + push_line( + buffer, + format_args!( + "OS: {} ({})", + report.environment.os, report.environment.os_family + ), + ); + push_line( + buffer, + format_args!("Architecture: {}", report.environment.arch), + ); + push_line( + buffer, + format_args!("App Version: {}", report.environment.version), + ); + push_line( + buffer, + format_args!("Executable Path: {}", report.environment.exec_path), + ); + push_line(buffer, format_args!("")); +} + +pub(crate) fn write_local_network_section(buffer: &mut String, report: &NetworkReport) { + push_line(buffer, format_args!("--- LOCAL NETWORK SETTINGS ---")); + push_line( + buffer, + format_args!("Local LAN IP: {}", report.system_network.local_ip), + ); + + if let Some(proxies) = &report.system_network.proxy_settings { + push_line(buffer, format_args!("System Proxies Detected:")); + for (key, value) in proxies { + push_line(buffer, format_args!(" {}: {}", key.to_uppercase(), value)); + } + } else { + push_line(buffer, format_args!("System Proxies Detected: None")); + } + + if report.system_network.local_dns_servers.is_empty() { + push_line( + buffer, + format_args!("Local DNS Servers: Unknown/Failed to parse"), + ); + push_line(buffer, format_args!("")); + } else { + push_line( + buffer, + format_args!( + "Local DNS Servers: {}", + report.system_network.local_dns_servers.join(", ") + ), + ); + push_line(buffer, format_args!("")); + } +} + +pub(crate) fn write_current_configuration_section(buffer: &mut String, report: &NetworkReport) { + push_line(buffer, format_args!("--- CURRENT CONFIGURATION ---")); + push_line( + buffer, + format_args!( + "Selected API Server: {}", + report.selected_api.as_deref().unwrap_or("None") + ), + ); + push_line( + buffer, + format_args!( + "Selected CDN Server: {}", + report.selected_cdn.as_deref().unwrap_or("None") + ), + ); + push_line(buffer, format_args!("")); +} + +pub(crate) fn write_ping_section(buffer: &mut String, report: &NetworkReport) { + push_line( + buffer, + format_args!("--- HTTP SERVER REACHABILITY (PING) ---"), + ); + + for ping in &report.pings { + push_line(buffer, format_args!("URL: {}", ping.url)); + + if let Some(latency) = ping.latency_ms { + push_line(buffer, format_args!(" HTTP Latency: {} ms", latency)); + } + + if let Some(status) = ping.status_code { + push_line(buffer, format_args!(" HTTP Status: {}", status)); + } + + if let Some(length) = ping.content_length { + push_line(buffer, format_args!(" Content Length: {} bytes", length)); + } + + if let Some(headers) = &ping.headers { + push_line(buffer, format_args!(" Response Headers:")); + for (key, value) in headers { + push_line(buffer, format_args!(" {}: {}", key, value)); + } + } + + if let Some(snippet) = &ping.response_snippet { + push_line(buffer, format_args!(" Response Snippet:")); + push_line(buffer, format_args!(" {}", snippet.replace('\n', "\\n"))); + } + + if let Some(error) = &ping.error { + push_line(buffer, format_args!(" HTTP Error: {}", error)); + } + + push_line(buffer, format_args!("")); + } +} + +pub(crate) fn write_dns_section(buffer: &mut String, report: &NetworkReport) { + push_line(buffer, format_args!("--- DNS RESOLUTION & TCP CHECK ---")); + + for dns in &report.dns { + push_line(buffer, format_args!("Host: {}", dns.host)); + + if dns.resolved_ips.is_empty() { + push_line( + buffer, + format_args!(" IPs: None resolved (Blocked or DNS down)"), + ); + } else { + push_line( + buffer, + format_args!(" IPs: {}", dns.resolved_ips.join(", ")), + ); + } + + push_line( + buffer, + format_args!( + " TCP 443 Reachable: {}", + if dns.tcp_port_443_reachable { + "YES" + } else { + "NO" + } + ), + ); + + if let Some(latency) = dns.tcp_latency_ms { + push_line( + buffer, + format_args!(" TCP Latency (best reachable): {} ms", latency), + ); + } + + if let Some(dns_ms) = dns.dns_lookup_ms { + push_line(buffer, format_args!(" DNS Lookup Time: {} ms", dns_ms)); + } + + if !dns.ip_latencies.is_empty() { + push_line(buffer, format_args!(" Per-IP TCP Latencies:")); + for ip in &dns.ip_latencies { + push_line( + buffer, + format_args!( + " {} - {}", + ip.ip, + ip.tcp_latency_ms + .map(|latency| format!("{} ms", latency)) + .unwrap_or_else(|| "unreachable".to_string()) + ), + ); + } + } + + if let Some(error) = &dns.error { + push_line(buffer, format_args!(" Error: {}", error)); + } + + push_line(buffer, format_args!("")); + } +} diff --git a/src-tauri/src/commands/report.rs b/src-tauri/src/commands/report/mod.rs similarity index 56% rename from src-tauri/src/commands/report.rs rename to src-tauri/src/commands/report/mod.rs index 3af8dd34..0fc01b7d 100644 --- a/src-tauri/src/commands/report.rs +++ b/src-tauri/src/commands/report/mod.rs @@ -1,7 +1,10 @@ +mod helpers; + use crate::core::network::create_client; use crate::core::network::servers::SERVERS; use crate::core::storage::data::DATA; use crate::core::utils::globals::{API_SERVERS, CDN_SERVERS}; +use helpers::*; use serde::Serialize; use std::env; use std::sync::OnceLock; @@ -65,91 +68,6 @@ pub struct NetworkReport { pub selected_cdn: Option, } -fn get_local_ip() -> String { - std::net::UdpSocket::bind("0.0.0.0:0") - .and_then(|s| { - s.connect("8.8.8.8:80")?; - s.local_addr() - }) - .map(|addr| addr.ip().to_string()) - .unwrap_or_else(|_| "Unknown".to_string()) -} - -fn get_hostname() -> String { - if let Ok(name) = std::env::var("COMPUTERNAME") { - return name; - } - if let Ok(name) = std::env::var("HOSTNAME") { - return name; - } - if let Ok(name) = std::fs::read_to_string("/etc/hostname") { - return name.trim().to_string(); - } - "Unknown".to_string() -} - -fn get_proxy_env_vars() -> Option> { - let mut proxies = std::collections::HashMap::new(); - for key in &["http_proxy", "https_proxy", "all_proxy", "no_proxy"] { - if let Ok(val) = - std::env::var(key.to_lowercase()).or_else(|_| std::env::var(key.to_uppercase())) - { - proxies.insert(key.to_string(), val); - } - } - - if proxies.is_empty() { - None - } else { - Some(proxies) - } -} - -pub async fn get_local_dns_servers() -> Vec { - tokio::task::spawn_blocking(|| { - let mut dns_servers = Vec::new(); - - #[cfg(target_os = "windows")] - { - if let Ok(output) = std::process::Command::new("powershell") - .args([ - "-NoProfile", - "-Command", - "(Get-DnsClientServerAddress -AddressFamily IPv4).ServerAddresses", - ]) - .output() - { - let stdout = String::from_utf8_lossy(&output.stdout); - for line in stdout.lines() { - let ip = line.trim(); - if !ip.is_empty() { - dns_servers.push(ip.to_string()); - } - } - } - } - - #[cfg(not(target_os = "windows"))] - { - if let Ok(content) = std::fs::read_to_string("/etc/resolv.conf") { - for line in content.lines() { - if line.starts_with("nameserver ") { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() > 1 { - dns_servers.push(parts[1].to_string()); - } - } - } - } - } - - dns_servers.dedup(); - dns_servers - }) - .await - .unwrap_or_default() -} - #[tauri::command] pub async fn generate_network_report(app_handle: AppHandle) -> Result { static REPORT_CLIENT: OnceLock = OnceLock::new(); @@ -353,223 +271,6 @@ pub async fn generate_network_report(app_handle: AppHandle) -> Result) { - use std::fmt::Write; - - let _ = buffer.write_fmt(args); - let _ = buffer.write_char('\n'); -} - -fn write_report_header(buffer: &mut String, report: &NetworkReport) { - push_line( - buffer, - format_args!("=================================================="), - ); - push_line( - buffer, - format_args!(" NETWORK DIAGNOSTIC REPORT "), - ); - push_line( - buffer, - format_args!("=================================================="), - ); - push_line( - buffer, - format_args!( - "Generated at: {} (Timestamp: {})", - report.date, report.timestamp - ), - ); - push_line(buffer, format_args!("")); -} - -fn write_environment_section(buffer: &mut String, report: &NetworkReport) { - push_line(buffer, format_args!("--- SYSTEM & APP ENVIRONMENT ---")); - push_line( - buffer, - format_args!("Hostname: {}", report.system_network.hostname), - ); - push_line( - buffer, - format_args!( - "OS: {} ({})", - report.environment.os, report.environment.os_family - ), - ); - push_line( - buffer, - format_args!("Architecture: {}", report.environment.arch), - ); - push_line( - buffer, - format_args!("App Version: {}", report.environment.version), - ); - push_line( - buffer, - format_args!("Executable Path: {}", report.environment.exec_path), - ); - push_line(buffer, format_args!("")); -} - -fn write_local_network_section(buffer: &mut String, report: &NetworkReport) { - push_line(buffer, format_args!("--- LOCAL NETWORK SETTINGS ---")); - push_line( - buffer, - format_args!("Local LAN IP: {}", report.system_network.local_ip), - ); - - if let Some(proxies) = &report.system_network.proxy_settings { - push_line(buffer, format_args!("System Proxies Detected:")); - for (key, value) in proxies { - push_line(buffer, format_args!(" {}: {}", key.to_uppercase(), value)); - } - } else { - push_line(buffer, format_args!("System Proxies Detected: None")); - } - - if report.system_network.local_dns_servers.is_empty() { - push_line( - buffer, - format_args!("Local DNS Servers: Unknown/Failed to parse"), - ); - push_line(buffer, format_args!("")); - } else { - push_line( - buffer, - format_args!( - "Local DNS Servers: {}", - report.system_network.local_dns_servers.join(", ") - ), - ); - push_line(buffer, format_args!("")); - } -} - -fn write_current_configuration_section(buffer: &mut String, report: &NetworkReport) { - push_line(buffer, format_args!("--- CURRENT CONFIGURATION ---")); - push_line( - buffer, - format_args!( - "Selected API Server: {}", - report.selected_api.as_deref().unwrap_or("None") - ), - ); - push_line( - buffer, - format_args!( - "Selected CDN Server: {}", - report.selected_cdn.as_deref().unwrap_or("None") - ), - ); - push_line(buffer, format_args!("")); -} - -fn write_ping_section(buffer: &mut String, report: &NetworkReport) { - push_line( - buffer, - format_args!("--- HTTP SERVER REACHABILITY (PING) ---"), - ); - - for ping in &report.pings { - push_line(buffer, format_args!("URL: {}", ping.url)); - - if let Some(latency) = ping.latency_ms { - push_line(buffer, format_args!(" HTTP Latency: {} ms", latency)); - } - - if let Some(status) = ping.status_code { - push_line(buffer, format_args!(" HTTP Status: {}", status)); - } - - if let Some(length) = ping.content_length { - push_line(buffer, format_args!(" Content Length: {} bytes", length)); - } - - if let Some(headers) = &ping.headers { - push_line(buffer, format_args!(" Response Headers:")); - for (key, value) in headers { - push_line(buffer, format_args!(" {}: {}", key, value)); - } - } - - if let Some(snippet) = &ping.response_snippet { - push_line(buffer, format_args!(" Response Snippet:")); - push_line(buffer, format_args!(" {}", snippet.replace('\n', "\\n"))); - } - - if let Some(error) = &ping.error { - push_line(buffer, format_args!(" HTTP Error: {}", error)); - } - - push_line(buffer, format_args!("")); - } -} - -fn write_dns_section(buffer: &mut String, report: &NetworkReport) { - push_line(buffer, format_args!("--- DNS RESOLUTION & TCP CHECK ---")); - - for dns in &report.dns { - push_line(buffer, format_args!("Host: {}", dns.host)); - - if dns.resolved_ips.is_empty() { - push_line( - buffer, - format_args!(" IPs: None resolved (Blocked or DNS down)"), - ); - } else { - push_line( - buffer, - format_args!(" IPs: {}", dns.resolved_ips.join(", ")), - ); - } - - push_line( - buffer, - format_args!( - " TCP 443 Reachable: {}", - if dns.tcp_port_443_reachable { - "YES" - } else { - "NO" - } - ), - ); - - if let Some(latency) = dns.tcp_latency_ms { - push_line( - buffer, - format_args!(" TCP Latency (best reachable): {} ms", latency), - ); - } - - if let Some(dns_ms) = dns.dns_lookup_ms { - push_line(buffer, format_args!(" DNS Lookup Time: {} ms", dns_ms)); - } - - if !dns.ip_latencies.is_empty() { - push_line(buffer, format_args!(" Per-IP TCP Latencies:")); - for ip in &dns.ip_latencies { - push_line( - buffer, - format_args!( - " {} - {}", - ip.ip, - ip.tcp_latency_ms - .map(|latency| format!("{} ms", latency)) - .unwrap_or_else(|| "unreachable".to_string()) - ), - ); - } - } - - if let Some(error) = &dns.error { - push_line(buffer, format_args!(" Error: {}", error)); - } - - push_line(buffer, format_args!("")); - } -} - #[tauri::command] pub async fn export_network_report(app_handle: AppHandle) -> Result { let report = generate_network_report(app_handle).await?; diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 286c701f..8b5690fe 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -1,9 +1,8 @@ use crate::commands::utils::refresh_tray_menu; -use crate::core::storage::accounts::{Account, ACCOUNT_MANAGER}; +use crate::core::storage::accounts::Account; use crate::core::storage::common::JsonStorage; -use crate::core::storage::favorites::FAVORITE_MANAGER; -use crate::core::storage::flags::{Flags, FLAGS_MANAGER}; -use crate::core::storage::settings::{settings_schema, Settings, SETTINGS}; +use crate::core::storage::flags::Flags; +use crate::core::storage::settings::{settings_schema, Settings}; use crate::core::utils::discord_rpc; #[cfg(target_os = "windows")] use crate::core::utils::dpi; @@ -11,50 +10,6 @@ use crate::{log_debug, log_error, log_info, log_warn, AppState}; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; use tauri::State; -fn with_account_manager( - operation: impl FnOnce(&mut crate::core::storage::accounts::AccountManager) -> Result, -) -> Result { - let mut account_manager = ACCOUNT_MANAGER.lock().map_err(|e| { - log_error!("Failed to acquire lock on account manager: {}", e); - "Failed to acquire lock on account manager".to_string() - })?; - - operation(&mut account_manager) -} - -fn with_favorite_manager( - operation: impl FnOnce(&mut crate::core::storage::favorites::FavoriteManager) -> Result, -) -> Result { - let mut favorite_manager = FAVORITE_MANAGER.lock().map_err(|e| { - log_error!("Failed to acquire lock on favorite manager: {}", e); - "Failed to acquire lock on favorite manager".to_string() - })?; - - let result = operation(&mut favorite_manager)?; - favorite_manager.save_to_disk(); - Ok(result) -} - -fn update_flags(operation: impl FnOnce(&mut Flags)) -> Result<(), String> { - let mut flags = FLAGS_MANAGER.lock().map_err(|e| { - log_error!("Failed to acquire lock on flags manager: {}", e); - "Failed to acquire lock on flags manager".to_string() - })?; - operation(&mut flags); - flags.save_to_disk(); - Ok(()) -} - -fn update_settings(operation: impl FnOnce(&mut Settings)) -> Result<(), String> { - let mut settings = SETTINGS.lock().map_err(|e| { - log_error!("Failed to acquire lock on settings manager: {}", e); - "Failed to acquire lock on settings manager".to_string() - })?; - operation(&mut settings); - settings.save_to_disk(); - Ok(()) -} - #[cfg(target_os = "windows")] fn set_autostart_registry(enabled: bool) -> Result<(), String> { use winreg::enums::*; @@ -179,8 +134,8 @@ fn set_autostart_registry(enabled: bool) -> Result<(), String> { } #[tauri::command] -pub fn get_settings() -> Settings { - SETTINGS.lock().unwrap().clone() +pub fn get_settings(state: State<'_, AppState>) -> Settings { + state.settings().clone() } #[tauri::command] @@ -189,8 +144,8 @@ pub fn get_settings_schema() -> Vec<(String, String)> { } #[tauri::command] -pub fn get_setting_bool(key: String) -> bool { - let s = SETTINGS.lock().unwrap(); +pub fn get_setting_bool(state: State<'_, AppState>, key: String) -> bool { + let s = state.settings(); matches!( key.as_str(), "auto_update" @@ -209,19 +164,22 @@ pub fn get_setting_bool(key: String) -> bool { } #[tauri::command] -pub fn get_flags() -> Flags { - FLAGS_MANAGER.lock().unwrap().clone() +pub fn get_flags(state: State<'_, AppState>) -> Flags { + state.flags().clone() } #[tauri::command] -pub fn reset_flags() -> Result<(), String> { +pub fn reset_flags(state: State<'_, AppState>) -> Result<(), String> { log_info!("Resetting application flags to default"); - update_flags(|flags| *flags = Flags::default()) + let mut flags = state.flags(); + *flags = Flags::default(); + flags.save_to_disk(); + Ok(()) } #[tauri::command] -pub fn save_settings(input_settings: Settings) -> Result<(), String> { - let mut current_settings = SETTINGS.lock().unwrap(); +pub fn save_settings(state: State<'_, AppState>, input_settings: Settings) -> Result<(), String> { + let mut current_settings = state.settings(); let config_path = current_settings.config_path.clone(); let old_discord_rpc_enabled = current_settings.discord_rpc_enabled.value; @@ -297,134 +255,126 @@ pub fn save_settings(input_settings: Settings) -> Result<(), String> { } #[tauri::command] -pub fn reset_settings() -> Result<(), String> { +pub fn reset_settings(state: State<'_, AppState>) -> Result<(), String> { log_info!("Resetting application settings to default"); - update_settings(|current_settings| { - *current_settings = Settings::default(); - current_settings.config_path = Settings::default().config_path; - })?; + let mut settings = state.settings(); + *settings = Settings::default(); + settings.config_path = Settings::default().config_path; + settings.save_to_disk(); log_info!("Default settings saved to disk"); Ok(()) } #[tauri::command] -pub fn mark_disclaimer_shown() -> Result<(), String> { +pub fn mark_disclaimer_shown(state: State<'_, AppState>) -> Result<(), String> { log_info!("Marking disclaimer as shown"); - update_flags(|flags| flags.set_disclaimer_shown(true)) + let mut flags = state.flags(); + flags.set_disclaimer_shown(true); + flags.save_to_disk(); + Ok(()) } #[tauri::command] -pub fn mark_first_run_shown() -> Result<(), String> { +pub fn mark_first_run_shown(state: State<'_, AppState>) -> Result<(), String> { log_info!("Marking first run as shown"); - update_flags(|flags| flags.set_first_run(false)) + let mut flags = state.flags(); + flags.set_first_run(false); + flags.save_to_disk(); + Ok(()) } #[tauri::command] -pub fn set_optional_telemetry(enabled: bool) -> Result<(), String> { +pub fn set_optional_telemetry(state: State<'_, AppState>, enabled: bool) -> Result<(), String> { log_info!("Setting optional telemetry to: {}", enabled); - update_settings(|settings| settings.optional_telemetry.value = enabled) + let mut settings = state.settings(); + settings.optional_telemetry.value = enabled; + settings.save_to_disk(); + Ok(()) } #[tauri::command] -pub fn get_accounts() -> Vec { - ACCOUNT_MANAGER.lock().map_or_else( - |e| { - log_error!("Failed to acquire lock on account manager: {}", e); - Vec::new() - }, - |account_manager| account_manager.accounts.clone(), - ) +pub fn get_accounts(state: State<'_, AppState>) -> Vec { + state.accounts().accounts.clone() } #[tauri::command] -pub fn add_account(username: String, tags: Vec) -> Result { +pub fn add_account( + state: State<'_, AppState>, + username: String, + tags: Vec, +) -> Result { log_info!("Adding new account for user: '{}'", username); - with_account_manager(|account_manager| { - let id = account_manager.add_account(username.clone(), tags); - log_debug!("New account created with ID: {}", id); - log_info!("Account for '{}' saved to disk", username); - Ok(id) - }) + let mut account_manager = state.accounts(); + let id = account_manager.add_account(username.clone(), tags); + log_debug!("New account created with ID: {}", id); + log_info!("Account for '{}' saved to disk", username); + Ok(id) } #[tauri::command] -pub fn remove_account(id: String) -> Result<(), String> { +pub fn remove_account(state: State<'_, AppState>, id: String) -> Result<(), String> { log_info!("Removing account with ID: {}", id); - with_account_manager(|account_manager| { - if account_manager.remove_account(&id) { - log_info!("Account ID {} removed and saved to disk", id); - Ok(()) - } else { - log_error!("Account with ID {} not found for removal", id); - Err("Account not found".to_string()) - } - }) + let mut account_manager = state.accounts(); + if account_manager.remove_account(&id) { + log_info!("Account ID {} removed and saved to disk", id); + Ok(()) + } else { + log_error!("Account with ID {} not found for removal", id); + Err("Account not found".to_string()) + } } #[tauri::command] -pub fn set_active_account(id: String) -> Result<(), String> { +pub fn set_active_account(state: State<'_, AppState>, id: String) -> Result<(), String> { log_info!("Setting active account to ID: {}", id); - with_account_manager(|account_manager| { - if account_manager.set_active_account(&id) { - log_info!("Active account set to {} and saved to disk", id); - Ok(()) - } else { - log_error!("Account with ID {} not found to set as active", id); - Err("Account not found".to_string()) - } - }) + let mut account_manager = state.accounts(); + if account_manager.set_active_account(&id) { + log_info!("Active account set to {} and saved to disk", id); + Ok(()) + } else { + log_error!("Account with ID {} not found to set as active", id); + Err("Account not found".to_string()) + } } #[tauri::command] pub fn update_account( + state: State<'_, AppState>, id: String, username: Option, tags: Option>, ) -> Result<(), String> { log_info!("Updating account with ID: {}", id); - with_account_manager(|account_manager| { - if account_manager.update_account(&id, username, tags) { - log_info!("Account ID {} updated and saved to disk", id); - Ok(()) - } else { - log_error!("Account with ID {} not found for update", id); - Err("Account not found".to_string()) - } - }) + let mut account_manager = state.accounts(); + if account_manager.update_account(&id, username, tags) { + log_info!("Account ID {} updated and saved to disk", id); + Ok(()) + } else { + log_error!("Account with ID {} not found for update", id); + Err("Account not found".to_string()) + } } #[tauri::command] -pub fn get_active_account() -> Option { +pub fn get_active_account(state: State<'_, AppState>) -> Option { log_debug!("Fetching active account"); - ACCOUNT_MANAGER.lock().map_or_else( - |e| { - log_error!("Failed to acquire lock on account manager: {}", e); - None - }, - |account_manager| account_manager.get_active_account().cloned(), - ) + state.accounts().get_active_account().cloned() } #[tauri::command] -pub fn get_favorite_clients() -> Result, String> { - FAVORITE_MANAGER.lock().map_or_else( - |e| { - log_error!("Failed to acquire lock on favorite manager: {}", e); - Err("Failed to acquire lock on favorite manager".to_string()) - }, - |favorite_manager| Ok(favorite_manager.favorites.clone()), - ) +pub fn get_favorite_clients(state: State<'_, AppState>) -> Result, String> { + Ok(state.favorites().favorites.clone()) } #[tauri::command] pub fn add_favorite_client(state: State<'_, AppState>, client_id: u32) -> Result<(), String> { log_info!("Adding client ID {} to favorites", client_id); - with_favorite_manager(|favorite_manager| { - favorite_manager.add_favorite(client_id); - log_info!("Client ID {} added to favorites and saved", client_id); - Ok(()) - })?; + let mut favorite_manager = state.favorites(); + favorite_manager.add_favorite(client_id); + favorite_manager.save_to_disk(); + log_info!("Client ID {} added to favorites and saved", client_id); + drop(favorite_manager); refresh_tray_menu(state); Ok(()) @@ -433,11 +383,11 @@ pub fn add_favorite_client(state: State<'_, AppState>, client_id: u32) -> Result #[tauri::command] pub fn remove_favorite_client(state: State<'_, AppState>, client_id: u32) -> Result<(), String> { log_info!("Removing client ID {} from favorites", client_id); - with_favorite_manager(|favorite_manager| { - favorite_manager.remove_favorite(client_id); - log_info!("Client ID {} removed from favorites and saved", client_id); - Ok(()) - })?; + let mut favorite_manager = state.favorites(); + favorite_manager.remove_favorite(client_id); + favorite_manager.save_to_disk(); + log_info!("Client ID {} removed from favorites and saved", client_id); + drop(favorite_manager); refresh_tray_menu(state); Ok(()) @@ -446,53 +396,59 @@ pub fn remove_favorite_client(state: State<'_, AppState>, client_id: u32) -> Res #[tauri::command] pub fn set_all_favorites(state: State<'_, AppState>, client_ids: Vec) -> Result<(), String> { log_info!("Setting all favorites to: {:?}", client_ids); - with_favorite_manager(|favorite_manager| { - favorite_manager.favorites = client_ids; - log_info!("All favorites updated and saved"); - Ok(()) - })?; + let mut favorite_manager = state.favorites(); + favorite_manager.favorites = client_ids; + favorite_manager.save_to_disk(); + log_info!("All favorites updated and saved"); + drop(favorite_manager); refresh_tray_menu(state); Ok(()) } #[tauri::command] -pub fn is_client_favorite(client_id: u32) -> Result { +pub fn is_client_favorite(state: State<'_, AppState>, client_id: u32) -> Result { log_debug!("Checking if client ID {} is a favorite", client_id); - FAVORITE_MANAGER.lock().map_or_else( - |e| { - log_error!("Failed to acquire lock on favorite manager: {}", e); - Err("Failed to acquire lock on favorite manager".to_string()) - }, - |favorite_manager| Ok(favorite_manager.is_favorite(client_id)), - ) + Ok(state.favorites().is_favorite(client_id)) } #[tauri::command] -pub fn reorder_accounts(ordered_ids: Vec) -> Result<(), String> { +pub fn reorder_accounts( + state: State<'_, AppState>, + ordered_ids: Vec, +) -> Result<(), String> { log_info!("Reordering accounts"); - with_account_manager(|account_manager| { - account_manager.reorder_accounts(ordered_ids); - Ok(()) - }) + let mut account_manager = state.accounts(); + account_manager.reorder_accounts(ordered_ids); + Ok(()) } #[tauri::command] -pub fn mark_telemetry_consent_shown() -> Result<(), String> { +pub fn mark_telemetry_consent_shown(state: State<'_, AppState>) -> Result<(), String> { log_info!("Marking telemetry consent as shown"); - update_flags(|flags| flags.set_telemetry_consent_shown(true)) + let mut flags = state.flags(); + flags.set_telemetry_consent_shown(true); + flags.save_to_disk(); + Ok(()) } #[tauri::command] -pub fn is_telemetry_consent_shown() -> Result { +pub fn is_telemetry_consent_shown(state: State<'_, AppState>) -> Result { log_debug!("Checking if telemetry consent has been shown"); - let flags = FLAGS_MANAGER.lock().unwrap(); + let flags = state.flags(); Ok(flags.telemetry_consent_shown.value) } #[tauri::command] -pub fn set_custom_clients_display(display: String) -> Result<(), String> { - update_flags(|flags| flags.set_custom_clients_display(display)) +pub fn set_custom_clients_display( + state: State<'_, AppState>, + display: String, +) -> Result<(), String> { + log_info!("Setting custom clients display to: {}", display); + let mut flags = state.flags(); + flags.set_custom_clients_display(display); + flags.save_to_disk(); + Ok(()) } #[tauri::command] diff --git a/src-tauri/src/commands/updater.rs b/src-tauri/src/commands/updater.rs index 50b0e074..10c433bb 100644 --- a/src-tauri/src/commands/updater.rs +++ b/src-tauri/src/commands/updater.rs @@ -100,10 +100,15 @@ pub(crate) fn compare_versions(v1: &str, v2: &str) -> Result { } pub(crate) fn truncate_str(s: &str, max: usize) -> String { - if s.len() <= max { + if s.chars().count() <= max { s.to_string() } else { - format!("{}...", &s[..max], s.len() - max) + let truncated: String = s.chars().take(max).collect(); + format!( + "{}...", + truncated, + s.chars().count() - max + ) } } diff --git a/src-tauri/src/commands/utils.rs b/src-tauri/src/commands/utils.rs deleted file mode 100644 index b530648e..00000000 --- a/src-tauri/src/commands/utils.rs +++ /dev/null @@ -1,607 +0,0 @@ -use base64::{engine::general_purpose, Engine}; - -use crate::commands::clients::{ - get_running_client_ids, get_running_custom_client_ids, stop_client, stop_custom_client, -}; -use crate::core::storage::accounts::ACCOUNT_MANAGER; -use crate::core::storage::custom_clients::CUSTOM_CLIENT_MANAGER; -use crate::core::storage::favorites::FAVORITE_MANAGER; -use crate::core::storage::flags::FLAGS_MANAGER; -use crate::core::storage::launch_history::{LaunchEntry, LAUNCH_HISTORY}; -use crate::core::storage::presets::PRESET_MANAGER; -use crate::core::storage::settings::SETTINGS; -use crate::core::utils::discord_rpc; -use crate::core::utils::fs as fs_utils; -use crate::core::utils::globals::{API_SERVERS, CDN_SERVERS, CODENAME}; -use crate::core::utils::helpers::is_development_enabled; -use crate::core::{network::servers::SERVERS, storage::data::DATA}; -use crate::AppState; -use crate::{log_debug, log_error, log_info, log_warn}; -use std::{fs, path::PathBuf}; -use tauri::{AppHandle, Emitter, Manager, State, Theme, Window}; -use tokio::task; - -#[tauri::command] -pub fn cancel_download(name: String) -> Result { - Ok(crate::core::network::downloader::cancel_download(&name)) -} - -#[tauri::command] -pub fn get_version() -> Result { - let result = serde_json::json!({ - "version": env!("CARGO_PKG_VERSION").to_string(), - "codename": CODENAME, - "commitHash": env!("GIT_HASH").to_string(), - "commitMessage": env!("GIT_COMMIT_BODY").to_string(), - "branch": env!("GIT_BRANCH").to_string(), - "development": env!("DEVELOPMENT").to_lowercase(), - }); - - Ok(result) -} - -#[tauri::command] -pub fn is_development() -> Result { - Ok(is_development_enabled()) -} - -#[tauri::command] -pub fn open_data_folder() -> Result { - let path = DATA.root_dir.lock().unwrap().to_string_lossy().to_string(); - log_info!("Opening data folder at: {}", path); - - if let Err(e) = open::that(&path) { - log_error!("Failed to open data folder at {}: {}", path, e); - return Err(format!("Failed to open data folder: {e}")); - } - - Ok(path) -} - -#[tauri::command] -pub async fn reset_requirements() -> Result<(), String> { - if let Err(e) = DATA.reset_requirements().await { - log_error!("Failed to reset requirements: {}", e); - return Err(format!("Failed to reset requirements: {e}")); - } - log_info!("Client requirements reset successfully"); - Ok(()) -} - -#[tauri::command] -pub fn get_data_folder() -> Result { - let path = DATA.root_dir.lock().unwrap().to_string_lossy().to_string(); - // log_debug!("Getting data folder path: {}", path); - Ok(path) -} - -#[tauri::command] -pub async fn change_data_folder( - app: AppHandle, - new_path: String, - mode: String, - state: State<'_, AppState>, -) -> Result<(), String> { - log_info!( - "Changing data folder to '{}' with mode '{}'", - new_path, - mode - ); - let new_dir = PathBuf::from(new_path.clone()); - if new_dir.as_os_str().is_empty() { - log_warn!("Change data folder failed: Target path is empty"); - return Err("Target path is empty".to_string()); - } - - if !new_dir.exists() { - log_debug!( - "Target directory does not exist, creating it: {:?}", - new_dir - ); - fs_utils::ensure_dir(&new_dir).map_err(|e| { - log_error!("Failed to create target directory {:?}: {}", new_dir, e); - format!("Failed to create target dir: {e}") - })?; - } - - log_info!("Stopping all running clients before changing data folder"); - - let running: Vec = get_running_client_ids(state.clone()).await?; - - for id in running { - log_debug!("Stopping client with ID: {}", id); - let _ = stop_client(id, state.clone()).await; - } - - let running_custom: Vec = get_running_custom_client_ids().await; - for id in running_custom { - log_debug!("Stopping custom client with ID: {}", id); - let _ = stop_custom_client(id, state.clone()).await; - } - - let current_dir = DATA.root_dir.lock().unwrap().clone(); - log_debug!("Current data directory is: {:?}", current_dir); - - if mode == "move" { - log_info!("Moving data from old folder to new folder"); - if current_dir.exists() { - task::spawn_blocking(move || -> Result<(), String> { - log_debug!( - "Starting recursive copy from {:?} to {:?}", - current_dir, - new_dir - ); - fs_utils::copy_dir_recursive(¤t_dir, &new_dir, true)?; - log_debug!( - "Finished recursive copy. Removing old directory contents (except aci.json)." - ); - if current_dir.exists() { - if let Ok(entries) = fs::read_dir(¤t_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() - && path.file_name().and_then(|n| n.to_str()) == Some("aci.json") - { - continue; - } - let _ = fs_utils::remove_path(&path); - } - } - } - Ok(()) - }) - .await - .map_err(|e| { - log_error!("Task to move data folder failed: {}", e); - format!("Task join error: {e}") - })??; - } - } else if mode == "wipe" { - log_info!("Wiping old data folder (preserving aci.json)"); - if current_dir.exists() { - if let Ok(entries) = fs::read_dir(¤t_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() - && path.file_name().and_then(|n| n.to_str()) == Some("aci.json") - { - log_debug!("Preserving aci.json during wipe"); - continue; - } - let _ = fs_utils::remove_path(&path); - } - } - } - } else { - log_warn!("Invalid mode for changing data folder: {}", mode); - return Err("Invalid mode".to_string()); - } - - let roaming_dir = std::env::var("APPDATA") - .unwrap_or_else(|_| std::env::var("HOME").unwrap_or_else(|_| ".".to_string())); - let override_file = PathBuf::from(roaming_dir).join("CollapseLoaderRoot.txt"); - log_info!( - "Writing new data folder path to override file: {:?}", - override_file - ); - fs::write(&override_file, &new_path).map_err(|e| { - log_error!("Failed to write to override file: {:?}", e); - format!("Failed to write to override file: {e}") - })?; - - { - let mut root = DATA.root_dir.lock().unwrap(); - *root = PathBuf::from(new_path.clone()); - } - - let new_root = PathBuf::from(new_path.clone()); - - if let Ok(mut s) = SETTINGS.lock() { - s.config_path = new_root.join("config.json"); - log_debug!("Updated SETTINGS path: {:?}", s.config_path); - } - if let Ok(mut pm) = PRESET_MANAGER.lock() { - pm.config_path = new_root.join("presets.json"); - log_debug!("Updated PRESET_MANAGER path: {:?}", pm.config_path); - } - if let Ok(mut am) = ACCOUNT_MANAGER.lock() { - am.accounts_path = new_root.join("accounts.json"); - log_debug!("Updated ACCOUNT_MANAGER path: {:?}", am.accounts_path); - } - if let Ok(mut ccm) = CUSTOM_CLIENT_MANAGER.lock() { - ccm.custom_clients_path = new_root.join("custom_clients.json"); - log_debug!( - "Updated CUSTOM_CLIENT_MANAGER path: {:?}", - ccm.custom_clients_path - ); - } - if let Ok(mut fm) = FAVORITE_MANAGER.lock() { - fm.favorites_path = new_root.join("favorites.json"); - log_debug!("Updated FAVORITE_MANAGER path: {:?}", fm.favorites_path); - } - if let Ok(mut f) = FLAGS_MANAGER.lock() { - f.flags_path = new_root.join("flags.json"); - log_debug!("Updated FLAGS_MANAGER path: {:?}", f.flags_path); - } - - if let Some(window) = app.get_webview_window("main") { - log_debug!("Emitting 'data-folder-changed' event to main window"); - let _ = window.emit("data-folder-changed", &new_path); - } - - log_info!("Data folder change process completed successfully"); - Ok(()) -} - -#[tauri::command] -pub async fn get_api_url() -> Result { - SERVERS.wait_for_initial_check().await; - SERVERS.get_api_server_url().map_or_else( - || { - Ok(API_SERVERS - .first() - .map(|s| s.url.clone()) - .unwrap_or_default()) - }, - Ok, - ) -} - -#[tauri::command] -pub async fn get_cdn_url() -> Result { - SERVERS.wait_for_initial_check().await; - SERVERS.get_cdn_server_url().map_or_else( - || { - Ok(CDN_SERVERS - .first() - .map(|s| s.url.clone()) - .unwrap_or_default()) - }, - Ok, - ) -} - -// #[tauri::command] -// pub fn get_api_version() -> Result { -// Ok(API_VERSION.to_string()) -// } - -#[tauri::command] -pub async fn encode_base64(input: String) -> Result { - let encoded = general_purpose::STANDARD.encode(input); - Ok(encoded) -} - -#[tauri::command] -pub async fn decode_base64(input: String) -> Result { - general_purpose::STANDARD.decode(&input).ok().map_or_else( - || { - log_warn!("Failed to decode Base64 string"); - Err("Failed to decode base64".to_string()) - }, - |decoded| { - String::from_utf8(decoded).map_err(|e| { - log_warn!("Failed to convert decoded bytes to UTF-8 string: {}", e); - "Failed to decode base64 to UTF-8 string".to_string() - }) - }, - ) -} - -#[tauri::command] -pub fn update_presence(details: String, state: String) -> Result<(), String> { - log_debug!( - "Updating Discord presence: details='{}', state='{}'", - details, - state - ); - discord_rpc::update_activity_async(details, state); - Ok(()) -} - -#[tauri::command] -pub fn is_macos() -> bool { - cfg!(target_os = "macos") -} - -#[tauri::command] -pub fn set_window_theme(window: Window, theme: String) { - std::thread::spawn(move || { - let target_theme = match theme.as_str() { - "dark" => Some(Theme::Dark), - "light" => Some(Theme::Light), - _ => None, - }; - - if let Some(t) = target_theme { - let _ = window.set_theme(Some(t)); - } - }); -} - -#[tauri::command] -pub fn update_tray_menu(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { - use tauri::menu::PredefinedMenuItem; - use tauri::menu::{Menu, MenuItem}; - - #[allow(clippy::type_complexity)] - let (fav_clients, popular_clients): (Vec<(u32, String)>, Vec<(u32, String)>) = state - .clients - .manager - .lock() - .map(|m| { - let favorites = FAVORITE_MANAGER.lock().unwrap().favorites.clone(); - - let installed: Vec<_> = m - .clients - .iter() - .filter(|c| c.show && c.working && c.meta.installed) - .collect(); - - let mut favs = Vec::new(); - let mut others = Vec::new(); - - for c in installed { - let ver = c - .version - .replace('_', ".") - .trim_start_matches('V') - .to_string(); - if favorites.contains(&c.id) { - favs.push((c.id, format!("⭐ {} {}", c.name, ver))); - } else { - others.push(c); - } - } - - others.sort_by_key(|b| std::cmp::Reverse(b.launches)); - let popular: Vec<_> = others - .into_iter() - .take(10) - .map(|c| { - let ver = c - .version - .replace('_', ".") - .trim_start_matches('V') - .to_string(); - (c.id, format!("⚡ {} {}", c.name, ver)) - }) - .collect(); - - (favs, popular) - }) - .unwrap_or_default(); - - let show = MenuItem::with_id(&app, "show", "▶ Open CollapseLoader", true, None::<&str>) - .map_err(|e| e.to_string())?; - let quit = MenuItem::with_id(&app, "quit", "✕ Quit", true, None::<&str>) - .map_err(|e| e.to_string())?; - - let sep1 = PredefinedMenuItem::separator(&app).map_err(|e| e.to_string())?; - let sep2 = PredefinedMenuItem::separator(&app).map_err(|e| e.to_string())?; - let sep3 = PredefinedMenuItem::separator(&app).map_err(|e| e.to_string())?; - - let fav_items: Vec> = fav_clients - .iter() - .map(|(id, label)| { - MenuItem::with_id( - &app, - format!("launch_{id}"), - label.as_str(), - true, - None::<&str>, - ) - .expect("Failed to create client menu item") - }) - .collect(); - - let popular_items: Vec> = popular_clients - .iter() - .map(|(id, label)| { - MenuItem::with_id( - &app, - format!("launch_{id}"), - label.as_str(), - true, - None::<&str>, - ) - .expect("Failed to create client menu item") - }) - .collect(); - - let fav_header = MenuItem::with_id( - &app, - "_fav_header", - "── Favorited clients ──", - false, - None::<&str>, - ) - .map_err(|e| e.to_string())?; - - let popular_header = MenuItem::with_id( - &app, - "_popular_header", - "── Popular clients ──", - false, - None::<&str>, - ) - .map_err(|e| e.to_string())?; - - let mut item_refs: Vec<&dyn tauri::menu::IsMenuItem> = vec![&show, &sep1]; - - if fav_items.is_empty() && popular_items.is_empty() { - item_refs = vec![&show, &sep1, &quit]; - } else { - if !fav_items.is_empty() { - item_refs.push(&fav_header); - for item in &fav_items { - item_refs.push(item); - } - if !popular_items.is_empty() { - item_refs.push(&sep2); - } - } - - if !popular_items.is_empty() { - item_refs.push(&popular_header); - for item in &popular_items { - item_refs.push(item); - } - } - item_refs.push(&sep3); - item_refs.push(&quit); - } - - let new_menu = Menu::with_items(&app, &item_refs).map_err(|e| e.to_string())?; - - if let Some(tray) = app.tray_by_id("0").or_else(|| app.tray_by_id("main")) { - tray.set_menu(Some(new_menu)).map_err(|e| e.to_string())?; - } - - Ok(()) -} - -pub fn refresh_tray_menu(state: State<'_, AppState>) { - if let Some(app) = crate::core::storage::data::APP_HANDLE - .lock() - .unwrap() - .clone() - { - let _ = update_tray_menu(app, state); - } -} - -#[derive(serde::Serialize)] -pub struct StorageUsage { - pub clients: u64, - pub libraries: u64, - pub natives: u64, - pub assets: u64, - pub java: u64, - pub other: u64, - pub total: u64, -} - -pub(crate) fn dir_size(path: &std::path::Path) -> u64 { - if !path.exists() { - return 0; - } - let mut total = 0u64; - if let Ok(entries) = fs::read_dir(path) { - for entry in entries.flatten() { - let p = entry.path(); - if p.is_dir() { - total += dir_size(&p); - } else if let Ok(meta) = fs::metadata(&p) { - total += meta.len(); - } - } - } - total -} - -#[tauri::command] -pub async fn get_storage_usage() -> StorageUsage { - tokio::task::spawn_blocking(|| { - let root = DATA.root_dir.lock().unwrap().clone(); - - let libraries = dir_size(&root.join("libraries")) - + dir_size(&root.join("libraries-fabric")) - + dir_size(&root.join("libraries-legacy")); - - let natives = dir_size(&root.join("natives")) - + dir_size(&root.join("natives-macos-x64")) - + dir_size(&root.join("natives-macos-arm64")) - + dir_size(&root.join("natives-linux")) - + dir_size(&root.join("natives-legacy")) - + dir_size(&root.join("natives-legacy-linux")) - + dir_size(&root.join("natives-fabric")); - - let assets = dir_size(&root.join("assets")) + dir_size(&root.join("assets-fabric")); - - let mc_versions = dir_size(&root.join("minecraft-versions")); - let custom_clients_size = dir_size(&root.join("custom_clients")); - - let mut java = 0u64; - let mut client_folders = 0u64; - - if let Ok(entries) = fs::read_dir(&root) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_lowercase(); - let path = entry.path(); - - if !path.is_dir() { - continue; - } - - if name.starts_with("jdk") { - java += dir_size(&path); - } else if !fs_utils::SYSTEM_DIRS.contains(&name.as_str()) { - client_folders += dir_size(&path); - } - } - } - - let clients = mc_versions + custom_clients_size + client_folders; - let total = dir_size(&root); - let accounted = clients + libraries + natives + assets + java; - let other = total.saturating_sub(accounted); - - StorageUsage { - clients, - libraries, - natives, - assets, - java, - other, - total, - } - }) - .await - .unwrap_or(StorageUsage { - clients: 0, - libraries: 0, - natives: 0, - assets: 0, - java: 0, - other: 0, - total: 0, - }) -} - -#[tauri::command] -pub fn get_launch_history() -> Vec { - LAUNCH_HISTORY - .lock() - .map(|h| h.entries.clone()) - .unwrap_or_default() -} - -#[tauri::command] -pub fn clear_launch_history() -> Result<(), String> { - LAUNCH_HISTORY - .lock() - .map_err(|e| e.to_string()) - .map(|mut h| h.clear()) -} - -#[tauri::command] -pub fn record_launch( - client_id: u32, - client_name: String, - client_version: String, - account_name: Option, -) -> Result<(), String> { - let launched_at = chrono::Utc::now().to_rfc3339(); - let entry = LaunchEntry { - client_id, - client_name, - client_version, - launched_at, - account_name, - }; - LAUNCH_HISTORY - .lock() - .map_err(|e| e.to_string()) - .map(|mut h| h.record(entry)) -} diff --git a/src-tauri/src/commands/utils/data_folder.rs b/src-tauri/src/commands/utils/data_folder.rs new file mode 100644 index 00000000..c22a6351 --- /dev/null +++ b/src-tauri/src/commands/utils/data_folder.rs @@ -0,0 +1,178 @@ +use crate::core::utils::fs as fs_utils; +use crate::AppState; +use crate::{log_debug, log_error, log_info, log_warn}; +use std::{fs, path::PathBuf}; +use tauri::{AppHandle, Emitter, Manager, State}; +use tokio::task; + +use crate::commands::clients::{ + get_running_client_ids, get_running_custom_client_ids, stop_client, stop_custom_client, +}; + +#[tauri::command] +pub async fn change_data_folder( + app: AppHandle, + new_path: String, + mode: String, + state: State<'_, AppState>, +) -> Result<(), String> { + log_info!( + "Changing data folder to '{}' with mode '{}'", + new_path, + mode + ); + let new_dir = PathBuf::from(new_path.clone()); + if new_dir.as_os_str().is_empty() { + log_warn!("Change data folder failed: Target path is empty"); + return Err("Target path is empty".to_string()); + } + + if !new_dir.exists() { + log_debug!( + "Target directory does not exist, creating it: {:?}", + new_dir + ); + fs_utils::ensure_dir(&new_dir).map_err(|e| { + log_error!("Failed to create target directory {:?}: {}", new_dir, e); + format!("Failed to create target dir: {e}") + })?; + } + + log_info!("Stopping all running clients before changing data folder"); + + let running: Vec = get_running_client_ids(state.clone()).await?; + + for id in running { + log_debug!("Stopping client with ID: {}", id); + let _ = stop_client(id, state.clone()).await; + } + + let running_custom: Vec = get_running_custom_client_ids().await; + for id in running_custom { + log_debug!("Stopping custom client with ID: {}", id); + let _ = stop_custom_client(id, state.clone()).await; + } + + let current_dir = crate::core::storage::data::DATA + .root_dir + .lock() + .unwrap() + .clone(); + log_debug!("Current data directory is: {:?}", current_dir); + + if mode == "move" { + log_info!("Moving data from old folder to new folder"); + if current_dir.exists() { + task::spawn_blocking(move || -> Result<(), String> { + log_debug!( + "Starting recursive copy from {:?} to {:?}", + current_dir, + new_dir + ); + fs_utils::copy_dir_recursive(¤t_dir, &new_dir, true)?; + log_debug!( + "Finished recursive copy. Removing old directory contents (except aci.json)." + ); + if current_dir.exists() { + if let Ok(entries) = fs::read_dir(¤t_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() + && path.file_name().and_then(|n| n.to_str()) == Some("aci.json") + { + continue; + } + let _ = fs_utils::remove_path(&path); + } + } + } + Ok(()) + }) + .await + .map_err(|e| { + log_error!("Task to move data folder failed: {}", e); + format!("Task join error: {e}") + })??; + } + } else if mode == "wipe" { + log_info!("Wiping old data folder (preserving aci.json)"); + if current_dir.exists() { + if let Ok(entries) = fs::read_dir(¤t_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() + && path.file_name().and_then(|n| n.to_str()) == Some("aci.json") + { + log_debug!("Preserving aci.json during wipe"); + continue; + } + let _ = fs_utils::remove_path(&path); + } + } + } + } else { + log_warn!("Invalid mode for changing data folder: {}", mode); + return Err("Invalid mode".to_string()); + } + + let roaming_dir = std::env::var("APPDATA") + .unwrap_or_else(|_| std::env::var("HOME").unwrap_or_else(|_| ".".to_string())); + let override_file = PathBuf::from(roaming_dir).join("CollapseLoaderRoot.txt"); + log_info!( + "Writing new data folder path to override file: {:?}", + override_file + ); + fs::write(&override_file, &new_path).map_err(|e| { + log_error!("Failed to write to override file: {:?}", e); + format!("Failed to write to override file: {e}") + })?; + + { + let mut root = crate::core::storage::data::DATA.root_dir.lock().unwrap(); + *root = PathBuf::from(new_path.clone()); + } + + let new_root = PathBuf::from(new_path.clone()); + + { + let mut s = state.settings(); + s.config_path = new_root.join("config.json"); + log_debug!("Updated SETTINGS path: {:?}", s.config_path); + } + { + let mut pm = state.presets(); + pm.config_path = new_root.join("presets.json"); + log_debug!("Updated PRESET_MANAGER path: {:?}", pm.config_path); + } + { + let mut am = state.accounts(); + am.accounts_path = new_root.join("accounts.json"); + log_debug!("Updated ACCOUNT_MANAGER path: {:?}", am.accounts_path); + } + { + let mut ccm = state.custom_clients.lock(); + ccm.custom_clients_path = new_root.join("custom_clients.json"); + log_debug!( + "Updated CUSTOM_CLIENT_MANAGER path: {:?}", + ccm.custom_clients_path + ); + } + { + let mut fm = state.favorites(); + fm.favorites_path = new_root.join("favorites.json"); + log_debug!("Updated FAVORITE_MANAGER path: {:?}", fm.favorites_path); + } + { + let mut f = state.flags(); + f.flags_path = new_root.join("flags.json"); + log_debug!("Updated FLAGS_MANAGER path: {:?}", f.flags_path); + } + + if let Some(window) = app.get_webview_window("main") { + log_debug!("Emitting 'data-folder-changed' event to main window"); + let _ = window.emit("data-folder-changed", &new_path); + } + + log_info!("Data folder change process completed successfully"); + Ok(()) +} diff --git a/src-tauri/src/commands/utils/mod.rs b/src-tauri/src/commands/utils/mod.rs new file mode 100644 index 00000000..15eb5084 --- /dev/null +++ b/src-tauri/src/commands/utils/mod.rs @@ -0,0 +1,287 @@ +pub mod data_folder; +pub mod tray; + +pub use data_folder::*; +pub use tray::*; + +use base64::{engine::general_purpose, Engine}; + +use crate::core::storage::launch_history::LaunchEntry; +use crate::core::utils::discord_rpc; +use crate::core::utils::fs as fs_utils; +use crate::core::utils::globals::{API_SERVERS, CDN_SERVERS, CODENAME}; +use crate::core::utils::helpers::is_development_enabled; +use crate::core::{network::servers::SERVERS, storage::data::DATA}; +use crate::AppState; +use crate::{log_debug, log_error, log_info, log_warn}; +use std::fs; +use tauri::{State, Theme, Window}; + +#[tauri::command] +pub fn cancel_download(name: String) -> Result { + Ok(crate::core::network::downloader::cancel_download(&name)) +} + +#[tauri::command] +pub fn get_version() -> Result { + let result = serde_json::json!({ + "version": env!("CARGO_PKG_VERSION").to_string(), + "codename": CODENAME, + "commitHash": env!("GIT_HASH").to_string(), + "commitMessage": env!("GIT_COMMIT_BODY").to_string(), + "branch": env!("GIT_BRANCH").to_string(), + "development": env!("DEVELOPMENT").to_lowercase(), + }); + + Ok(result) +} + +#[tauri::command] +pub fn is_development() -> Result { + Ok(is_development_enabled()) +} + +#[tauri::command] +pub fn open_data_folder() -> Result { + let path = DATA.root_dir.lock().unwrap().to_string_lossy().to_string(); + log_info!("Opening data folder at: {}", path); + + if let Err(e) = open::that(&path) { + log_error!("Failed to open data folder at {}: {}", path, e); + return Err(format!("Failed to open data folder: {e}")); + } + + Ok(path) +} + +#[tauri::command] +pub async fn reset_requirements() -> Result<(), String> { + if let Err(e) = DATA.reset_requirements().await { + log_error!("Failed to reset requirements: {}", e); + return Err(format!("Failed to reset requirements: {e}")); + } + log_info!("Client requirements reset successfully"); + Ok(()) +} + +#[tauri::command] +pub fn get_data_folder() -> Result { + let path = DATA.root_dir.lock().unwrap().to_string_lossy().to_string(); + // log_debug!("Getting data folder path: {}", path); + Ok(path) +} + +#[tauri::command] +pub async fn get_api_url() -> Result { + SERVERS.wait_for_initial_check().await; + SERVERS.get_api_server_url().map_or_else( + || { + Ok(API_SERVERS + .first() + .map(|s| s.url.clone()) + .unwrap_or_default()) + }, + Ok, + ) +} + +#[tauri::command] +pub async fn get_cdn_url() -> Result { + SERVERS.wait_for_initial_check().await; + SERVERS.get_cdn_server_url().map_or_else( + || { + Ok(CDN_SERVERS + .first() + .map(|s| s.url.clone()) + .unwrap_or_default()) + }, + Ok, + ) +} + +// #[tauri::command] +// pub fn get_api_version() -> Result { +// Ok(API_VERSION.to_string()) +// } + +#[tauri::command] +pub async fn encode_base64(input: String) -> Result { + let encoded = general_purpose::STANDARD.encode(input); + Ok(encoded) +} + +#[tauri::command] +pub async fn decode_base64(input: String) -> Result { + general_purpose::STANDARD.decode(&input).ok().map_or_else( + || { + log_warn!("Failed to decode Base64 string"); + Err("Failed to decode base64".to_string()) + }, + |decoded| { + String::from_utf8(decoded).map_err(|e| { + log_warn!("Failed to convert decoded bytes to UTF-8 string: {}", e); + "Failed to decode base64 to UTF-8 string".to_string() + }) + }, + ) +} + +#[tauri::command] +pub fn update_presence(details: String, state: String) -> Result<(), String> { + log_debug!( + "Updating Discord presence: details='{}', state='{}'", + details, + state + ); + discord_rpc::update_activity_async(details, state); + Ok(()) +} + +#[tauri::command] +pub fn is_macos() -> bool { + cfg!(target_os = "macos") +} + +#[tauri::command] +pub fn set_window_theme(window: Window, theme: String) { + std::thread::spawn(move || { + let target_theme = match theme.as_str() { + "dark" => Some(Theme::Dark), + "light" => Some(Theme::Light), + _ => None, + }; + + if let Some(t) = target_theme { + let _ = window.set_theme(Some(t)); + } + }); +} + +#[derive(serde::Serialize)] +pub struct StorageUsage { + pub clients: u64, + pub libraries: u64, + pub natives: u64, + pub assets: u64, + pub java: u64, + pub other: u64, + pub total: u64, +} + +pub(crate) fn dir_size(path: &std::path::Path) -> u64 { + if !path.exists() { + return 0; + } + let mut total = 0u64; + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + let p = entry.path(); + if p.is_dir() { + total += dir_size(&p); + } else if let Ok(meta) = fs::metadata(&p) { + total += meta.len(); + } + } + } + total +} + +#[tauri::command] +pub async fn get_storage_usage() -> StorageUsage { + tokio::task::spawn_blocking(|| { + let root = DATA.root_dir.lock().unwrap().clone(); + + let libraries = dir_size(&root.join("libraries")) + + dir_size(&root.join("libraries-fabric")) + + dir_size(&root.join("libraries-legacy")); + + let natives = dir_size(&root.join("natives")) + + dir_size(&root.join("natives-macos-x64")) + + dir_size(&root.join("natives-macos-arm64")) + + dir_size(&root.join("natives-linux")) + + dir_size(&root.join("natives-legacy")) + + dir_size(&root.join("natives-legacy-linux")) + + dir_size(&root.join("natives-fabric")); + + let assets = dir_size(&root.join("assets")) + dir_size(&root.join("assets-fabric")); + + let mc_versions = dir_size(&root.join("minecraft-versions")); + let custom_clients_size = dir_size(&root.join("custom_clients")); + + let mut java = 0u64; + let mut client_folders = 0u64; + + if let Ok(entries) = fs::read_dir(&root) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_lowercase(); + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + if name.starts_with("jdk") { + java += dir_size(&path); + } else if !fs_utils::SYSTEM_DIRS.contains(&name.as_str()) { + client_folders += dir_size(&path); + } + } + } + + let clients = mc_versions + custom_clients_size + client_folders; + let total = dir_size(&root); + let accounted = clients + libraries + natives + assets + java; + let other = total.saturating_sub(accounted); + + StorageUsage { + clients, + libraries, + natives, + assets, + java, + other, + total, + } + }) + .await + .unwrap_or(StorageUsage { + clients: 0, + libraries: 0, + natives: 0, + assets: 0, + java: 0, + other: 0, + total: 0, + }) +} + +#[tauri::command] +pub fn get_launch_history(state: State<'_, AppState>) -> Vec { + state.launch_history().entries.clone() +} + +#[tauri::command] +pub fn clear_launch_history(state: State<'_, AppState>) -> Result<(), String> { + state.launch_history().clear(); + Ok(()) +} + +#[tauri::command] +pub fn record_launch( + state: State<'_, AppState>, + client_id: u32, + client_name: String, + client_version: String, + account_name: Option, +) -> Result<(), String> { + let launched_at = chrono::Utc::now().to_rfc3339(); + let entry = LaunchEntry { + client_id, + client_name, + client_version, + launched_at, + account_name, + }; + state.launch_history().record(entry); + Ok(()) +} diff --git a/src-tauri/src/commands/utils/tray.rs b/src-tauri/src/commands/utils/tray.rs new file mode 100644 index 00000000..d5e75ea9 --- /dev/null +++ b/src-tauri/src/commands/utils/tray.rs @@ -0,0 +1,154 @@ +use crate::AppState; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub fn update_tray_menu(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { + use tauri::menu::PredefinedMenuItem; + use tauri::menu::{Menu, MenuItem}; + + #[allow(clippy::type_complexity)] + let (fav_clients, popular_clients): (Vec<(u32, String)>, Vec<(u32, String)>) = state + .clients + .manager + .lock() + .map(|m| { + let favorites = state.favorites().favorites.clone(); + + let installed: Vec<_> = m + .clients + .iter() + .filter(|c| c.show && c.working && c.meta.installed) + .collect(); + + let mut favs = Vec::new(); + let mut others = Vec::new(); + + for c in installed { + let ver = c + .version + .replace('_', ".") + .trim_start_matches('V') + .to_string(); + if favorites.contains(&c.id) { + favs.push((c.id, format!("⭐ {} {}", c.name, ver))); + } else { + others.push(c); + } + } + + others.sort_by_key(|b| std::cmp::Reverse(b.launches)); + let popular: Vec<_> = others + .into_iter() + .take(10) + .map(|c| { + let ver = c + .version + .replace('_', ".") + .trim_start_matches('V') + .to_string(); + (c.id, format!("⚡ {} {}", c.name, ver)) + }) + .collect(); + + (favs, popular) + }) + .unwrap_or_default(); + + let show = MenuItem::with_id(&app, "show", "▶ Open CollapseLoader", true, None::<&str>) + .map_err(|e| e.to_string())?; + let quit = MenuItem::with_id(&app, "quit", "✕ Quit", true, None::<&str>) + .map_err(|e| e.to_string())?; + + let sep1 = PredefinedMenuItem::separator(&app).map_err(|e| e.to_string())?; + let sep2 = PredefinedMenuItem::separator(&app).map_err(|e| e.to_string())?; + let sep3 = PredefinedMenuItem::separator(&app).map_err(|e| e.to_string())?; + + let fav_items: Vec> = fav_clients + .iter() + .map(|(id, label)| { + MenuItem::with_id( + &app, + format!("launch_{id}"), + label.as_str(), + true, + None::<&str>, + ) + .expect("Failed to create client menu item") + }) + .collect(); + + let popular_items: Vec> = popular_clients + .iter() + .map(|(id, label)| { + MenuItem::with_id( + &app, + format!("launch_{id}"), + label.as_str(), + true, + None::<&str>, + ) + .expect("Failed to create client menu item") + }) + .collect(); + + let fav_header = MenuItem::with_id( + &app, + "_fav_header", + "── Favorited clients ──", + false, + None::<&str>, + ) + .map_err(|e| e.to_string())?; + + let popular_header = MenuItem::with_id( + &app, + "_popular_header", + "── Popular clients ──", + false, + None::<&str>, + ) + .map_err(|e| e.to_string())?; + + let mut item_refs: Vec<&dyn tauri::menu::IsMenuItem> = vec![&show, &sep1]; + + if fav_items.is_empty() && popular_items.is_empty() { + item_refs = vec![&show, &sep1, &quit]; + } else { + if !fav_items.is_empty() { + item_refs.push(&fav_header); + for item in &fav_items { + item_refs.push(item); + } + if !popular_items.is_empty() { + item_refs.push(&sep2); + } + } + + if !popular_items.is_empty() { + item_refs.push(&popular_header); + for item in &popular_items { + item_refs.push(item); + } + } + item_refs.push(&sep3); + item_refs.push(&quit); + } + + let new_menu = Menu::with_items(&app, &item_refs).map_err(|e| e.to_string())?; + + if let Some(tray) = app.tray_by_id("0").or_else(|| app.tray_by_id("main")) { + tray.set_menu(Some(new_menu)).map_err(|e| e.to_string())?; + } + + Ok(()) +} + +pub fn refresh_tray_menu(state: State<'_, AppState>) { + if let Some(app) = crate::core::storage::data::APP_HANDLE + .lock() + .unwrap() + .clone() + { + let _ = update_tray_menu(app, state); + } +} diff --git a/src-tauri/src/core/app_runtime.rs b/src-tauri/src/core/app_runtime.rs index 5af1a8e8..01c63fe2 100644 --- a/src-tauri/src/core/app_runtime.rs +++ b/src-tauri/src/core/app_runtime.rs @@ -23,6 +23,8 @@ impl StartupRuntime { check_webkit_environment()?; } + crate::core::clients::client::requirements::ensure_jdk_permissions_on_startup(); + Ok(()) } } diff --git a/src-tauri/src/core/clients/client.rs b/src-tauri/src/core/clients/client.rs index 9520858b..5f72eb84 100644 --- a/src-tauri/src/core/clients/client.rs +++ b/src-tauri/src/core/clients/client.rs @@ -15,14 +15,18 @@ use crate::core::storage::data::{Data, DATA}; use crate::core::utils::{ globals::{ CUSTOM_CLIENTS_FOLDER, FILE_EXTENSION, IS_LINUX, IS_MACOS, IS_WINDOWS, JDK21_FOLDER, - JDK8_FOLDER, MINECRAFT_VERSIONS_FOLDER, MODS_FOLDER, + JDK8_FOLDER, LIBRARIES_LEGACY_FOLDER, LIBRARIES_LEGACY_ZIP, MINECRAFT_VERSIONS_FOLDER, + MODS_FOLDER, LIBRARIES_VA1_8_9_FOLDER, LIBRARIES_VA1_8_9_VIA511_FOLDER, + LIBRARIES_VA1_8_9_VIA511_ZIP, LIBRARIES_VA1_8_9_VIA53_FOLDER, + LIBRARIES_VA1_8_9_VIA53_ZIP, LIBRARIES_VA1_8_9_VIA57_FOLDER, + LIBRARIES_VA1_8_9_VIA57_ZIP, LIBRARIES_VA1_8_9_ZIP, }, process, }; use crate::{log_error, log_info}; mod launch; -mod requirements; +pub(crate) mod requirements; pub static CLIENT_LOGS: std::sync::LazyLock>>> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new())); @@ -135,10 +139,18 @@ pub struct Meta { pub installed: bool, pub is_custom: bool, pub size: u64, + pub viaversion: Option, + pub java_version: Option, } impl Meta { - pub fn new(version: &str, filename: &str, client_type: &ClientType) -> Self { + pub fn new( + version: &str, + filename: &str, + client_type: &ClientType, + viaversion: Option, + java_version: Option, + ) -> Self { let semver = Version::parse(version).unwrap_or_else(|err| { log_error!("Failed to parse version '{}': {}", version, err); Version::new(1, 16, 5) @@ -193,6 +205,8 @@ impl Meta { is_fabric, is_forge, size: 0, + viaversion, + java_version, } } } @@ -240,6 +254,14 @@ pub struct Client { pub java_path: Option, #[serde(default)] pub java_args: Option, + #[serde(default)] + pub libraries_path: Option, + #[serde(default)] + pub natives_path: Option, + #[serde(default)] + pub viaversion: Option, + #[serde(default)] + pub java_version: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -258,6 +280,8 @@ fn default_meta() -> Meta { installed: false, is_custom: false, size: 0, + viaversion: None, + java_version: None, } } @@ -287,6 +311,55 @@ impl Client { semver.major == 1 && semver.minor <= 12 } + fn uses_sub_libraries(&self) -> bool { + if !self.is_legacy_client() { + return false; + } + if self.client_type == ClientType::Forge || self.client_type == ClientType::Fabric { + return false; + } + match Version::parse(&self.version) { + Ok(v) => v.major == 1 && v.minor == 8 && v.patch == 9, + Err(_) => self.version == "1.8.9", + } + } + + fn via_version_normalized(&self) -> String { + let v = self.meta.viaversion.as_deref().unwrap_or("5.9.1"); + match v { + "5.3.0" | "5.7.1" | "5.9.1" | "5.11.0" => v.to_string(), + _ => "5.9.1".to_string(), + } + } + + fn sub_libraries_folder(&self) -> &'static str { + if self.uses_sub_libraries() { + match self.via_version_normalized().as_str() { + "5.3.0" => LIBRARIES_VA1_8_9_VIA53_FOLDER, + "5.7.1" => LIBRARIES_VA1_8_9_VIA57_FOLDER, + "5.9.1" => LIBRARIES_VA1_8_9_FOLDER, + "5.11.0" => LIBRARIES_VA1_8_9_VIA511_FOLDER, + _ => LIBRARIES_VA1_8_9_FOLDER, + } + } else { + LIBRARIES_LEGACY_FOLDER + } + } + + fn sub_libraries_zip(&self) -> &'static str { + if self.uses_sub_libraries() { + match self.via_version_normalized().as_str() { + "5.3.0" => LIBRARIES_VA1_8_9_VIA53_ZIP, + "5.7.1" => LIBRARIES_VA1_8_9_VIA57_ZIP, + "5.9.1" => LIBRARIES_VA1_8_9_ZIP, + "5.11.0" => LIBRARIES_VA1_8_9_VIA511_ZIP, + _ => LIBRARIES_VA1_8_9_ZIP, + } + } else { + LIBRARIES_LEGACY_ZIP + } + } + fn client_base_folder(&self) -> PathBuf { let root = DATA.root_dir.lock().unwrap(); @@ -304,8 +377,18 @@ impl Client { .unwrap_or(&self.filename) } + fn wants_jdk8(&self) -> bool { + if let Some(v) = self.meta.java_version.as_deref() { + return v == "8"; + } + if self.client_type == ClientType::Forge || self.is_legacy_client() { + return true; + } + false + } + fn jdk_folder_name(&self) -> &'static str { - if self.client_type == ClientType::Forge { + if self.wants_jdk8() { JDK8_FOLDER } else { JDK21_FOLDER @@ -313,7 +396,7 @@ impl Client { } fn jdk_zip_name(&self) -> String { - if self.client_type == ClientType::Forge { + if self.wants_jdk8() { format!("misc/{JDK8_FOLDER}.zip") } else { format!("misc/{JDK21_FOLDER}.zip") diff --git a/src-tauri/src/core/clients/client/launch.rs b/src-tauri/src/core/clients/client/launch.rs index a7755941..4081235f 100644 --- a/src-tauri/src/core/clients/client/launch.rs +++ b/src-tauri/src/core/clients/client/launch.rs @@ -4,6 +4,10 @@ use std::{ sync::{Arc, Mutex}, }; +#[cfg(unix)] +#[allow(unused_imports)] +use std::os::unix::process::CommandExt; + use tokio::{ io::{AsyncBufReadExt, BufReader}, process::Command, @@ -14,7 +18,12 @@ use super::{add_log_line, Client, ClientType, LaunchOptions, CLIENT_LOGS}; #[allow(unused)] use crate::core::{ clients::{ - internal::agent_overlay::AgentArguments, log_checker::LogChecker, manager::ClientManager, + internal::{ + agent_overlay::AgentArguments, + titlebar_branding::TitlebarBrandingManager, + }, + log_checker::LogChecker, + manager::ClientManager, }, network::{analytics::Analytics, server_ads}, storage::{accounts::ACCOUNT_MANAGER, data::DATA, settings::SETTINGS}, @@ -23,17 +32,30 @@ use crate::core::{ AGENT_FILE, AGENT_OVERLAY_FOLDER, ARM64_SUFFIX, ASSETS_FABRIC_FOLDER, ASSETS_FOLDER, IS_AARCH64, IS_LINUX, IS_MACOS, IS_WINDOWS, LEGACY_SUFFIX, LINUX_SUFFIX, MACOS_SUFFIX, NATIVES_FOLDER, NATIVES_LEGACY_LINUX_FOLDER, NATIVES_MACOS_ARM64_FOLDER, - NATIVES_MACOS_FOLDER, PATH_SEPARATOR, + NATIVES_MACOS_FOLDER, PATH_SEPARATOR, SKIP_TITLEBAR_BRANDING, + NATIVES_VA1_8_9_LINUX_FOLDER, NATIVES_VA1_8_9_MACOS_FOLDER, + NATIVES_VA1_8_9_WINDOWS_FOLDER, TITLEBAR_FILE, }, helpers::emit_to_main_window, process::force_high_performance_gpu, }, }; -use crate::{log_debug, log_error, log_info}; +use crate::{log_debug, log_error, log_info, log_warn}; impl Client { + #[cfg(target_os = "linux")] + fn has_nvidia_gpu() -> bool { + std::process::Command::new("lspci") + .output() + .map(|o| { + let stdout = String::from_utf8_lossy(&o.stdout); + stdout.to_lowercase().contains("nvidia") + }) + .unwrap_or(false) + } + fn append_new_instance_separator(&self) { - let mut logs = CLIENT_LOGS.lock().unwrap(); + let mut logs = CLIENT_LOGS.lock().unwrap_or_else(|e| e.into_inner()); let client_logs = logs.entry(self.id).or_default(); if !client_logs.is_empty() { client_logs.push("-------------------------------------------".to_string()); @@ -65,7 +87,7 @@ impl Client { } fn resolve_assets_dir(&self) -> PathBuf { - let root = DATA.root_dir.lock().unwrap(); + let root = DATA.root_dir.lock().unwrap_or_else(|e| e.into_inner()); if self.client_type == ClientType::Fabric { root.join(ASSETS_FABRIC_FOLDER) } else { @@ -106,24 +128,42 @@ impl Client { } fn resolve_natives_path(&self) -> PathBuf { - let root = DATA.root_dir.lock().unwrap(); + if let Some(path) = self + .natives_path + .as_deref() + .filter(|p| !p.trim().is_empty()) + { + return PathBuf::from(path); + } + + let root = DATA.root_dir.lock().unwrap_or_else(|e| e.into_inner()); let use_legacy_layout = self.is_legacy_client() || (!self.meta.is_new && IS_WINDOWS); if IS_LINUX { - if self.is_legacy_client() { + if self.uses_sub_libraries() { + root.join(NATIVES_VA1_8_9_LINUX_FOLDER) + } else if self.is_legacy_client() { root.join(NATIVES_LEGACY_LINUX_FOLDER) } else { Self::resolve_linux_natives_path(&root) } } else if IS_MACOS { - Self::resolve_macos_natives_path(&root, use_legacy_layout) + if self.uses_sub_libraries() { + root.join(NATIVES_VA1_8_9_MACOS_FOLDER) + } else { + Self::resolve_macos_natives_path(&root, use_legacy_layout) + } } else { - Self::resolve_default_natives_path(&root, use_legacy_layout) + if self.uses_sub_libraries() { + root.join(NATIVES_VA1_8_9_WINDOWS_FOLDER) + } else { + Self::resolve_default_natives_path(&root, use_legacy_layout) + } } } fn get_launch_settings(&self) -> (bool, bool, String, u32) { - let s = SETTINGS.lock().unwrap(); + let s = SETTINGS.lock().unwrap_or_else(|e| e.into_inner()); ( s.optional_telemetry.value, s.irc_chat.value, @@ -251,6 +291,22 @@ impl Client { let assets_dir = self.resolve_assets_dir(); let natives_path = self.resolve_natives_path(); + let is_legacy_vanilla_for_natives = self.client_type == ClientType::Default + && self.is_legacy_client(); + if is_legacy_vanilla_for_natives { + let natives_link = client_folder.join("natives"); + if !natives_link.exists() { + #[cfg(unix)] + { + let _ = std::os::unix::fs::symlink(&natives_path, &natives_link); + } + #[cfg(windows)] + { + let _ = std::os::windows::fs::symlink_dir(&natives_path, &natives_link); + } + } + } + let classpath = self.build_classpath()?; let (analytics, irc, lang, ram_mb) = self.get_launch_settings(); @@ -269,7 +325,39 @@ impl Client { lang, ); - let agent_overlay_path = DATA.root_dir.lock().unwrap().join(AGENT_OVERLAY_FOLDER); + let agent_overlay_path = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(AGENT_OVERLAY_FOLDER); + + let is_legacy_vanilla = self.client_type == ClientType::Default && !self.meta.is_new; + + let should_apply_titlebar = !*SKIP_TITLEBAR_BRANDING + && !self.meta.is_custom + && self.client_type != ClientType::Forge + && !is_legacy_vanilla + && !TitlebarBrandingManager::has_branding_in_jar(&client_folder.join(&self.filename)); + + if should_apply_titlebar { + let titlebar_path = agent_overlay_path.join(TITLEBAR_FILE); + if titlebar_path.exists() { + log_info!( + "Titlebar branding will be applied for client: {}", + self.name + ); + } else { + log_debug!( + "Titlebar branding file not found, skipping for: {}", + self.name + ); + } + } else if !*SKIP_TITLEBAR_BRANDING && !self.meta.is_custom { + log_info!( + "Skipping titlebar branding for {} (already branded or excluded)", + self.name + ); + } let mut cmd = Command::new(java_bin); @@ -278,22 +366,101 @@ impl Client { cmd.current_dir(&client_folder); - cmd.arg("-Xverify:none"); + if self.wants_jdk8() { + cmd.arg("-Xverify:none"); + } + + if self.wants_jdk8() && self.client_type == ClientType::Default { + cmd.arg("-XX:+UseG1GC"); + cmd.arg("-XX:MaxPermSize=256m"); + cmd.arg("-Dorg.lwjgl.system.stacksize=16384"); + cmd.arg("-Dorg.lwjgl.system.nojni=false"); + } #[cfg(target_os = "macos")] cmd.arg("-XstartOnFirstThread"); #[cfg(target_os = "linux")] - { + if Self::has_nvidia_gpu() { cmd.env("__NV_PRIME_RENDER_OFFLOAD", "1"); cmd.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); cmd.env("__VK_LAYER_NV_optimus", "NVIDIA_only"); cmd.env("DRI_PRIME", "1"); } - let is_legacy_vanilla = self.client_type == ClientType::Default && !self.meta.is_new; + if self.is_legacy_client() && self.client_type == ClientType::Default { + #[cfg(target_os = "linux")] + { + cmd.env("MALLOC_TRIM_THRESHOLD_", "131072"); + cmd.env("MALLOC_TOP_PAD_", "131072"); + cmd.env("MALLOC_MMAP_THRESHOLD_", "131072"); + cmd.env("MALLOC_ARENA_MAX", "1"); + } + } + + if !should_apply_titlebar { + cmd.env("COLLAPSE_SKIP_TITLEBAR", "1"); + } + + #[cfg(target_os = "linux")] + { + if is_legacy_vanilla { + let jemalloc_path = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join("natives-linux") + .join("libjemalloc.so"); + if jemalloc_path.exists() { + cmd.env("LD_PRELOAD", &jemalloc_path); + cmd.env( + "MALLOC_CONF", + "background_thread:true,metadata_thp:auto,dirty_decay_ms:9000000000,muzzy_decay_ms:9000000000", + ); + log_info!( + "Loaded libjemalloc.so via LD_PRELOAD for legacy vanilla client: {}", + self.name + ); + } else { + log_warn!("libjemalloc.so not found at {}", jemalloc_path.display()); + } + } + } + + #[cfg(target_os = "linux")] + if should_apply_titlebar && agent_overlay_path.join(TITLEBAR_FILE).exists() { + let jemalloc_path = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join("natives-linux") + .join("libjemalloc.so"); + let titlebar = agent_overlay_path.join(TITLEBAR_FILE); + let preload = if jemalloc_path.exists() { + format!("{}:{}", jemalloc_path.display(), titlebar.display()) + } else { + titlebar.display().to_string() + }; + cmd.env("LD_PRELOAD", &preload); + } + + #[cfg(target_os = "windows")] + if should_apply_titlebar { + let titlebar_path = agent_overlay_path.join(TITLEBAR_FILE); + if titlebar_path.exists() { + cmd.arg(format!("-agentpath:{}", titlebar_path.display())); + } + } + + #[cfg(target_os = "macos")] + if should_apply_titlebar { + let titlebar_path = agent_overlay_path.join(TITLEBAR_FILE); + if titlebar_path.exists() { + cmd.arg(format!("-agentpath:{}", titlebar_path.display())); + } + } - if self.client_type != ClientType::Forge && !is_legacy_vanilla { + if !self.meta.is_custom && self.client_type != ClientType::Forge && !is_legacy_vanilla { cmd.arg(format!( "-javaagent:{}={}", agent_overlay_path.join(AGENT_FILE).display(), @@ -303,13 +470,34 @@ impl Client { self.apply_java_args(&mut cmd); + if is_legacy_vanilla && self.wants_jdk8() { + cmd.arg("-XX:+UseG1GC"); + cmd.arg("-Dorg.lwjgl.system.stacksize=16384"); + cmd.arg("-Dorg.lwjgl.system.nojni=false"); + #[cfg(target_os = "linux")] + { + cmd.arg("-Dcom.sun.java.util.jar.disableSHA1=true"); + } + } + + if is_legacy_vanilla && self.wants_jdk8() { + cmd.arg("-Xss256k"); + } + cmd.arg(format!("-Xmx{ram_mb}M")); - cmd.arg(format!( - "-Djava.library.path={}{}{}", - natives_path.display(), - PATH_SEPARATOR, - agent_overlay_path.display() - )); + if self.meta.is_custom { + cmd.arg(format!( + "-Djava.library.path={}", + natives_path.display(), + )); + } else { + cmd.arg(format!( + "-Djava.library.path={}{}{}", + natives_path.display(), + PATH_SEPARATOR, + agent_overlay_path.display() + )); + } let actual_main_class = if self.client_type == ClientType::Forge { "net.minecraft.launchwrapper.Launch".to_string() @@ -330,13 +518,26 @@ impl Client { cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); - // PROJECT CLOSED - //let servers_dat_path = client_folder.join("servers.dat"); - //let ads = server_ads::fetch_server_ads().await; - //server_ads::inject_servers_dat(&servers_dat_path, &ads); + let servers_dat_path = client_folder.join("servers.dat"); + let server_result = server_ads::fetch_server_ads().await; + server_ads::inject_servers_dat(&servers_dat_path, &server_result); log_debug!("Spawning client process: {}", self.name); + #[cfg(target_os = "linux")] + { + lower_nofile_in_parent(); + let is_legacy = self.is_legacy_client(); + unsafe { + cmd.pre_exec(move || { + if is_legacy { + lower_nofile_in_parent(); + } + Ok(()) + }); + } + } + let mut child = cmd .spawn() .map_err(|e| format!("Failed to spawn process: {e}"))?; @@ -450,4 +651,117 @@ impl Client { }), ); } + + #[cfg(target_os = "linux")] + pub fn lower_nofile_in_child() { + use std::os::raw::{c_int, c_ulong}; + + const RLIMIT_NOFILE: c_int = 7; + const TARGET_SOFT: c_ulong = 8192; + + #[repr(C)] + struct Rlimit { + rlim_cur: c_ulong, + rlim_max: c_ulong, + } + + extern "C" { + fn prlimit( + pid: c_int, + resource: c_int, + new_limit: *const Rlimit, + old_limit: *mut Rlimit, + ) -> c_int; + } + + let mut current = Rlimit { rlim_cur: 0, rlim_max: 0 }; + let ret = unsafe { + prlimit(0, RLIMIT_NOFILE, std::ptr::null(), &mut current) + }; + if ret != 0 { + return; + } + + if current.rlim_cur > TARGET_SOFT || current.rlim_max > TARGET_SOFT { + let target = if TARGET_SOFT < current.rlim_max { + TARGET_SOFT + } else { + current.rlim_max + }; + let new_limit = Rlimit { + rlim_cur: target, + rlim_max: target, + }; + unsafe { + prlimit(0, RLIMIT_NOFILE, &new_limit, std::ptr::null_mut()); + } + } + } +} + +#[cfg(target_os = "linux")] +pub fn lower_nofile_in_parent() { + use std::os::raw::{c_int, c_ulong}; + + const RLIMIT_NOFILE: c_int = 7; + const TARGET_SOFT: c_ulong = 8192; + + #[repr(C)] + struct Rlimit { + rlim_cur: c_ulong, + rlim_max: c_ulong, + } + + extern "C" { + fn prlimit( + pid: c_int, + resource: c_int, + new_limit: *const Rlimit, + old_limit: *mut Rlimit, + ) -> c_int; + } + + let mut current = Rlimit { rlim_cur: 0, rlim_max: 0 }; + let ret = unsafe { + prlimit(0, RLIMIT_NOFILE, std::ptr::null(), &mut current) + }; + if ret != 0 { + log_warn!("Failed to read RLIMIT_NOFILE"); + return; + } + + log_debug!( + "Current RLIMIT_NOFILE: cur={} max={}", + current.rlim_cur, + current.rlim_max + ); + + if current.rlim_cur > TARGET_SOFT || current.rlim_max > TARGET_SOFT { + let target = if TARGET_SOFT < current.rlim_max { + TARGET_SOFT + } else { + current.rlim_max + }; + let new_limit = Rlimit { + rlim_cur: target, + rlim_max: target, + }; + let ret = unsafe { + prlimit(0, RLIMIT_NOFILE, &new_limit, std::ptr::null_mut()) + }; + if ret == 0 { + log_info!( + "Lowered RLIMIT_NOFILE from {}/{} to {}/{} for legacy client", + current.rlim_cur, + current.rlim_max, + target, + target + ); + } else { + log_warn!( + "Failed to set RLIMIT_NOFILE: {}", + std::io::Error::last_os_error() + ); + } + } } diff --git a/src-tauri/src/core/clients/client/requirements.rs b/src-tauri/src/core/clients/client/requirements.rs index dc428193..c93af594 100644 --- a/src-tauri/src/core/clients/client/requirements.rs +++ b/src-tauri/src/core/clients/client/requirements.rs @@ -1,4 +1,4 @@ -use std::path::{Path, MAIN_SEPARATOR}; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use std::sync::LazyLock; use std::time::Duration; @@ -23,6 +23,9 @@ use crate::core::utils::globals::{ NATIVES_LEGACY_LINUX_FOLDER, NATIVES_LEGACY_LINUX_ZIP, NATIVES_LEGACY_ZIP, NATIVES_LINUX_FOLDER, NATIVES_LINUX_ZIP, NATIVES_MACOS_ARM64_FOLDER, NATIVES_MACOS_ARM64_ZIP, NATIVES_MACOS_FOLDER, NATIVES_MACOS_ZIP, NATIVES_ZIP, PATH_SEPARATOR, + NATIVES_VA1_8_9_LINUX_ZIP, NATIVES_VA1_8_9_MACOS_ZIP, NATIVES_VA1_8_9_WINDOWS_ZIP, + NATIVES_VA1_8_9_LINUX_FOLDER, NATIVES_VA1_8_9_MACOS_FOLDER, + NATIVES_VA1_8_9_WINDOWS_FOLDER, }; use crate::core::utils::{hashing::calculate_md5_hash, helpers::emit_to_main_window}; use crate::{log_debug, log_error, log_info, log_warn}; @@ -48,7 +51,9 @@ struct RequirementsDownloadStateGuard<'a> { impl<'a> RequirementsDownloadStateGuard<'a> { fn activate(app_handle: &'a AppHandle) -> Self { { - let mut downloading = REQUIREMENTS_DOWNLOADING.lock().unwrap(); + let mut downloading = REQUIREMENTS_DOWNLOADING + .lock() + .unwrap_or_else(|e| e.into_inner()); *downloading = true; } emit_to_main_window(app_handle, "requirements-status", true); @@ -59,7 +64,9 @@ impl<'a> RequirementsDownloadStateGuard<'a> { impl Drop for RequirementsDownloadStateGuard<'_> { fn drop(&mut self) { { - let mut downloading = REQUIREMENTS_DOWNLOADING.lock().unwrap(); + let mut downloading = REQUIREMENTS_DOWNLOADING + .lock() + .unwrap_or_else(|e| e.into_inner()); *downloading = false; } emit_to_main_window(self.app_handle, "requirements-status", false); @@ -455,7 +462,11 @@ impl Client { || folder == JDK8_FOLDER || folder == JDK21_FOLDER { - let path = DATA.root_dir.lock().unwrap().join(folder); + let path = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(folder); if !path.exists() { log_info!("Folder '{}' missing. Queuing {} for download.", folder, zip); files_to_download.push(zip.to_string()); @@ -468,7 +479,11 @@ impl Client { "Integrity check failed for '{}'. Wiping folder for clean redownload.", folder ); - let path = DATA.root_dir.lock().unwrap().join(folder); + let path = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(folder); if path.exists() { let _ = std::fs::remove_dir_all(&path); } @@ -488,12 +503,21 @@ impl Client { &self, ) -> (&'static str, &'static str, &'static str, &'static str) { if self.is_legacy_client() { - ( - LIBRARIES_LEGACY_ZIP, - LIBRARIES_LEGACY_FOLDER, - NATIVES_LEGACY_ZIP, - NATIVES_LEGACY_FOLDER, - ) + if self.uses_sub_libraries() { + ( + self.sub_libraries_zip(), + self.sub_libraries_folder(), + NATIVES_VA1_8_9_WINDOWS_ZIP, + NATIVES_VA1_8_9_WINDOWS_FOLDER, + ) + } else { + ( + LIBRARIES_LEGACY_ZIP, + LIBRARIES_LEGACY_FOLDER, + NATIVES_LEGACY_ZIP, + NATIVES_LEGACY_FOLDER, + ) + } } else { (LIBRARIES_ZIP, LIBRARIES_FOLDER, NATIVES_ZIP, NATIVES_FOLDER) } @@ -505,13 +529,17 @@ impl Client { natives_folder: &'static str, ) -> (&'static str, &'static str) { if IS_LINUX { - if self.is_legacy_client() { + if self.uses_sub_libraries() { + (NATIVES_VA1_8_9_LINUX_ZIP, NATIVES_VA1_8_9_LINUX_FOLDER) + } else if self.is_legacy_client() { (NATIVES_LEGACY_LINUX_ZIP, NATIVES_LEGACY_LINUX_FOLDER) } else { (NATIVES_LINUX_ZIP, NATIVES_LINUX_FOLDER) } } else if IS_MACOS { - if IS_AARCH64 { + if self.uses_sub_libraries() { + (NATIVES_VA1_8_9_MACOS_ZIP, NATIVES_VA1_8_9_MACOS_FOLDER) + } else if IS_AARCH64 { (NATIVES_MACOS_ARM64_ZIP, NATIVES_MACOS_ARM64_FOLDER) } else { (NATIVES_MACOS_ZIP, NATIVES_MACOS_FOLDER) @@ -571,7 +599,7 @@ impl Client { let local_path = DATA .root_dir .lock() - .unwrap() + .unwrap_or_else(|e| e.into_inner()) .join(MINECRAFT_VERSIONS_FOLDER) .join(dest_filename); @@ -616,9 +644,10 @@ impl Client { let _state_guard = RequirementsDownloadStateGuard::activate(app_handle); let needs_java_permission_fix = (IS_LINUX || IS_MACOS) - && files - .iter() - .any(|file| file.starts_with(self.jdk_folder_name())); + && files.iter().any(|file| { + let folder = self.jdk_folder_name(); + file.starts_with(folder) || file.contains(&format!("/{folder}")) + }); let downloads = files.into_iter().map(|file| async move { log_info!("Downloading requirement: {}", file); @@ -647,38 +676,21 @@ impl Client { fn fix_java_permissions(&self) { #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; - let bin_dir = DATA + let jdk_root = DATA .root_dir .lock() - .unwrap() - .join(self.jdk_folder_name()) - .join("bin"); - if bin_dir.exists() { - if let Ok(entries) = std::fs::read_dir(&bin_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() { - if let Ok(mut perms) = std::fs::metadata(&path).map(|m| m.permissions()) - { - perms.set_mode(0o755); - if let Err(e) = std::fs::set_permissions(&path, perms) { - log_warn!( - "Failed to set exec perm on {}: {}", - path.display(), - e - ); - } - } - } - } - } - } + .unwrap_or_else(|e| e.into_inner()) + .join(self.jdk_folder_name()); + set_exec_bit_recursive(&jdk_root); } } fn clean_fabric_libraries(&self) { - let fabric_libs_dir = DATA.root_dir.lock().unwrap().join(LIBRARIES_FABRIC_FOLDER); + let fabric_libs_dir = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FABRIC_FOLDER); if !fabric_libs_dir.exists() { return; @@ -733,7 +745,11 @@ impl Client { } async fn ensure_fabric_libraries(&self) -> Result<(), String> { - let common_dir = DATA.root_dir.lock().unwrap().join(LIBRARIES_FABRIC_FOLDER); + let common_dir = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FABRIC_FOLDER); let need_common = !dir_has_any_jars(&common_dir, true); @@ -743,12 +759,16 @@ impl Client { sanitize_version_for_paths(&self.version) ); - let versioned_dir = DATA.root_dir.lock().unwrap().join( - versioned_zip - .strip_prefix("misc/") - .unwrap_or(&versioned_zip) - .replace(".zip", ""), - ); + let versioned_dir = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join( + versioned_zip + .strip_prefix("misc/") + .unwrap_or(&versioned_zip) + .replace(".zip", ""), + ); let need_versioned = !dir_has_any_jars(&versioned_dir, false); let mut downloads: Vec>> = @@ -782,7 +802,11 @@ impl Client { } async fn ensure_slf4j(&self) -> Result<(), String> { - let fabric_libs_dir = DATA.root_dir.lock().unwrap().join(LIBRARIES_FABRIC_FOLDER); + let fabric_libs_dir = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FABRIC_FOLDER); let already_present = { let mut found = false; @@ -846,9 +870,17 @@ impl Client { log_warn!("Java executable missing. Redownloading requirements..."); - let jdk_dir = DATA.root_dir.lock().unwrap().join(self.jdk_folder_name()); + let jdk_dir = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(self.jdk_folder_name()); let _ = tokio::fs::remove_dir_all(jdk_dir).await; - let jdk_zip = DATA.root_dir.lock().unwrap().join(self.jdk_zip_name()); + let jdk_zip = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(self.jdk_zip_name()); let _ = tokio::fs::remove_file(jdk_zip).await; self.download_requirements(app_handle).await?; @@ -861,7 +893,49 @@ impl Client { pub(super) fn build_classpath(&self) -> Result { let (_, client_jar) = self.get_launch_paths()?; - let agent_overlay = DATA.root_dir.lock().unwrap().join(AGENT_OVERLAY_FOLDER); + let agent_overlay = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(AGENT_OVERLAY_FOLDER); + + let resolve_libraries_root = |this: &Self| { + if let Some(path) = this + .libraries_path + .as_deref() + .filter(|p| !p.trim().is_empty()) + { + return PathBuf::from(path); + } + + match this.client_type { + ClientType::Fabric => DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FABRIC_FOLDER), + ClientType::Forge => DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_LEGACY_FOLDER), + ClientType::Default if this.uses_sub_libraries() => DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(this.sub_libraries_folder()), + ClientType::Default if this.is_legacy_client() => DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_LEGACY_FOLDER), + ClientType::Default => DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FOLDER), + } + }; let mut cp_parts = Vec::new(); @@ -869,29 +943,21 @@ impl Client { ClientType::Fabric => { cp_parts.push(self.get_minecraft_jar_path()); + let libs_root = resolve_libraries_root(self); let safe_ver = sanitize_version_for_paths(&self.version); - let fabric_libs_root = DATA.root_dir.lock().unwrap().join(LIBRARIES_FABRIC_FOLDER); - - let v_libs = fabric_libs_root.join(&safe_ver); + let v_libs = libs_root.join(&safe_ver); cp_parts.extend(collect_jars_recursive(&v_libs, false)); - - cp_parts.extend(collect_jars_recursive(&fabric_libs_root, true)); - + cp_parts.extend(collect_jars_recursive(&libs_root, true)); cp_parts.push(client_jar); } ClientType::Forge => { cp_parts.push(self.get_minecraft_jar_path()); - let libs = DATA.root_dir.lock().unwrap().join(LIBRARIES_LEGACY_FOLDER); + let libs = resolve_libraries_root(self); cp_parts.extend(collect_jars_recursive(&libs, false)); - cp_parts.push(client_jar); } ClientType::Default => { - let libs = if self.is_legacy_client() { - DATA.root_dir.lock().unwrap().join(LIBRARIES_LEGACY_FOLDER) - } else { - DATA.root_dir.lock().unwrap().join(LIBRARIES_FOLDER) - }; + let libs = resolve_libraries_root(self); return Ok(format!( "{}{}*{}{}{}{}", @@ -921,3 +987,65 @@ impl Client { .join(PATH_SEPARATOR)) } } + +#[cfg(unix)] +pub(crate) fn ensure_jdk_permissions_on_startup() { + use std::os::unix::fs::PermissionsExt; + let Ok(root) = DATA.root_dir.lock() else { + return; + }; + for folder in [JDK8_FOLDER, JDK21_FOLDER] { + let jdk_root = root.join(folder); + if !jdk_root.exists() { + continue; + } + set_exec_bit_recursive(&jdk_root); + let java_bin = jdk_root.join("bin").join("java"); + if let Ok(meta) = std::fs::metadata(&java_bin) { + let mut perms = meta.permissions(); + let mode = perms.mode(); + if mode & 0o111 == 0 { + perms.set_mode(mode | 0o755); + let _ = std::fs::set_permissions(&java_bin, perms); + log_warn!( + "Fixed missing exec bit on java binary: {}", + java_bin.display() + ); + } + } + } +} + +#[cfg(not(unix))] +pub(crate) fn ensure_jdk_permissions_on_startup() {} + +#[cfg(unix)] +fn set_exec_bit_recursive(dir: &std::path::Path) { + use std::os::unix::fs::PermissionsExt; + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + if file_type.is_dir() { + set_exec_bit_recursive(&path); + continue; + } + if !file_type.is_file() { + continue; + } + let Ok(mut perms) = std::fs::metadata(&path).map(|m| m.permissions()) else { + continue; + }; + let mode = perms.mode(); + if mode & 0o111 == 0 { + perms.set_mode(mode | 0o755); + if let Err(e) = std::fs::set_permissions(&path, perms) { + log_warn!("Failed to set exec perm on {}: {}", path.display(), e); + } + } + } +} diff --git a/src-tauri/src/core/clients/client_detect.rs b/src-tauri/src/core/clients/client_detect.rs new file mode 100644 index 00000000..3d0b7786 --- /dev/null +++ b/src-tauri/src/core/clients/client_detect.rs @@ -0,0 +1,299 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; + +use crate::core::clients::client::ClientType; +use crate::{log_debug, log_warn}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DetectedClient { + pub main_class: String, + pub client_type: ClientType, + pub confidence: u8, + pub reason: String, +} + +#[allow(dead_code)] +const FORGE_MAIN_CANDIDATES: &[&str] = &[ + "net.minecraftforge.legacy.server.start.LegacyServerStart", + "GradleStart", + "net.minecraftforge.fml.common.launcher.FMLServerTweaker", + "cpw.mods.bootstrap.Bootstrapper", + "cpw.mods.fml.common.launcher.FMLTweaker", + "net.minecraftforge.fml.common.launcher.FMLTweaker", +]; + +const FORGE_LAUNCHER_HINTS: &[&str] = &[ + "net/minecraft/launchwrapper/Launch", + "net/minecraft/launchwrapper/LaunchClassLoader", + "cpw/mods/fml/common/launcher/FMLTweaker", + "net/minecraftforge/fml/common/launcher/FMLTweaker", + "net/minecraftforge/legacy/ForgeTweaker", +]; + +const FABRIC_LAUNCHER_HINTS: &[&str] = &[ + "net/fabricmc/loader/impl/launch/knot/KnotClient", + "net/fabricmc/loader/impl/launch/knot/Knot", + "fabric/loader/impl/launch/knot/KnotClient", + "net/fabricmc/loader/impl/launch/knot/KnotServer", +]; + +const VANILLA_MAIN_CANDIDATES: &[&str] = &["net/minecraft/client/main/Main"]; + +const FORGE_DEPENDENCY_HINTS: &[&str] = &[ + "forge_universal", + "forgeSrc", + "net/minecraftforge", + "cpw/mods/fml", +]; + +const FABRIC_DEPENDENCY_HINTS: &[&str] = &["fabric-api", "fabric-loader", "net/fabricmc"]; + +const OPTIFINE_HINT: &str = "optifine/OptiFineClassTransformer"; + +const LWJGL_HINTS: &[&str] = &["org/lwjgl", "Lwjgl"]; + +pub fn detect_client_type(jar_path: &Path) -> Option { + let file = match File::open(jar_path) { + Ok(f) => f, + Err(e) => { + log_warn!("Cannot open {}: {}", jar_path.display(), e); + return None; + } + }; + let reader = BufReader::new(file); + let mut archive = match zip::ZipArchive::new(reader) { + Ok(a) => a, + Err(e) => { + log_warn!("{} is not a valid zip/jar: {}", jar_path.display(), e); + return None; + } + }; + + let manifest_main_class = read_manifest_main_class(&mut archive); + + let mut class_names: Vec = Vec::new(); + let mut has_forge_class = false; + let mut has_fabric_class = false; + let mut has_optifine = false; + let mut has_launchwrapper = false; + let mut has_lwjgl = false; + let mut has_vanilla_main = false; + + for i in 0..archive.len() { + let entry = match archive.by_index(i) { + Ok(e) => e, + Err(_) => continue, + }; + if !entry.is_file() { + continue; + } + let name = match entry.name().to_string() { + n if n.len() < 8 => continue, + n => n, + }; + if !name.ends_with(".class") { + continue; + } + if name.len() > 4096 { + continue; + } + + for hint in FORGE_LAUNCHER_HINTS { + if name.contains(hint) { + has_forge_class = true; + has_launchwrapper = true; + break; + } + } + for hint in FABRIC_LAUNCHER_HINTS { + if name.contains(hint) { + has_fabric_class = true; + break; + } + } + if name.contains(OPTIFINE_HINT) { + has_optifine = true; + } + for hint in VANILLA_MAIN_CANDIDATES { + if name.contains(hint) { + has_vanilla_main = true; + } + } + for hint in LWJGL_HINTS { + if name.contains(hint) { + has_lwjgl = true; + } + } + + if class_names.len() < 4000 { + class_names.push(name); + } + + if has_launchwrapper && has_fabric_class { + break; + } + } + + let mut deps: HashMap = HashMap::new(); + if let Ok(data) = read_file_from_archive(&mut archive, "dependencies.json") { + if let Ok(map) = serde_json::from_slice::>(&data) { + deps = map; + } + } + + let has_forge_dep = deps + .values() + .any(|v| FORGE_DEPENDENCY_HINTS.iter().any(|h| v.contains(h))); + let has_fabric_dep = deps + .values() + .any(|v| FABRIC_DEPENDENCY_HINTS.iter().any(|h| v.contains(h))); + + log_debug!( + "Detection: forge={} fabric={} optifine={} launchwrapper={} vanilla={} lwjgl={} forge_dep={} fabric_dep={}", + has_forge_class, + has_fabric_class, + has_optifine, + has_launchwrapper, + has_vanilla_main, + has_lwjgl, + has_forge_dep, + has_fabric_dep, + ); + + if let Some(main) = &manifest_main_class { + log_debug!("Manifest Main-Class: {}", main); + } + + if has_fabric_class || has_fabric_dep { + let main = manifest_main_class + .clone() + .unwrap_or_else(|| "net.fabricmc.loader.impl.launch.knot.KnotClient".to_string()); + return Some(DetectedClient { + main_class: main, + client_type: ClientType::Fabric, + confidence: if has_fabric_class && has_fabric_dep { 100 } else { 80 }, + reason: "Detected fabric loader classes/dependencies".to_string(), + }); + } + + if has_forge_class || has_launchwrapper || has_forge_dep { + let main = if let Some(m) = &manifest_main_class { + if m.contains("launchwrapper") + || m == "GradleStart" + || m == "net.minecraft.launchwrapper.Launch" + { + m.clone() + } else if has_optifine { + "net.minecraft.launchwrapper.Launch".to_string() + } else { + m.clone() + } + } else { + "net.minecraft.launchwrapper.Launch".to_string() + }; + + if !main.contains("launchwrapper") && !main.contains("GradleStart") && !has_optifine { + log_warn!( + "Forge-like client detected but main class is '{}' which is not a known launcher; falling back to vanilla main", + main + ); + } else { + return Some(DetectedClient { + main_class: "net.minecraft.launchwrapper.Launch".to_string(), + client_type: ClientType::Forge, + confidence: if has_forge_class && has_forge_dep { 100 } else { 80 }, + reason: "Detected Forge launchwrapper / OptiFine-Forge / Forge deps".to_string(), + }); + } + } + + if has_optifine && !has_launchwrapper { + let has_optifine_forge = class_names + .iter() + .any(|n| n.contains("optifine/reflect/ReflectorForge")); + if has_optifine_forge { + return Some(DetectedClient { + main_class: "net.minecraft.launchwrapper.Launch".to_string(), + client_type: ClientType::Forge, + confidence: 85, + reason: "OptiFine Forge integration detected (ReflectorForge class)" + .to_string(), + }); + } + let main = manifest_main_class.clone().unwrap_or_else(|| { + "net.minecraft.client.main.Main".to_string() + }); + return Some(DetectedClient { + main_class: main, + client_type: ClientType::Default, + confidence: 60, + reason: "OptiFine without Forge launchwrapper; using manifest or vanilla main".to_string(), + }); + } + + if has_vanilla_main { + let main = "net.minecraft.client.main.Main".to_string(); + return Some(DetectedClient { + main_class: main, + client_type: ClientType::Default, + confidence: 90, + reason: "Detected vanilla Main class".to_string(), + }); + } + + if let Some(main) = manifest_main_class { + return Some(DetectedClient { + main_class: main, + client_type: ClientType::Default, + confidence: 30, + reason: "Using manifest Main-Class (unknown structure)".to_string(), + }); + } + + log_warn!("Could not detect client type for {}", jar_path.display()); + None +} + +fn read_manifest_main_class( + archive: &mut zip::ZipArchive>, +) -> Option { + let mut entry = archive + .by_name("META-INF/MANIFEST.MF") + .ok()?; + let mut contents = String::new(); + entry.read_to_string(&mut contents).ok()?; + for line in contents.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("Main-Class:") { + return Some(rest.trim().to_string()); + } + if let Some(rest) = trimmed.strip_prefix("Main-Class ") { + return Some(rest.trim().to_string()); + } + } + None +} + +fn read_file_from_archive( + archive: &mut zip::ZipArchive>, + name: &str, +) -> Result, String> { + let mut entry = archive + .by_name(name) + .map_err(|e| format!("entry not found: {e}"))?; + let mut buf = Vec::new(); + entry + .read_to_end(&mut buf) + .map_err(|e| format!("read error: {e}"))?; + Ok(buf) +} + +#[allow(dead_code)] +pub fn describe(detected: &DetectedClient) -> String { + format!( + "{:?} main='{}' (confidence {}%, reason: {})", + detected.client_type, detected.main_class, detected.confidence, detected.reason + ) +} diff --git a/src-tauri/src/core/clients/custom_clients.rs b/src-tauri/src/core/clients/custom_clients.rs index 880de8d7..3f0f41eb 100644 --- a/src-tauri/src/core/clients/custom_clients.rs +++ b/src-tauri/src/core/clients/custom_clients.rs @@ -21,7 +21,13 @@ pub struct CustomClient { pub insecure: bool, pub java_path: Option, pub java_args: Option, + pub libraries_path: Option, + pub natives_path: Option, pub client_type: ClientType, + #[serde(default)] + pub viaversion: Option, + #[serde(default)] + pub java_version: Option, } impl CustomClient { @@ -46,7 +52,11 @@ impl CustomClient { insecure: false, java_path: None, java_args: None, + libraries_path: None, + natives_path: None, client_type: ClientType::Default, + viaversion: None, + java_version: None, } } @@ -85,9 +95,15 @@ impl CustomClient { installed: self.is_installed, is_custom: true, size: 0, + viaversion: None, + java_version: None, }, java_path: self.java_path.clone(), java_args: self.java_args.clone(), + libraries_path: self.libraries_path.clone(), + natives_path: self.natives_path.clone(), + viaversion: self.viaversion.clone(), + java_version: self.java_version.clone(), } } @@ -141,4 +157,44 @@ impl CustomClient { pub fn stop(&self) -> Result<(), String> { process::stop_process_by_filename(&self.filename, &self.name) } + + pub fn resolve_libraries_dir(&self) -> PathBuf { + self.libraries_path + .as_deref() + .filter(|p| !p.trim().is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| { + let root = crate::core::storage::data::DATA.root_dir.lock().unwrap_or_else(|e| e.into_inner()); + match self.client_type { + ClientType::Fabric => root.join(crate::core::utils::globals::LIBRARIES_FABRIC_FOLDER), + ClientType::Forge => root.join(crate::core::utils::globals::LIBRARIES_LEGACY_FOLDER), + ClientType::Default if self.version.contains("1.8") || self.version.contains("1.7") => { + root.join(crate::core::utils::globals::LIBRARIES_LEGACY_FOLDER) + } + _ => root.join(crate::core::utils::globals::LIBRARIES_FOLDER), + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::CustomClient; + use std::path::PathBuf; + + #[test] + fn custom_client_resolves_libraries_dir_override() { + let mut client = CustomClient::new( + 1, + "Test Client".to_string(), + "1.20.1".to_string(), + "test.jar".to_string(), + PathBuf::from("/tmp/test.jar"), + "net.minecraft.client.main.Main".to_string(), + ); + + client.libraries_path = Some("/tmp/custom-libs".to_string()); + + assert_eq!(client.resolve_libraries_dir(), PathBuf::from("/tmp/custom-libs")); + } } diff --git a/src-tauri/src/core/clients/internal/mod.rs b/src-tauri/src/core/clients/internal/mod.rs index 0432aa99..0f839360 100644 --- a/src-tauri/src/core/clients/internal/mod.rs +++ b/src-tauri/src/core/clients/internal/mod.rs @@ -1 +1,2 @@ pub mod agent_overlay; +pub mod titlebar_branding; diff --git a/src-tauri/src/core/clients/internal/titlebar_branding.rs b/src-tauri/src/core/clients/internal/titlebar_branding.rs new file mode 100644 index 00000000..2a29a523 --- /dev/null +++ b/src-tauri/src/core/clients/internal/titlebar_branding.rs @@ -0,0 +1,249 @@ +use crate::core::network::servers::SERVERS; +use crate::core::storage::data::DATA; +use crate::core::utils::globals::{AGENT_OVERLAY_FOLDER, TITLEBAR_FILE}; +use crate::core::utils::hashing::calculate_md5_hash; +use crate::{log_debug, log_error, log_info, log_warn}; +use serde::Deserialize; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Deserialize)] +struct TitlebarHashes { + titlebar_windows: Option, + titlebar_linux: Option, + titlebar_macos: Option, +} + +pub struct TitlebarBrandingManager; + +impl TitlebarBrandingManager { + fn get_api_base_url() -> Result { + SERVERS + .selected_api + .read() + .unwrap() + .as_ref() + .map(|server| server.url.clone()) + .ok_or_else(|| "No API server available".to_string()) + } + + fn system_name() -> &'static str { + if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos" + } else { + "linux" + } + } + + fn download_url(base: &str, system: &str) -> String { + match system { + "windows" => format!("{base}/agent/libCollapseTitlebar.dll"), + "macos" => format!("{base}/agent/libCollapseTitlebar.dylib"), + _ => format!("{base}/agent/libCollapseTitlebar.so"), + } + } + + fn expected_hash(info: &TitlebarHashes, system: &str) -> Option { + match system { + "windows" => info.titlebar_windows.clone(), + "macos" => info.titlebar_macos.clone(), + _ => info.titlebar_linux.clone(), + } + } + + pub async fn download_titlebar_file() -> Result<(), String> { + log_debug!("Starting download of titlebar branding file..."); + + let info = Self::get_titlebar_hashes().await?; + + let system = Self::system_name(); + + if Self::expected_hash(&info, system).is_none() { + log_warn!( + "No titlebar hash available for platform '{}', skipping download", + system + ); + return Ok(()); + } + + let folder = DATA.root_dir.lock().unwrap().join(AGENT_OVERLAY_FOLDER); + if !folder.exists() { + log_debug!("Titlebar folder missing, creating: {}", folder.display()); + fs::create_dir_all(&folder).map_err(|e| format!("Failed to create directory: {e}"))?; + log_info!("Created titlebar folder: {}", folder.display()); + } + + let titlebar_path = folder.join(TITLEBAR_FILE); + + let base_url = Self::get_api_base_url()?; + let base = base_url.trim_end_matches('/').to_string(); + + let url = Self::download_url(&base, system); + log_info!("Downloading titlebar branding file for {system}"); + + match Self::download_file(&url, &titlebar_path).await { + Ok(()) => {} + Err(e) => { + log_warn!( + "Titlebar branding file not available for '{}': {}", + system, + e + ); + return Ok(()); + } + } + + let downloaded_hash = calculate_md5_hash(&titlebar_path)?; + let expected_hash = Self::expected_hash(&info, system).unwrap_or_default(); + + if !expected_hash.is_empty() && downloaded_hash != expected_hash { + log_error!( + "Titlebar file hash mismatch. expected={} got={}", + expected_hash, + downloaded_hash + ); + return Err(format!( + "Titlebar file hash mismatch. Expected: {}, Got: {}", + expected_hash, downloaded_hash + )); + } + + log_info!("Titlebar branding file downloaded and verified successfully"); + Ok(()) + } + + pub async fn verify_titlebar_file() -> Result { + log_debug!("Verifying titlebar branding file..."); + + let folder = DATA.root_dir.lock().unwrap().join(AGENT_OVERLAY_FOLDER); + if !folder.exists() { + log_debug!( + "Titlebar folder missing during verify, creating: {}", + folder.display() + ); + fs::create_dir_all(&folder).map_err(|e| format!("Failed to create directory: {e}"))?; + } + + let titlebar_path = folder.join(TITLEBAR_FILE); + + if !titlebar_path.exists() { + log_warn!( + "Titlebar branding file is missing: {}", + titlebar_path.display() + ); + return Ok(false); + } + + let info = Self::get_titlebar_hashes().await?; + let system = Self::system_name(); + + let expected_hash = match Self::expected_hash(&info, system) { + Some(h) => h, + None => { + log_warn!( + "No titlebar hash for platform '{}', skipping verification", + system + ); + return Ok(false); + } + }; + + let hash = calculate_md5_hash(&titlebar_path)?; + + if hash != expected_hash { + log_warn!( + "Titlebar file hash verification failed. Expected: {}, Got: {}", + expected_hash, + hash + ); + return Ok(false); + } + + log_info!("Titlebar branding file verified successfully"); + Ok(true) + } + + pub fn has_branding_in_jar(jar_path: &Path) -> bool { + let data = match fs::read(jar_path) { + Ok(d) => d, + Err(_) => return false, + }; + + let marker = b"@CollapseLoader"; + + if let Some(pos) = data.windows(marker.len()).position(|w| w == marker) { + let context_start = pos.saturating_sub(16); + let context_end = (pos + marker.len() + 16).min(data.len()); + let context = &data[context_start..context_end]; + let context_str = String::from_utf8_lossy(context); + + log_info!( + "Found existing titlebar branding in JAR at offset {}: ...{}...", + pos, + context_str.replace('\n', " ").replace('\r', "") + ); + return true; + } + + false + } + + async fn get_titlebar_hashes() -> Result { + let base_url = Self::get_api_base_url()?; + let base = base_url.trim_end_matches('/').to_string(); + + let url = format!("{base}/agent/hashes.json"); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Failed to get titlebar hashes: {e}"))?; + + if !response.status().is_success() { + return Err(format!("Backend returned error: {}", response.status())); + } + + let hashes: TitlebarHashes = response.json().await.map_err(|e| { + log_error!("Failed to parse titlebar hashes response: {}", e); + format!("Failed to parse titlebar hashes: {e}") + })?; + + Ok(hashes) + } + + async fn download_file(url: &str, path: &PathBuf) -> Result<(), String> { + let client = reqwest::Client::new(); + let response = client.get(url).send().await.map_err(|e| { + log_error!("HTTP request failed for {}: {}", url, e); + format!("Failed to download file: {e}") + })?; + + if !response.status().is_success() { + log_error!("Download failed for {}: HTTP {}", url, response.status()); + return Err(format!( + "Download failed with status: {}", + response.status() + )); + } + + let bytes = response.bytes().await.map_err(|e| { + log_error!("Failed to read bytes from response for {}: {}", url, e); + format!("Failed to read file bytes: {e}") + })?; + + fs::write(path, bytes).map_err(|e| { + log_error!( + "Failed to write downloaded file to {}: {}", + path.display(), + e + ); + format!("Failed to write file to disk: {e}") + })?; + + Ok(()) + } +} diff --git a/src-tauri/src/core/clients/log_checker.rs b/src-tauri/src/core/clients/log_checker.rs index de9d6802..5daf3936 100644 --- a/src-tauri/src/core/clients/log_checker.rs +++ b/src-tauri/src/core/clients/log_checker.rs @@ -1,18 +1,36 @@ use regex::Regex; use serde::Serialize; -use std::sync::LazyLock; +use std::collections::HashSet; +use std::sync::{Arc, LazyLock, Mutex}; use tauri::AppHandle; use crate::{ core::{ clients::client::{Client, CLIENT_LOGS}, network::servers::SERVERS, + storage::data::DATA, utils::globals::API_VERSION, utils::helpers::emit_to_main_window, }, log_debug, log_error, log_info, log_warn, }; +static OPTIONS_BLACKLIST: LazyLock>>> = + LazyLock::new(|| Arc::new(Mutex::new(HashSet::new()))); + +pub fn is_options_sync_blocked(client_base: &str) -> bool { + OPTIONS_BLACKLIST + .lock() + .map(|s| s.contains(client_base)) + .unwrap_or(false) +} + +pub fn block_options_sync(client_base: &str) { + if let Ok(mut s) = OPTIONS_BLACKLIST.lock() { + s.insert(client_base.to_string()); + } +} + pub struct LogChecker { pub client: Client, user_token: String, @@ -23,6 +41,7 @@ enum CrashType { MissingMainClass, OutOfMemory, GameCrashed, + OptionsCorrupted, } #[derive(Serialize)] @@ -128,6 +147,9 @@ impl LogChecker { } else if log_string.contains("java.lang.OutOfMemoryError") { log_debug!("Detected OutOfMemory crash type"); Some(CrashType::OutOfMemory) + } else if self.is_options_corruption(log_string) { + log_debug!("Detected options.txt corruption crash type"); + Some(CrashType::OptionsCorrupted) } else if log_string.contains("#@!@# Game crashed!") || log_string.contains("Error occurred during initialization of VM") || log_string.contains("java.lang.UnsupportedClassVersionError") @@ -139,6 +161,15 @@ impl LogChecker { } } + fn is_options_corruption(&self, log_string: &str) -> bool { + log_string.contains("NumberFormatException: For input string:") + && log_string.contains("GameSettings.loadOptions") + || log_string.contains("java.lang.NoSuchMethodError") + && log_string.contains("ITransformation") + || log_string.contains("NoClassDefFoundError") + && log_string.contains("Reflector") + } + fn handle_crash(&self, crash_type: CrashType, client_logs: &[String], app_handle: &AppHandle) { log_warn!( "Client {} crashed! Detected reason: {:?}", @@ -183,9 +214,59 @@ impl LogChecker { }), ); } + CrashType::OptionsCorrupted => { + self.handle_options_corruption(app_handle); + } } } + fn handle_options_corruption(&self, app_handle: &AppHandle) { + let client_base = crate::core::storage::data::Data::get_filename(&self.client.filename); + log_warn!( + "Detected corrupted options for client '{}' (base={}), cleaning up and disabling sync", + self.client.name, + client_base + ); + + let client_dir = DATA + .root_dir + .lock() + .ok() + .map(|root| root.join(&client_base)); + + if let Some(dir) = client_dir { + if dir.exists() { + for name in ["options.txt", "optionsof.txt"] { + let path = dir.join(name); + if path.exists() { + if let Err(e) = std::fs::remove_file(&path) { + log_warn!("Failed to remove {}: {}", path.display(), e); + } else { + log_info!("Removed corrupted {}", path.display()); + } + } + } + } + } + + block_options_sync(&client_base); + + let _ = std::fs::remove_file(DATA.root_dir.lock().ok() + .map(|r| r.join("synced_options").join("options.txt")).unwrap_or_default()); + let _ = std::fs::remove_file(DATA.root_dir.lock().ok() + .map(|r| r.join("synced_options").join("optionsof.txt")).unwrap_or_default()); + + emit_to_main_window( + app_handle, + "client-options-reset", + serde_json::json!({ + "id": self.client.id, + "name": self.client.name.clone(), + "reason": "Corrupted options.txt detected; files removed and sync disabled" + }), + ); + } + fn emit_crash_details(&self, client_logs: &[String], app_handle: &AppHandle) { log_debug!( "Emitting client-crash-details for client '{}'", @@ -229,6 +310,7 @@ impl LogChecker { CrashType::MissingMainClass => "MissingMainClass", CrashType::OutOfMemory => "OutOfMemory", CrashType::GameCrashed => "GameCrashed", + CrashType::OptionsCorrupted => "OptionsCorrupted", } .to_string(); diff --git a/src-tauri/src/core/clients/manager.rs b/src-tauri/src/core/clients/manager.rs index feee49b5..84964aee 100644 --- a/src-tauri/src/core/clients/manager.rs +++ b/src-tauri/src/core/clients/manager.rs @@ -49,6 +49,8 @@ impl ClientManager { installed: rng.random_bool(1.0 / 3.0), is_custom: false, size: rng.random_range(50..=100), + viaversion: None, + java_version: None, }, ..Default::default() }) @@ -96,7 +98,15 @@ impl ClientManager { let dev_enabled = crate::core::utils::helpers::is_development_enabled(); for client in clients { - client.meta = Meta::new(&client.version, &client.filename, &client.client_type); + let viaversion = client.viaversion.clone(); + let java_version = client.java_version.clone(); + client.meta = Meta::new( + &client.version, + &client.filename, + &client.client_type, + viaversion, + java_version, + ); client.meta.size = client.size; if dev_enabled { diff --git a/src-tauri/src/core/clients/mod.rs b/src-tauri/src/core/clients/mod.rs index e43f7e2a..4cdaf1e7 100644 --- a/src-tauri/src/core/clients/mod.rs +++ b/src-tauri/src/core/clients/mod.rs @@ -1,4 +1,5 @@ pub mod client; +pub mod client_detect; pub mod custom_clients; pub mod internal; pub mod log_checker; diff --git a/src-tauri/src/core/network/mod.rs b/src-tauri/src/core/network/mod.rs index 4708cb12..e65ee830 100644 --- a/src-tauri/src/core/network/mod.rs +++ b/src-tauri/src/core/network/mod.rs @@ -7,9 +7,15 @@ pub mod servers; use crate::log_error; use reqwest::{Client, ClientBuilder}; +use std::sync::OnceLock; use std::time::Duration; -use std::sync::OnceLock; +/// Returns a lazily-initialized shared HTTP client with a 30-second timeout. +/// Used by both `commands/network.rs` and `core/network/server_ads.rs`. +pub fn get_api_client() -> &'static Client { + static API_CLIENT: OnceLock = OnceLock::new(); + API_CLIENT.get_or_init(|| create_client(Duration::from_secs(30))) +} pub fn user_agent() -> &'static str { static USER_AGENT: OnceLock = OnceLock::new(); diff --git a/src-tauri/src/core/network/server_ads.rs b/src-tauri/src/core/network/server_ads.rs index bdefb5a8..2b7d2aa6 100644 --- a/src-tauri/src/core/network/server_ads.rs +++ b/src-tauri/src/core/network/server_ads.rs @@ -1,10 +1,14 @@ use serde::Deserialize; use std::path::Path; -use crate::core::network::api::API; +use super::get_api_client; +use crate::core::storage::settings::SETTINGS; use crate::{log_error, log_info, log_warn}; -const SERVER_ADS_URL: &str = "server-ads"; +const SERVER_ADS_URL: &str = + "https://huggingface.co/datasets/Collapsecdn/collapsecdn/raw/main/server-ads/autoaddserverads.json"; +const SERVER_NOT_ADS_URL: &str = + "https://huggingface.co/datasets/Collapsecdn/collapsecdn/raw/main/server-ads/autoaddservernotads.json"; /// Data structure for a server advertisement. #[derive(Debug, Clone, Deserialize)] @@ -15,31 +19,102 @@ pub struct ServerAdData { pub ip: String, } -/// Fetches the current list of server advertisements from the API. -pub async fn fetch_server_ads() -> Vec { - let Some(api) = API.as_ref() else { - log_warn!("API not available, skipping server ads fetch"); - return vec![]; - }; +/// Result of fetching server lists from CDN. +pub struct ServerFetchResult { + /// Paid advertisement servers (placed at the top of the list). + pub ads: Vec, + /// Regular servers (placed after ads, before user servers). + pub regular: Vec, +} + +/// Fetches a JSON list from a URL using the global network client. +/// Returns None if the response is empty. +async fn fetch_server_list(url: &str) -> Result, String> { + let client = get_api_client(); + let response = client + .get(url) + .send() + .await + .map_err(|e| format!("Failed to fetch {}: {}", url, e))?; + + if !response.status().is_success() { + return Err(format!("HTTP {} for {}", response.status(), url)); + } + + let text = response + .text() + .await + .map_err(|e| format!("Failed to read response from {}: {}", url, e))?; + + if text.trim().is_empty() { + return Ok(None); + } - match api.json_async::>(SERVER_ADS_URL).await { - Ok(ads) => { - log_info!("Fetched {} server ad(s)", ads.len()); - ads + let value: T = serde_json::from_str(&text) + .map_err(|e| format!("Failed to parse JSON from {}: {}", url, e))?; + + Ok(Some(value)) +} + +/// Fetches server lists from HuggingFace CDN. +/// - autoaddserverads.json → paid ads (priority, placed first) +/// - autoaddservernotads.json → regular servers (placed after ads) +pub async fn fetch_server_ads() -> ServerFetchResult { + let mut ads = Vec::new(); + let mut regular = Vec::new(); + + // Fetch paid ads + match fetch_server_list::>(SERVER_ADS_URL).await { + Ok(Some(fetched)) => { + log_info!("Fetched {} paid server ad(s) from CDN", fetched.len()); + ads = fetched; + } + Ok(None) => { + log_info!("Paid server ads file is empty"); } Err(e) => { - log_warn!("Failed to fetch server ads: {}", e); - vec![] + log_warn!("Failed to fetch paid server ads from CDN: {}", e); } } + + // Fetch regular servers + match fetch_server_list::>(SERVER_NOT_ADS_URL).await { + Ok(Some(fetched)) => { + log_info!("Fetched {} regular server(s) from CDN", fetched.len()); + regular = fetched; + } + Ok(None) => { + log_info!("Regular servers file is empty"); + } + Err(e) => { + log_warn!("Failed to fetch regular servers from CDN: {}", e); + } + } + + log_info!( + "Server fetch result: {} paid ads, {} regular servers", + ads.len(), + regular.len() + ); + + ServerFetchResult { ads, regular } } -/// Injects server advertisements into a Minecraft `servers.dat` file. +/// Injects server lists into a Minecraft `servers.dat` file. /// -/// This function merges the ads with existing user servers, ensuring that -/// ads are placed at the top of the list and duplicates are removed. -pub fn inject_servers_dat(path: &Path, ads: &[ServerAdData]) { - if ads.is_empty() { +/// Priority order: paid ads → regular servers → user servers +/// Duplicates by IP are removed (first occurrence wins). +pub fn inject_servers_dat(path: &Path, result: &ServerFetchResult) { + // Check if server ads are disabled in settings + if SETTINGS.lock().unwrap().disable_server_ads.value { + log_info!("Server ads disabled by user setting, skipping injection"); + return; + } + + let has_ads = !result.ads.is_empty(); + let has_regular = !result.regular.is_empty(); + + if !has_ads && !has_regular { return; } @@ -49,19 +124,40 @@ pub fn inject_servers_dat(path: &Path, ads: &[ServerAdData]) { vec![] }; - let ad_ips: std::collections::HashSet<&str> = ads.iter().map(|a| a.ip.as_str()).collect(); + // Collect all CDN IPs to filter user servers + let mut seen_ips: std::collections::HashSet<&str> = std::collections::HashSet::new(); + for ad in &result.ads { + seen_ips.insert(ad.ip.as_str()); + } + for server in &result.regular { + seen_ips.insert(server.ip.as_str()); + } + // User servers (without duplicates) let user_servers: Vec<(String, String)> = existing .into_iter() - .filter(|(_, ip)| !ad_ips.contains(ip.as_str())) + .filter(|(_, ip)| !seen_ips.contains(ip.as_str())) .collect(); - let mut all_servers: Vec<(String, String)> = - ads.iter().map(|a| (a.name.clone(), a.ip.clone())).collect(); + // Build final list: ads first, then regular, then user servers + let mut all_servers: Vec<(String, String)> = Vec::new(); + + for ad in &result.ads { + all_servers.push((ad.name.clone(), ad.ip.clone())); + } + for server in &result.regular { + all_servers.push((server.name.clone(), server.ip.clone())); + } all_servers.extend(user_servers); + let total_injected = result.ads.len() + result.regular.len(); match write_servers_dat(path, &all_servers) { - Ok(_) => log_info!("Injected {} server(s) into servers.dat", ads.len()), + Ok(_) => log_info!( + "Injected {} server(s) into servers.dat ({} ads, {} regular)", + total_injected, + result.ads.len(), + result.regular.len() + ), Err(e) => log_error!("Failed to write servers.dat: {}", e), } } diff --git a/src-tauri/src/core/network/servers.rs b/src-tauri/src/core/network/servers.rs index 48763b33..d387170b 100644 --- a/src-tauri/src/core/network/servers.rs +++ b/src-tauri/src/core/network/servers.rs @@ -213,7 +213,10 @@ impl Servers { } pub fn set_status(&self) -> ServerConnectivityStatus { - let mut status = self.connectivity_status.lock().unwrap(); + let mut status = self + .connectivity_status + .lock() + .unwrap_or_else(|e| e.into_inner()); status.cdn_online = self.selected_cdn.read().unwrap().is_some(); status.api_online = self.selected_api.read().unwrap().is_some(); status.clone() diff --git a/src-tauri/src/core/platform/linux.rs b/src-tauri/src/core/platform/linux.rs index 97211f03..578a214e 100644 --- a/src-tauri/src/core/platform/linux.rs +++ b/src-tauri/src/core/platform/linux.rs @@ -1,41 +1,106 @@ use crate::core::platform::error::StartupError; -use std::process::Command; +use crate::log_warn; +use std::path::Path; -fn has_pkg_config_binary() -> bool { - Command::new("pkg-config") - .arg("--version") - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) -} +/// Checks if the WebKitGTK shared library is available on the system. +/// new since 18.08.2026 +/// on user pc old pkg-config variant isn't worked, so now we check for the lib in folders +fn has_webkit2gtk_library() -> bool { + let search_dirs = ["/usr/lib", "/usr/lib64", "/usr/local/lib"]; -fn has_pkg_config_package(name: &str) -> bool { - if !has_pkg_config_binary() { - return false; + for dir in &search_dirs { + let Ok(entries) = std::fs::read_dir(dir) else { + continue; + }; + + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + + if entry.path().is_dir() { + if let Ok(sub) = std::fs::read_dir(entry.path()) { + for sub_entry in sub.flatten() { + let sub_name = sub_entry.file_name(); + let sub_name = sub_name.to_string_lossy(); + if (sub_name.starts_with("libwebkit2gtk-4.1.so") + || sub_name.starts_with("libwebkit2gtk-4.0.so")) + && sub_entry.path().is_file() + { + return true; + } + } + } + } + + if (name.starts_with("libwebkit2gtk-4.1.so") + || name.starts_with("libwebkit2gtk-4.0.so")) + && entry.path().is_file() + { + return true; + } + } + } + + if let Ok(conf) = std::fs::read_to_string("/etc/ld.so.conf") { + for line in conf.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if line.starts_with("include ") { + let pattern = line.strip_prefix("include ").unwrap_or(line); + if let Some((dir_part, glob_part)) = pattern.rsplit_once('/') { + if glob_part == "*" { + if let Ok(entries) = std::fs::read_dir(dir_part) { + for entry in entries.flatten() { + if let Ok(include_conf) = std::fs::read_to_string(entry.path()) { + for inc_line in include_conf.lines() { + let inc_line = inc_line.trim(); + if !inc_line.is_empty() && !inc_line.starts_with('#') { + if check_dir_for_webkit(Path::new(inc_line)) { + return true; + } + } + } + } + } + } + } + } + } else if check_dir_for_webkit(Path::new(line)) { + return true; + } + } } - Command::new("pkg-config") - .args(["--exists", name]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) + false } -pub fn check_platform_dependencies() -> Result<(), StartupError> { - if !has_pkg_config_binary() { - return Err(StartupError::LinuxDependenciesMissing); +fn check_dir_for_webkit(dir: &Path) -> bool { + let Ok(entries) = std::fs::read_dir(dir) else { + return false; + }; + + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + if (name.starts_with("libwebkit2gtk-4.1.so") || name.starts_with("libwebkit2gtk-4.0.so")) + && entry.path().is_file() + { + return true; + } } - let ok = has_pkg_config_package("webkit2gtk-4.1") || has_pkg_config_package("webkit2gtk-4.0"); + false +} - if !ok { - return Err(StartupError::LinuxDependenciesMissing); +pub fn check_platform_dependencies() -> Result<(), StartupError> { + if !has_webkit2gtk_library() { + log_warn!( + "WebKitGTK shared library (libwebkit2gtk-4.1.so or libwebkit2gtk-4.0.so) was not found. \ + The app may fail to start if WebKitGTK is not installed." + ); } - Ok(()) } diff --git a/src-tauri/src/core/state.rs b/src-tauri/src/core/state.rs index 614140e4..afc3a935 100644 --- a/src-tauri/src/core/state.rs +++ b/src-tauri/src/core/state.rs @@ -1,6 +1,12 @@ use crate::core::clients::manager::ClientManager; -use std::sync::{Arc, Mutex}; -use std::sync::{MutexGuard, PoisonError}; +use crate::core::storage::accounts::ACCOUNT_MANAGER; +use crate::core::storage::favorites::FAVORITE_MANAGER; +use crate::core::storage::flags::FLAGS_MANAGER; +use crate::core::storage::launch_history::LAUNCH_HISTORY; +use crate::core::storage::mod_builds::MOD_BUILDS; +use crate::core::storage::presets::PRESET_MANAGER; +use crate::core::storage::settings::SETTINGS; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; pub struct ClientState { pub manager: Arc>, @@ -41,4 +47,53 @@ impl AppState { custom_clients: CustomClientsState::new(), } } + + pub fn settings(&self) -> MutexGuard<'static, crate::core::storage::settings::Settings> { + SETTINGS.lock().unwrap_or_else(PoisonError::into_inner) + } + + pub fn accounts(&self) -> MutexGuard<'static, crate::core::storage::accounts::AccountManager> { + ACCOUNT_MANAGER + .lock() + .unwrap_or_else(PoisonError::into_inner) + } + + pub fn favorites( + &self, + ) -> MutexGuard<'static, crate::core::storage::favorites::FavoriteManager> { + FAVORITE_MANAGER + .lock() + .unwrap_or_else(PoisonError::into_inner) + } + + pub fn flags(&self) -> MutexGuard<'static, crate::core::storage::flags::Flags> { + FLAGS_MANAGER.lock().unwrap_or_else(PoisonError::into_inner) + } + + pub fn presets(&self) -> MutexGuard<'static, crate::core::storage::presets::PresetManager> { + PRESET_MANAGER + .lock() + .unwrap_or_else(PoisonError::into_inner) + } + + pub fn mod_builds( + &self, + ) -> MutexGuard<'static, crate::core::storage::mod_builds::ModBuildManager> { + MOD_BUILDS.lock().unwrap_or_else(PoisonError::into_inner) + } + + pub fn launch_history( + &self, + ) -> MutexGuard<'static, crate::core::storage::launch_history::LaunchHistoryManager> { + LAUNCH_HISTORY + .lock() + .unwrap_or_else(PoisonError::into_inner) + } + + pub fn data(&self) -> MutexGuard<'static, std::path::PathBuf> { + crate::core::storage::data::DATA + .root_dir + .lock() + .unwrap_or_else(PoisonError::into_inner) + } } diff --git a/src-tauri/src/core/storage/custom_clients.rs b/src-tauri/src/core/storage/custom_clients.rs index 3a3de5b9..8d285c82 100644 --- a/src-tauri/src/core/storage/custom_clients.rs +++ b/src-tauri/src/core/storage/custom_clients.rs @@ -1,17 +1,14 @@ use std::{path::PathBuf, sync::Mutex}; +use super::common::JsonStorage; use crate::core::clients::client::ClientType; use crate::core::clients::custom_clients::CustomClient; use crate::core::storage::data::DATA; use crate::core::storage::settings::SETTINGS; use crate::core::utils::fs as fs_utils; use crate::core::utils::globals::CUSTOM_CLIENTS_FOLDER; -use crate::log_warn; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; -use tauri::async_runtime::block_on; - -use super::common::JsonStorage; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CustomClientManager { @@ -28,7 +25,11 @@ impl CustomClientManager { }) } - pub fn add_client(&mut self, mut custom_client: CustomClient) -> Result<(), String> { + /// Returns the client_base path if sync is needed, so the caller can do it async. + pub fn add_client( + &mut self, + mut custom_client: CustomClient, + ) -> Result, String> { if !custom_client.file_path.exists() { return Err(format!( "File '{}' does not exist. Please select a valid .jar file.", @@ -73,28 +74,21 @@ impl CustomClientManager { custom_client.file_path = target_path; custom_client.is_installed = true; - if SETTINGS + let sync_needed = SETTINGS .lock() .map(|s| s.sync_client_settings.value) .unwrap_or(false) - { - let client_base = format!( - "custom_clients{}{}", - std::path::MAIN_SEPARATOR, - custom_client.name - ); - if let Err(e) = block_on(DATA.ensure_client_synced(&client_base)) { - log_warn!( - "Failed to ensure client sync for custom client {}: {}", - custom_client.name, - e - ); - } - } + .then(|| { + format!( + "custom_clients{}{}", + std::path::MAIN_SEPARATOR, + custom_client.name + ) + }); self.clients.push(custom_client); self.save_to_disk(); - Ok(()) + Ok(sync_needed) } pub fn remove_client(&mut self, id: u32) -> Result<(), String> { @@ -150,10 +144,34 @@ impl CustomClientManager { client.java_args = Some(java_args); } + if let Some(libraries_path) = updates.libraries_path { + client.libraries_path = Some(libraries_path); + } + + if let Some(natives_path) = updates.natives_path { + client.natives_path = Some(natives_path); + } + if let Some(client_type) = updates.client_type { client.client_type = client_type; } + if let Some(viaversion) = updates.viaversion { + client.viaversion = if viaversion.is_empty() { + None + } else { + Some(viaversion) + }; + } + + if let Some(java_version) = updates.java_version { + client.java_version = if java_version.is_empty() { + None + } else { + Some(java_version) + }; + } + self.save_to_disk(); Ok(()) } else { @@ -169,7 +187,11 @@ pub struct CustomClientUpdate { pub main_class: Option, pub java_path: Option, pub java_args: Option, + pub libraries_path: Option, + pub natives_path: Option, pub client_type: Option, + pub viaversion: Option, + pub java_version: Option, } impl JsonStorage for CustomClientManager { diff --git a/src-tauri/src/core/storage/data.rs b/src-tauri/src/core/storage/data.rs index b0a33d07..c72b1de1 100644 --- a/src-tauri/src/core/storage/data.rs +++ b/src-tauri/src/core/storage/data.rs @@ -160,7 +160,10 @@ impl Data { } fn root_dir_snapshot(&self) -> PathBuf { - self.root_dir.lock().unwrap().clone() + self.root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() } fn get_local_with_root(root_dir: &Path, relative_path: &str) -> PathBuf { @@ -186,7 +189,7 @@ impl Data { let zip_path = Self::get_local_with_root(&root_dir, &info.local_file); let unzip_path = info.unzip_path(&root_dir); - let app_handle = APP_HANDLE.lock().unwrap().clone(); + let app_handle = APP_HANDLE.lock().unwrap_or_else(|e| e.into_inner()).clone(); let emit_name = info.local_file.clone(); task::spawn_blocking(move || { @@ -247,7 +250,11 @@ impl Data { pub async fn download_to_folder(&self, file: &str, dest_folder: &str) -> Result<(), String> { let info = Self::resolve_local_file_info(file); let root_dir = self.root_dir_snapshot(); - if let Some(app_handle) = APP_HANDLE.lock().unwrap().as_ref() { + if let Some(app_handle) = APP_HANDLE + .lock() + .unwrap_or_else(|e| e.into_inner()) + .as_ref() + { emit_to_main_window(app_handle, "download-start", &info.local_file); } @@ -268,7 +275,7 @@ impl Data { &info.local_file, )); - let app_handle = APP_HANDLE.lock().unwrap().clone(); + let app_handle = APP_HANDLE.lock().unwrap_or_else(|e| e.into_inner()).clone(); download_file( &download_urls, &dest_path, @@ -391,7 +398,11 @@ impl Data { return Ok(()); } - if let Some(app_handle) = APP_HANDLE.lock().unwrap().as_ref() { + if let Some(app_handle) = APP_HANDLE + .lock() + .unwrap_or_else(|e| e.into_inner()) + .as_ref() + { emit_to_main_window(app_handle, "download-start", &info.local_file); } @@ -400,7 +411,7 @@ impl Data { let download_urls = Self::get_download_urls(file)?; let dest_path = Self::get_destination_path(&root_dir, &info); - let app_handle = APP_HANDLE.lock().unwrap().clone(); + let app_handle = APP_HANDLE.lock().unwrap_or_else(|e| e.into_inner()).clone(); download_file( &download_urls, &dest_path, @@ -416,7 +427,11 @@ impl Data { if info.is_zip() { self.unzip(file).await.map_err(|e| { log_error!("Failed to extract {}: {}", file, e); - if let Some(app_handle) = APP_HANDLE.lock().unwrap().as_ref() { + if let Some(app_handle) = APP_HANDLE + .lock() + .unwrap_or_else(|e| e.into_inner()) + .as_ref() + { let err_data = serde_json::json!({ "file": info.local_file, "error": e @@ -620,6 +635,14 @@ impl Data { return Ok(()); } + if crate::core::clients::log_checker::is_options_sync_blocked(client_base) { + log_debug!( + "Skipping options sync for {} (client is on the corrupted-options blacklist)", + client_base + ); + return Ok(()); + } + let file_items = ["options.txt", "optionsof.txt"]; for name in file_items { @@ -741,7 +764,11 @@ impl Data { } pub fn verify_folder_integrity(&self, folder_name: &str) -> bool { - let folder_path = self.root_dir.lock().unwrap().join(folder_name); + let folder_path = self + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(folder_name); let manifest_path = folder_path.join("manifest.txt"); if !manifest_path.exists() { diff --git a/src-tauri/src/core/storage/settings.rs b/src-tauri/src/core/storage/settings.rs index c890cd8e..e8dc3bc3 100644 --- a/src-tauri/src/core/storage/settings.rs +++ b/src-tauri/src/core/storage/settings.rs @@ -161,6 +161,7 @@ define_settings! { autostart: bool = (false, true), start_minimized: bool = (false, true), auto_hide_sidebar: bool = (false, true), + disable_server_ads: bool = (false, true), } } @@ -186,5 +187,6 @@ pub fn settings_schema() -> Vec<(String, String)> { ("java_path".to_string(), "settings.java_path".to_string()), ("java_args".to_string(), "settings.java_args".to_string()), ("auto_hide_sidebar".to_string(), "settings.auto_hide_sidebar".to_string()), + ("disable_server_ads".to_string(), "settings.disable_server_ads".to_string()), ] } diff --git a/src-tauri/src/core/utils/archive.rs b/src-tauri/src/core/utils/archive.rs index ed188201..1589cbc6 100644 --- a/src-tauri/src/core/utils/archive.rs +++ b/src-tauri/src/core/utils/archive.rs @@ -17,13 +17,16 @@ pub fn unzip( if unzip_path.exists() { log_debug!( - "Directory {} exists, will overwrite contents", + "Directory {} exists, wiping for clean re-extract", unzip_path.display() ); - } else { - log_debug!("Creating unzip directory: {}", unzip_path.display()); - fs::create_dir_all(unzip_path).map_err(|e| e.to_string())?; + fs::remove_dir_all(unzip_path).map_err(|e| { + log_error!("Failed to wipe unzip dir {}: {}", unzip_path.display(), e); + e.to_string() + })?; } + log_debug!("Creating unzip directory: {}", unzip_path.display()); + fs::create_dir_all(unzip_path).map_err(|e| e.to_string())?; if !zip_path.exists() { log_error!( diff --git a/src-tauri/src/core/utils/dpi.rs b/src-tauri/src/core/utils/dpi.rs index 9fc29eb6..2460d08f 100644 --- a/src-tauri/src/core/utils/dpi.rs +++ b/src-tauri/src/core/utils/dpi.rs @@ -157,12 +157,12 @@ pub fn download_dpi_bypass() -> Result<(), String> { log_info!("Downloading DPI bypass package from {}", download_url); - let rt = tokio::runtime::Builder::new_current_thread() + let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .map_err(|e| format!("Failed to create Tokio runtime: {}", e))?; - rt.block_on(async { DATA.download(&download_url).await }) + rt.block_on(DATA.download(&download_url)) .map_err(|e| format!("Failed to download DPI package: {}", e))?; Ok(()) diff --git a/src-tauri/src/core/utils/globals.rs b/src-tauri/src/core/utils/globals.rs index a45e7003..35c56c1f 100644 --- a/src-tauri/src/core/utils/globals.rs +++ b/src-tauri/src/core/utils/globals.rs @@ -5,7 +5,7 @@ use std::{fs, path::PathBuf, sync::LazyLock}; use crate::{core::network::servers::Server, log_debug, log_info}; /// The internal codename for this version of the application. -pub static CODENAME: &str = "Refresh"; +pub static CODENAME: &str = "Horizon"; /// The current API version string. pub static API_VERSION: &str = "v1"; @@ -73,6 +73,14 @@ pub static LIBRARIES_FOLDER: &str = "libraries"; pub static LIBRARIES_FABRIC_FOLDER: &str = "libraries-fabric"; /// Folder for legacy game libraries. pub static LIBRARIES_LEGACY_FOLDER: &str = "libraries-legacy"; +/// Folder for minimal 1.8.9 vanilla libraries (no Forge). +pub static LIBRARIES_VA1_8_9_FOLDER: &str = "libraries-va1.8.9"; +/// Folder for minimal 1.8.9 vanilla libraries with ViaVersion 5.3.0. +pub static LIBRARIES_VA1_8_9_VIA53_FOLDER: &str = "libraries-va1.8.9-via53"; +/// Folder for minimal 1.8.9 vanilla libraries with ViaVersion 5.7.1. +pub static LIBRARIES_VA1_8_9_VIA57_FOLDER: &str = "libraries-va1.8.9-via57"; +/// Folder for minimal 1.8.9 vanilla libraries with ViaVersion 5.11.0. +pub static LIBRARIES_VA1_8_9_VIA511_FOLDER: &str = "libraries-va1.8.9-via511"; /// Folder for standard native libraries. pub static NATIVES_FOLDER: &str = "natives"; /// Folder for Linux-specific native libraries. @@ -85,6 +93,10 @@ pub static NATIVES_MACOS_ARM64_FOLDER: &str = "natives-macos-arm64"; pub static NATIVES_LEGACY_FOLDER: &str = "natives-legacy"; /// Folder for legacy Linux native libraries. pub static NATIVES_LEGACY_LINUX_FOLDER: &str = "natives-legacy-linux"; +/// Folders for minimal 1.8.9 vanilla native libraries (per platform). +pub static NATIVES_VA1_8_9_LINUX_FOLDER: &str = "natives-va1.8.9-linux"; +pub static NATIVES_VA1_8_9_MACOS_FOLDER: &str = "natives-va1.8.9-macos"; +pub static NATIVES_VA1_8_9_WINDOWS_FOLDER: &str = "natives-va1.8.9-windows"; /// Folder for Fabric-specific native libraries. pub static NATIVES_FABRIC_FOLDER: &str = "natives-fabric"; @@ -99,6 +111,14 @@ pub static LIBRARIES_ZIP: &str = "misc/libraries.zip"; pub static LIBRARIES_FABRIC_ZIP: &str = "misc/libraries-fabric.zip"; /// ZIP file containing legacy game libraries. pub static LIBRARIES_LEGACY_ZIP: &str = "misc/libraries-legacy.zip"; +/// ZIP file containing minimal 1.8.9 vanilla libraries (no Forge). +pub static LIBRARIES_VA1_8_9_ZIP: &str = "misc/libraries-va1.8.9.zip"; +/// ZIP file containing 1.8.9 libraries with ViaVersion 5.3.0. +pub static LIBRARIES_VA1_8_9_VIA53_ZIP: &str = "misc/libraries-va1.8.9-via53.zip"; +/// ZIP file containing 1.8.9 libraries with ViaVersion 5.7.1. +pub static LIBRARIES_VA1_8_9_VIA57_ZIP: &str = "misc/libraries-va1.8.9-via57.zip"; +/// ZIP file containing 1.8.9 libraries with ViaVersion 5.11.0. +pub static LIBRARIES_VA1_8_9_VIA511_ZIP: &str = "misc/libraries-va1.8.9-via511.zip"; /// ZIP file containing standard native libraries. pub static NATIVES_ZIP: &str = "misc/natives.zip"; /// ZIP file containing Linux-specific native libraries. @@ -111,6 +131,10 @@ pub static NATIVES_MACOS_ARM64_ZIP: &str = "misc/natives-macos-arm64.zip"; pub static NATIVES_LEGACY_ZIP: &str = "misc/natives-legacy.zip"; /// ZIP file containing legacy Linux native libraries. pub static NATIVES_LEGACY_LINUX_ZIP: &str = "misc/natives-legacy-linux.zip"; +/// ZIP file containing sub-set legacy native libraries (per platform). +pub static NATIVES_VA1_8_9_LINUX_ZIP: &str = "misc/natives-va1.8.9-linux.zip"; +pub static NATIVES_VA1_8_9_MACOS_ZIP: &str = "misc/natives-va1.8.9-macos.zip"; +pub static NATIVES_VA1_8_9_WINDOWS_ZIP: &str = "misc/natives-va1.8.9-windows.zip"; /// Folder where Minecraft version JARs are stored. pub static MINECRAFT_VERSIONS_FOLDER: &str = "minecraft-versions"; @@ -130,6 +154,20 @@ pub static OVERLAY_FILE: &str = if IS_LINUX { "CollapseOverlay.dll" }; +pub static TITLEBAR_FILE: &str = if IS_LINUX { + "libCollapseTitlebar.so" +} else if IS_MACOS { + "libCollapseTitlebar.dylib" +} else { + "libCollapseTitlebar.dll" +}; + +pub static TITLEBAR_BRANDING_MARKER: &str = "@CollapseLoader"; + +pub static SKIP_TITLEBAR_BRANDING: LazyLock = LazyLock::new(|| { + parse_env_bool("SKIP_TITLEBAR_BRANDING") +}); + /// The IRC server host and port. pub static IRC_HOST: LazyLock = LazyLock::new(|| { if let Ok(url) = std::env::var("FORCE_IRC") { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 91bc4ca1..29c37fab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -147,6 +147,7 @@ pub fn run() { // client commands commands::clients::add_custom_client, commands::clients::delete_client, + commands::clients::detect_custom_client, commands::clients::detect_main_class, commands::clients::download_client_only, commands::clients::get_app_logs, @@ -277,7 +278,7 @@ pub fn run() { } let app_handle = app.handle(); - *APP_HANDLE.lock().unwrap() = Some(app_handle.clone()); + *APP_HANDLE.lock().unwrap_or_else(|e| e.into_inner()) = Some(app_handle.clone()); let startup_metadata = StartupMetadata::from_env(); startup_metadata.configure_main_window(app_handle); @@ -333,7 +334,9 @@ pub fn run() { }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { - let settings = crate::core::storage::settings::SETTINGS.lock().unwrap(); + let settings = crate::core::storage::settings::SETTINGS + .lock() + .unwrap_or_else(|e| e.into_inner()); if settings.close_to_tray.value { api.prevent_close(); let _ = window.hide(); diff --git a/src-tauri/src/tests/manager_tests.rs b/src-tauri/src/tests/manager_tests.rs index 341d3500..f8e55bda 100644 --- a/src-tauri/src/tests/manager_tests.rs +++ b/src-tauri/src/tests/manager_tests.rs @@ -114,7 +114,7 @@ fn clients_can_be_found_by_id_after_push() { fn sorting_clients_by_created_at_descending() { use chrono::{TimeZone, Utc}; - let mut clients = vec![ + let mut clients = [ Client { id: 1, created_at: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(), diff --git a/src-tauri/src/tests/settings_tests.rs b/src-tauri/src/tests/settings_tests.rs index 084df334..c6d9647d 100644 --- a/src-tauri/src/tests/settings_tests.rs +++ b/src-tauri/src/tests/settings_tests.rs @@ -5,13 +5,13 @@ fn default_settings_have_expected_values() { let s = Settings::default(); assert_eq!(s.ram.value, 2048u32); - assert_eq!(s.ram.show, true); + assert!(s.ram.show); assert_eq!(s.theme.value, "dark".to_string()); - assert_eq!(s.theme.show, false); + assert!(!s.theme.show); assert_eq!(s.language.value, "en".to_string()); - assert_eq!(s.language.show, true); + assert!(s.language.show); } #[test] @@ -26,8 +26,8 @@ fn from_input_applies_visibility_defaults_and_sets_path() { let s = Settings::from_input(input, path.clone()); // visibility defaults should be applied (ram true, theme false) - assert_eq!(s.ram.show, true); - assert_eq!(s.theme.show, false); + assert!(s.ram.show); + assert!(!s.theme.show); // config_path should be set to provided path assert_eq!(s.config_path, path); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 67c5741f..2c3e3000 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CollapseLoader", - "version": "1.2.2", + "version": "1.3.0", "identifier": "org.collapseloader", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/core/SpotlightSearch.vue b/src/components/core/SpotlightSearch.vue index 2482b979..a9815a32 100644 --- a/src/components/core/SpotlightSearch.vue +++ b/src/components/core/SpotlightSearch.vue @@ -17,6 +17,7 @@ import { ChevronRight, CheckCircle, Store, + Database, } from "@lucide/vue"; import type { Client } from "@shared/types/ui"; @@ -52,6 +53,7 @@ const tabItems = [ }, { id: "account", icon: User, labelKey: "navigation.account" }, { id: "news", icon: Newspaper, labelKey: "navigation.news" }, + { id: "hf_clients", icon: Database, labelKey: "navigation.hf_clients" }, { id: "marketplace", icon: Store, labelKey: "navigation.marketplace" }, { id: "custom_clients", diff --git a/src/components/customization/AccentColorPicker.vue b/src/components/customization/AccentColorPicker.vue index 6dba3a14..14fc5f8e 100644 --- a/src/components/customization/AccentColorPicker.vue +++ b/src/components/customization/AccentColorPicker.vue @@ -77,9 +77,9 @@ import { Palette, RotateCcw, Check } from "@lucide/vue"; const { t } = useI18n(); -const props = defineProps<{ +defineProps<{ primaryColor: string | null; - themeMode: "dark" | "light" | "schedule"; + themeMode: "dark" | "light" | "system" | "schedule"; }>(); defineEmits<{ diff --git a/src/components/customization/ThemeModeSelector.vue b/src/components/customization/ThemeModeSelector.vue index 36288bb6..ea1cfd06 100644 --- a/src/components/customization/ThemeModeSelector.vue +++ b/src/components/customization/ThemeModeSelector.vue @@ -86,6 +86,38 @@ + + + + + +
+ +
+ + +
+
+ +
+ +
+ + +
diff --git a/src/features/clients/modals/EditCustomClientModal.vue b/src/features/clients/modals/EditCustomClientModal.vue index 9754e08a..98452ead 100644 --- a/src/features/clients/modals/EditCustomClientModal.vue +++ b/src/features/clients/modals/EditCustomClientModal.vue @@ -1,6 +1,7 @@ diff --git a/src/views/HfClients.vue b/src/views/HfClients.vue new file mode 100644 index 00000000..9fadf206 --- /dev/null +++ b/src/views/HfClients.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/src/views/News.vue b/src/views/News.vue index 84483344..a7727e62 100644 --- a/src/views/News.vue +++ b/src/views/News.vue @@ -18,13 +18,24 @@
+
-
+ + +
>({}); +const showHfApi = ref(false); let observer: IntersectionObserver | null = null; +const DISALLOWED_TAGS = new Set([ + "script", "style", "iframe", "object", "embed", + "form", "input", "textarea", "select", "button", + "link", "meta", "base", "applet", +]); + +function sanitizeHtml(dirty: string): string { + const doc = new DOMParser().parseFromString(dirty, "text/html"); + const walk = (el: Element) => { + for (const child of Array.from(el.children)) { + if (DISALLOWED_TAGS.has(child.tagName.toLowerCase())) { + child.remove(); + continue; + } + for (const attr of Array.from(child.attributes)) { + if (/^on/i.test(attr.name)) child.removeAttribute(attr.name); + if (/^\s*javascript\s*:/i.test(attr.value) && (attr.name === "href" || attr.name === "src" || attr.name === "action")) { + child.removeAttribute(attr.name); + } + } + walk(child); + } + }; + walk(doc.body); + return doc.body.innerHTML; +} + const emit = defineEmits<{ "change-view": [view: string]; "unread-count-updated": [count: number]; diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 8c41c761..938013a7 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -34,6 +34,7 @@ import { HardDrive, RefreshCcw, PanelRightClose, + Megaphone, } from "@lucide/vue"; import { useToast } from "@shared/composables/useToast"; import type { ToastPosition } from "@shared/types/toast"; @@ -123,6 +124,7 @@ const filteredSettingsEntries = computed(() => { "irc_chat", "hash_verify", "sync_client_settings", + "disable_server_ads", "dpi_bypass", "minimize_to_tray_on_launch", "close_to_tray", @@ -558,6 +560,10 @@ const getFormattedLabel = (key: string) => { return t("settings.auto_hide_sidebar"); } + if (key === "disable_server_ads") { + return t("settings.disable_server_ads"); + } + if (key === "java_path") { return "Custom Java Path"; } @@ -1043,6 +1049,10 @@ const handleToastPositionChange = (position: string) => { v-if="key === 'auto_hide_sidebar'" class="w-5 h-5 text-primary" /> +