From 075b451ecc1301ca9604ff4eab0ad84f5506571b Mon Sep 17 00:00:00 2001 From: W1xced-io <266015510+W1xced-io@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:44:25 +0300 Subject: [PATCH 01/21] feat(theme): add system theme mode that follows OS preference skip-ci --- .../customization/AccentColorPicker.vue | 4 +- .../customization/ThemeModeSelector.vue | 82 ++++++++++++++++++- src/composables/useAppInit.ts | 14 ++++ src/services/i18n/locales/en.json | 3 + src/services/i18n/locales/pl.json | 3 + src/services/i18n/locales/ru.json | 3 + src/services/i18n/locales/ua.json | 3 + src/services/i18n/locales/zh_cn.json | 3 + src/services/theme/themeService.ts | 49 +++++++++++ src/utils/settings.ts | 14 ++++ src/views/Customization.vue | 43 +++++++++- 11 files changed, 213 insertions(+), 8 deletions(-) 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 @@ + + + +
Computing...
@@ -127,11 +225,8 @@ def compute_md5(filepath): - + @@ -142,12 +237,21 @@ def compute_md5(filepath):
- -
- - - - +
+ + + + +
+ + + + +
+ +
@@ -160,6 +264,7 @@ def compute_md5(filepath):
""" @@ -256,7 +431,7 @@ def compute_md5(filepath): class Handler(BaseHTTPRequestHandler): def log_message(self, format, *args): - pass + print(f"[SERVER] {format % args}") def _json(self, code, data): body = json.dumps(data).encode() @@ -272,7 +447,10 @@ def _read_body(self): def do_GET(self): if self.path == "/" or self.path == "/index.html": - body = HTML.replace("CDN_ROOT_PLACEHOLDER", CDN_ROOT).encode() + versions = scan_cdn_client_versions(CDN_ROOT) + deps = scan_local_deps(CDN_ROOT) + payload = json.dumps({"versions": versions, "deps": deps}).replace("\\", "\\\\").replace("'", "\\'") + 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))) @@ -283,12 +461,32 @@ def do_GET(self): def do_POST(self): try: - if self.path == "/api/md5": + 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 or not os.path.isfile(filepath): - self._json(200, {"error": f"File not found: {filepath}"}) + 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}) @@ -342,12 +540,33 @@ def do_POST(self): } 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]) + 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: + dep = _find_dep(local_other, "baritone") + if dep: deps.append(dep) + elif version in BARITONE_DEPS: + deps.append(BARITONE_DEPS[version]) entry["dependencies"] = deps elif client_type == "forge": entry["dependencies"] = [] @@ -369,7 +588,7 @@ def main(): 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) + print(f"CDN root: {CDN_ROOT}") try: server.serve_forever() except KeyboardInterrupt: From 08684ca20ebb09780801cb74cafbd627272ec30f Mon Sep 17 00:00:00 2001 From: dest4590 Date: Thu, 23 Jul 2026 22:21:47 +0300 Subject: [PATCH 03/21] chore(deps): updated deps, cleanup code and directories --- .gitignore | 4 +- AGENTS.md | 111 +- package-lock.json | 960 ++++++++---------- package.json | 32 +- .../__pycache__/new_client.cpython-313.pyc | Bin 18643 -> 0 bytes .../__pycache__/scripts_gui.cpython-313.pyc | Bin 29998 -> 0 bytes scripts/clients/gui_template.html | 524 ++++++++++ scripts/clients/md5.cjs | 36 + scripts/{ => clients}/md5.py | 0 scripts/clients/new_client.cjs | 229 +++++ scripts/{ => clients}/new_client.py | 0 scripts/clients/scripts_gui.py | 309 ++++++ scripts/md5.cjs | 36 - scripts/new_client.cjs | 145 --- scripts/scripts_gui.py | 600 ----------- src-tauri/Cargo.lock | 648 ++++++------ src-tauri/Cargo.toml | 20 +- 17 files changed, 1955 insertions(+), 1699 deletions(-) delete mode 100644 scripts/__pycache__/new_client.cpython-313.pyc delete mode 100644 scripts/__pycache__/scripts_gui.cpython-313.pyc create mode 100644 scripts/clients/gui_template.html create mode 100644 scripts/clients/md5.cjs rename scripts/{ => clients}/md5.py (100%) mode change 100755 => 100644 create mode 100644 scripts/clients/new_client.cjs rename scripts/{ => clients}/new_client.py (100%) mode change 100755 => 100644 create mode 100644 scripts/clients/scripts_gui.py delete mode 100644 scripts/md5.cjs delete mode 100644 scripts/new_client.cjs delete mode 100755 scripts/scripts_gui.py diff --git a/.gitignore b/.gitignore index 28b197fb..bf7eb1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ target/ .DS_Store/ .opencode/ -opencode.json \ No newline at end of file +opencode.json + +__pycache__/ \ 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..0a4d0c29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,50 +1,50 @@ { "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.26.0", "@stomp/stompjs": "^7.3.0", - "@supabase/supabase-js": "^2.110.5", + "@supabase/supabase-js": "^2.110.8", "@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", "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.40", + "vue-i18n": "11.4.7" }, "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", + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", "@vitejs/plugin-vue": "6.0.8", "axios-mock-adapter": "^2.1.0", - "daisyui": "^5.6.18", + "daisyui": "^5.7.0", "eslint": "^10.7.0", - "eslint-plugin-vue": "10.9.2", + "eslint-plugin-vue": "10.10.0", "jsdom": "^29.1.1", - "prettier": "3.9.5", - "tailwindcss": "4.3.2", + "prettier": "3.9.6", + "tailwindcss": "4.3.3", "typescript": "^5.8.3", - "vite": "^8.1.4", + "vite": "^8.1.5", "vitest": "^4.1.10", "vue-eslint-parser": "^10.4.1", - "vue-tsc": "3.3.7" + "vue-tsc": "3.3.8" } }, "node_modules/@alloc/quick-lru": { @@ -569,14 +569,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.7", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz", + "integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==", "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.7", + "@intlify/message-compiler": "11.4.7", + "@intlify/shared": "11.4.7" }, "engines": { "node": ">= 22" @@ -586,13 +586,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.7", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz", + "integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.6", - "@intlify/shared": "11.4.6" + "@intlify/core-base": "11.4.7", + "@intlify/shared": "11.4.7" }, "engines": { "node": ">= 22" @@ -602,12 +602,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.7", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz", + "integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.4.6", + "@intlify/shared": "11.4.7", "source-map-js": "^1.0.2" }, "engines": { @@ -618,9 +618,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.7", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz", + "integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==", "license": "MIT", "engines": { "node": ">= 22" @@ -679,9 +679,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.26.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.26.0.tgz", + "integrity": "sha512-jz00zLm8+i7VzBuQTbmjChIvIKehaIK5YbOVC+bab7qTh4JTkCd0XJQ4l5TY57gT08fMPVlvUITROB/vm/xImA==", "license": "ISC", "peerDependencies": { "vue": ">=3.0.1" @@ -716,9 +716,9 @@ } }, "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==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -726,9 +726,9 @@ } }, "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -743,9 +743,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -760,9 +760,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -777,9 +777,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -794,9 +794,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -811,13 +811,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -828,13 +831,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -845,13 +851,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -862,13 +871,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -879,13 +891,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -896,13 +911,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -913,9 +931,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -930,9 +948,9 @@ } }, "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==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -949,9 +967,9 @@ } }, "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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -966,9 +984,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.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -1003,9 +1021,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.110.8", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.8.tgz", + "integrity": "sha512-TQ5neTUDX2C2WmyYa03yGhLMkhdE/SkHXtK8/qxO/APUy3rsymsJCBP48p4jcN6iO2G0ow6RRexQd2mX+dSyJg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1015,9 +1033,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.110.8", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.110.8.tgz", + "integrity": "sha512-5yB9TLYzvv2oSQxwb0gamEvIAsuH66pVt7AM/pz03S7wN6ehD34GNgbShrccetqPedXQSz7e/1hAJ9NeEhoZVg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1027,15 +1045,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.110.8", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.110.8.tgz", + "integrity": "sha512-QeRROxl1PpOZw5Jzi7BwdN9icsycMrLlCCvsjS0hYLW+nZoaT46zdagz/glJirj8jHF4jSd5Jyipuae2cBClCw==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1045,12 +1063,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.110.8", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.110.8.tgz", + "integrity": "sha512-mwX7ituX6O31fLf+0g65rpLlNxqgnMaPltPsQwzox6jfmbfVl3tCxXrfr3HEsQcCRjpjuJG1+A0vFzP1yVjKHA==", "license": "MIT", "dependencies": { - "@supabase/phoenix": "0.4.4", + "@supabase/phoenix": "0.4.5", "tslib": "2.8.1" }, "engines": { @@ -1058,9 +1076,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.110.8", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.110.8.tgz", + "integrity": "sha512-CcfhkZFBLxsthgUabZKxwfsoXdrikIGsL3LsGoV3FZTqCMx/s1y49taT4jT/oya5+1IuB0sFFHw6pF0o0iJniQ==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -1071,65 +1089,65 @@ } }, "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.110.8", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.110.8.tgz", + "integrity": "sha512-E5qzoe74zhJRv4wRcbO9eMYzeQDb/+h6c603pL8shcxLGBjTKsIF7XXj05IcNj23TLDgJN1WkMw7mwAPyu5dZg==", "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.110.8", + "@supabase/functions-js": "2.110.8", + "@supabase/postgrest-js": "2.110.8", + "@supabase/realtime-js": "2.110.8", + "@supabase/storage-js": "2.110.8" }, "engines": { "node": ">=22.0.0" } }, "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 +1162,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 +1179,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 +1196,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 +1213,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 +1230,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 +1250,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 +1270,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 +1290,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 +1310,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 +1340,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 +1357,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 +1374,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" @@ -1600,9 +1630,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" @@ -1703,17 +1733,17 @@ "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==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "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", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1726,23 +1756,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, - "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/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "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" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1756,22 +1786,16 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "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/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "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" + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1784,16 +1808,15 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "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/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1801,15 +1824,12 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" } }, - "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/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -1823,45 +1843,17 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "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/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "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" - }, - "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" - } - }, - "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==", - "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", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "engines": { @@ -1872,84 +1864,35 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, - "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==", - "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/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==", - "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/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/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "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" } }, - "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==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "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", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1967,34 +1910,18 @@ "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==", + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "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" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, - "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" }, @@ -2003,49 +1930,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "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==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2229,13 +2125,13 @@ } }, "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==", + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.39", + "@vue/shared": "3.5.40", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -2260,29 +2156,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.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" } }, "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.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", "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", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", "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 +2189,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.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "node_modules/@vue/devtools-api": { @@ -2309,9 +2205,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.8", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.8.tgz", + "integrity": "sha512-ieGT8jJdhhy0mGzStZhsg/qPw5bQZJg5yF+3+XU6saf4sM7yo9ZXy3h+nCwrm2+b4qS/SypkNdR2jAF3uei9tA==", "dev": true, "license": "MIT", "dependencies": { @@ -2325,53 +2221,51 @@ } }, "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.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.39" + "@vue/shared": "3.5.40" } }, "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.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" } }, "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.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/runtime-core": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", "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.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39" - }, - "peerDependencies": { - "vue": "3.5.39" + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" } }, "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.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", "license": "MIT" }, "node_modules/acorn": { @@ -2606,9 +2500,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.0", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.0.tgz", + "integrity": "sha512-2/kYbxaKtv349lPrTyxMKC9SHsyA7fBULMSabJljDE82D079cjqz+UyAzsogWgy4sTs5NDvD000acfcFqbO1XA==", "dev": true, "license": "MIT", "funding": { @@ -2680,9 +2574,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 +2597,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.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2854,18 +2748,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" @@ -3468,16 +3362,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", @@ -3885,12 +3769,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 +3792,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.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -4073,9 +3957,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.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "funding": [ { "type": "opencollective", @@ -4092,7 +3976,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4101,9 +3985,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.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", "dependencies": { @@ -4125,9 +4009,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 +4054,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.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4186,21 +4070,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-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/saxes": { @@ -4217,9 +4101,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": { @@ -4296,9 +4180,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" }, @@ -4492,16 +4376,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.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { @@ -4667,16 +4551,16 @@ "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.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", "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.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" }, "peerDependencies": { "typescript": "*" @@ -4725,14 +4609,14 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz", + "integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==", "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.7", + "@intlify/devtools-types": "11.4.7", + "@intlify/shared": "11.4.7", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -4746,14 +4630,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.8", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.8.tgz", + "integrity": "sha512-xXmYlVQpcwJDWyGlqbHrGVOl1h3UOsASymRibrHc+iy9j/UNnOrOn4u+fntHz4D6Cs74RtapeqVV6CzJeg+UlA==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.3.7" + "@vue/language-core": "3.3.8" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -4775,16 +4659,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", @@ -4864,13 +4738,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..59130256 100644 --- a/package.json +++ b/package.json @@ -25,40 +25,40 @@ }, "dependencies": { "@guolao/vue-monaco-editor": "1.6.0", - "@lucide/vue": "1.24.0", + "@lucide/vue": "1.26.0", "@stomp/stompjs": "^7.3.0", - "@supabase/supabase-js": "^2.110.5", + "@supabase/supabase-js": "^2.110.8", "@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", "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.40", + "vue-i18n": "11.4.7" }, "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", + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", "@vitejs/plugin-vue": "6.0.8", "axios-mock-adapter": "^2.1.0", - "daisyui": "^5.6.18", + "daisyui": "^5.7.0", "eslint": "^10.7.0", - "eslint-plugin-vue": "10.9.2", + "eslint-plugin-vue": "10.10.0", "jsdom": "^29.1.1", - "prettier": "3.9.5", - "tailwindcss": "4.3.2", + "prettier": "3.9.6", + "tailwindcss": "4.3.3", "typescript": "^5.8.3", - "vite": "^8.1.4", + "vite": "^8.1.5", "vitest": "^4.1.10", "vue-eslint-parser": "^10.4.1", - "vue-tsc": "3.3.7" + "vue-tsc": "3.3.8" } } diff --git a/scripts/__pycache__/new_client.cpython-313.pyc b/scripts/__pycache__/new_client.cpython-313.pyc deleted file mode 100644 index fa64a6c604a4cb1b74c789d28d3886425eb064eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18643 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 Vi?Nip^tTNAZwEKwXBokv{|D*ZwbTFr diff --git a/scripts/__pycache__/scripts_gui.cpython-313.pyc b/scripts/__pycache__/scripts_gui.cpython-313.pyc deleted file mode 100644 index c68e43992f681ea39577534c86b69c63193ebf51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29998 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 diff --git a/scripts/clients/gui_template.html b/scripts/clients/gui_template.html new file mode 100644 index 00000000..4c2ca315 --- /dev/null +++ b/scripts/clients/gui_template.html @@ -0,0 +1,524 @@ + + + + + + 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..c8fb57e9 --- /dev/null +++ b/scripts/clients/new_client.cjs @@ -0,0 +1,229 @@ +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.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}`); +} + +main(); diff --git a/scripts/new_client.py b/scripts/clients/new_client.py old mode 100755 new mode 100644 similarity index 100% rename from scripts/new_client.py rename to scripts/clients/new_client.py diff --git a/scripts/clients/scripts_gui.py b/scripts/clients/scripts_gui.py new file mode 100644 index 00000000..8c955eb0 --- /dev/null +++ b/scripts/clients/scripts_gui.py @@ -0,0 +1,309 @@ +#!/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.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.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"} + +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 result + 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, []) + 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}).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", []) + 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 = [] + 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: + dep = _find_dep(local_other, "baritone") + if dep: deps.append(dep) + elif 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}") + 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 525c42b2..00000000 --- a/scripts/new_client.cjs +++ /dev/null @@ -1,145 +0,0 @@ -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.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}`); -} - -main(); diff --git a/scripts/scripts_gui.py b/scripts/scripts_gui.py deleted file mode 100755 index 4372d182..00000000 --- a/scripts/scripts_gui.py +++ /dev/null @@ -1,600 +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 re -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/collapsecdn") -FALLBACK_VERSIONS: dict[str, list[str]] = { - "default": ["1.16.5"], - "fabric": ["1.21.4", "1.21.8", "1.21.11"], - "forge": ["1.8.9"], -} - - -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 scan_cdn_client_versions(cdn_root: str) -> dict[str, list[str]]: - """Scan local CDN misc/minecraft-versions/ to find available MC versions per type.""" - 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 result - 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, []) - return result - -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() - - -import re - -def _find_dep(local_other: dict, keyword: str) -> dict | None: - """Find a dep by keyword in local scanned deps.""" - 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 - - -FABRIC_API_RE = re.compile(r"^fabric-api-([0-9.]+\+\d+\.\d+\.\d+)\.jar$") -OTHER_DEP_RE = re.compile(r"^(.+)\.jar$") - - -def _find_file_by_name(name: str) -> str | None: - """Search CDN for a file by name in common locations.""" - 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: - """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 = {"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 - - -HTML = r""" - - - - -CollapseLoader Scripts - - - -
-

CollapseLoader Scripts

- -
-
MD5 Hash
-
New Client
-
- - -
- -
- - -
-
-
Computing...
- -
- - -
- -
- - -
- - - - - - - -
-
- - - - -
- - - - -
- -
-
- - - - -
-
Processing...
- -
-
- - - -""" - - -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}).replace("\\", "\\\\").replace("'", "\\'") - 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", []) - 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 = [] - 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: - dep = _find_dep(local_other, "baritone") - if dep: deps.append(dep) - elif 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}") - print(f"CDN root: {CDN_ROOT}") - 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..fccd8ca5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[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", @@ -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.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" dependencies = [ "serde_core", ] @@ -467,7 +473,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -482,9 +488,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -506,7 +512,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.23.5", + "uuid 1.24.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.0", "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.19", "tokio", - "uuid 1.23.5", + "uuid 1.24.0", "windows 0.62.2", "winreg 0.56.0", "zbus", @@ -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", ] @@ -782,18 +788,18 @@ dependencies = [ [[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", @@ -1004,7 +1010,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_repr", - "thiserror 2.0.18", + "thiserror 2.0.19", "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", @@ -1028,7 +1034,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1051,7 +1057,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1143,14 +1149,14 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[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.3+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]] @@ -1253,9 +1259,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" @@ -1317,13 +1323,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 +1361,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 +1371,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[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 +1388,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -1401,32 +1407,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-io", @@ -1624,7 +1630,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 +1658,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1667,9 +1673,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 +1737,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1826,9 +1832,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 +1842,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 +1861,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" 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", @@ -2198,7 +2204,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link 0.2.1", ] @@ -2213,7 +2219,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2241,7 +2247,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 +2262,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", ] @@ -2304,7 +2309,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 +2346,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 +2371,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -2408,23 +2413,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.24.0", ] [[package]] @@ -2487,9 +2494,9 @@ dependencies = [ [[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 +2505,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,7 +2520,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -2533,7 +2540,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "raw-window-handle", - "thiserror 2.0.18", + "thiserror 2.0.19", "versions", "wfd", "which", @@ -2546,7 +2553,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 +2597,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", @@ -2645,7 +2652,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2664,7 +2671,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 +2692,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 +2703,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 +2714,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 +2725,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 +2758,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 +2770,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 +2798,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 +2821,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 +2843,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 +2855,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 +2886,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", @@ -3064,7 +3071,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3101,13 +3108,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", ] @@ -3131,7 +3138,7 @@ 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", @@ -3214,7 +3221,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 +3250,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 +3280,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -3305,7 +3303,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -3327,9 +3325,9 @@ dependencies = [ [[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 +3346,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 +3411,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 +3422,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[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 +3459,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,7 +3588,7 @@ 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", @@ -3599,9 +3597,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "once_cell", @@ -3625,9 +3623,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", @@ -3674,9 +3672,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 +3706,7 @@ dependencies = [ "serde", "serde_json", "url", - "uuid 1.23.5", + "uuid 1.24.0", ] [[package]] @@ -3744,7 +3742,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3759,7 +3757,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 +3780,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 +3805,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 +3827,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 +3853,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 +3871,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 +3900,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", @@ -3922,14 +3920,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 +3949,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4014,15 +4012,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", @@ -4175,9 +4173,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -4201,7 +4210,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4225,7 +4234,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 +4268,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", @@ -4301,7 +4310,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4351,7 +4360,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tray-icon", "url", @@ -4400,12 +4409,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.19", "time", "url", - "uuid 1.23.5", + "uuid 1.24.0", "walkdir", ] @@ -4418,16 +4427,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 +4462,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "url", "windows-registry 0.5.3", @@ -4462,9 +4471,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 +4483,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", ] @@ -4497,8 +4506,8 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.3+spec-1.1.0", "url", ] @@ -4510,13 +4519,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.19", "time", "url", ] @@ -4537,7 +4546,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "windows 0.61.3", "zbus", @@ -4553,7 +4562,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin-deep-link", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "windows-sys 0.60.2", @@ -4578,7 +4587,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "webkit2gtk", "webview2-com", @@ -4641,11 +4650,11 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "thiserror 2.0.19", + "toml 1.1.3+spec-1.1.0", "url", "urlpattern", - "uuid 1.23.5", + "uuid 1.24.0", "walkdir", ] @@ -4657,17 +4666,16 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.3+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.19", "windows 0.61.3", "windows-version", ] @@ -4687,12 +4695,11 @@ dependencies = [ [[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 +4713,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4721,25 +4728,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.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "js-sys", @@ -4758,9 +4765,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 +4794,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 +4809,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 +4826,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4840,13 +4847,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 +4888,9 @@ dependencies = [ [[package]] name = "toml" -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 = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -4890,7 +4898,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,14 +4954,14 @@ 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]] @@ -4962,14 +4970,14 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" 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 +5000,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 +5043,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5065,7 +5073,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -5194,12 +5202,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 +5219,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.5" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -5305,9 +5307,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 +5320,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 +5330,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 +5340,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 +5375,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 +5395,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 +5451,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 +5480,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5487,7 +5489,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.19", "windows 0.61.3", "windows-core 0.61.2", ] @@ -5659,7 +5661,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5670,7 +5672,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6038,9 +6040,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", ] @@ -6116,7 +6118,7 @@ dependencies = [ "sha2 0.10.9", "soup3", "tao-macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6167,15 +6169,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.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" dependencies = [ "async-broadcast", "async-executor", @@ -6199,9 +6201,9 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "uuid 1.23.5", + "uuid 1.24.0", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -6209,14 +6211,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.17.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zbus_names", "zvariant", "zvariant_utils", @@ -6224,33 +6226,33 @@ 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 = "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 +6272,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -6310,7 +6312,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6342,15 +6344,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[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,28 +6396,28 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.13.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.13.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "zvariant_utils", ] @@ -6428,6 +6430,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.118", - "winnow 1.0.3", + "syn 2.0.119", + "winnow 1.0.4", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 91234249..5dbc2c2d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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,25 +31,25 @@ 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.24.0", features = ["v4"] } chrono = { version = "0.4.45", features = ["serde"] } paste = "1.0.15" open = "5.4.0" 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.33" +base64 = "0.23.0" +tauri-plugin-dialog = "2.7.2" dotenvy = "0.15.7" tauri-plugin-fs = "2.5.1" -thiserror = "2.0.18" +thiserror = "2.0.19" native-dialog = "0.9.7" sysinfo = "0.39.6" socket2 = "0.6.5" sha2 = "0.11.0" -regex = "1.13.0" +regex = "1.13.1" flate2 = "1.1.9" [target.'cfg(target_os = "macos")'.dependencies] @@ -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.18.0", default-features = false, features = ["tokio"] } [target.'cfg(windows)'.dependencies] junction = "2.0.0" From 9f1bb0f416a76479485c7e5c5db62cf662615db4 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Thu, 23 Jul 2026 23:19:19 +0300 Subject: [PATCH 04/21] refactor: rust code refactor & borrowing fixes --- src-tauri/src/commands/clients.rs | 1350 ----------------- src-tauri/src/commands/clients/custom.rs | 198 +++ src-tauri/src/commands/clients/general.rs | 556 +++++++ src-tauri/src/commands/clients/mod.rs | 66 + src-tauri/src/commands/clients/mods.rs | 212 +++ src-tauri/src/commands/clients/ram.rs | 73 + src-tauri/src/commands/clients/shortcuts.rs | 214 +++ src-tauri/src/commands/mod_builds.rs | 44 +- src-tauri/src/commands/network.rs | 17 +- src-tauri/src/commands/presets.rs | 46 +- src-tauri/src/commands/report/helpers.rs | 303 ++++ .../src/commands/{report.rs => report/mod.rs} | 305 +--- src-tauri/src/commands/settings.rs | 282 ++-- src-tauri/src/commands/updater.rs | 9 +- src-tauri/src/commands/utils.rs | 607 -------- src-tauri/src/commands/utils/data_folder.rs | 178 +++ src-tauri/src/commands/utils/mod.rs | 287 ++++ src-tauri/src/commands/utils/tray.rs | 154 ++ src-tauri/src/core/clients/client/launch.rs | 14 +- .../src/core/clients/client/requirements.rs | 98 +- src-tauri/src/core/network/servers.rs | 5 +- src-tauri/src/core/state.rs | 59 +- src-tauri/src/core/storage/custom_clients.rs | 36 +- src-tauri/src/core/storage/data.rs | 35 +- src-tauri/src/core/utils/dpi.rs | 4 +- src-tauri/src/lib.rs | 6 +- src-tauri/src/tests/manager_tests.rs | 2 +- src-tauri/src/tests/settings_tests.rs | 10 +- 28 files changed, 2635 insertions(+), 2535 deletions(-) delete mode 100644 src-tauri/src/commands/clients.rs create mode 100644 src-tauri/src/commands/clients/custom.rs create mode 100644 src-tauri/src/commands/clients/general.rs create mode 100644 src-tauri/src/commands/clients/mod.rs create mode 100644 src-tauri/src/commands/clients/mods.rs create mode 100644 src-tauri/src/commands/clients/ram.rs create mode 100644 src-tauri/src/commands/clients/shortcuts.rs create mode 100644 src-tauri/src/commands/report/helpers.rs rename src-tauri/src/commands/{report.rs => report/mod.rs} (56%) delete mode 100644 src-tauri/src/commands/utils.rs create mode 100644 src-tauri/src/commands/utils/data_folder.rs create mode 100644 src-tauri/src/commands/utils/mod.rs create mode 100644 src-tauri/src/commands/utils/tray.rs 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..7e2dfd07 --- /dev/null +++ b/src-tauri/src/commands/clients/custom.rs @@ -0,0 +1,198 @@ +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] +#[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, + 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); + 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, + 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 = 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..048e102c --- /dev/null +++ b/src-tauri/src/commands/clients/general.rs @@ -0,0 +1,556 @@ +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::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(()) + } + } +} + +#[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 = 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..edda2da1 --- /dev/null +++ b/src-tauri/src/commands/clients/mods.rs @@ -0,0 +1,212 @@ +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 { + custom_client + .file_path + .parent() + .ok_or_else(|| "Cannot determine client folder".to_string()) + .map(|p| p.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..1b1aa544 100644 --- a/src-tauri/src/commands/network.rs +++ b/src-tauri/src/commands/network.rs @@ -1,5 +1,6 @@ use crate::core::network::create_client; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; use std::sync::{Mutex, OnceLock}; use std::time::Duration; use tauri::{AppHandle, Emitter}; @@ -21,13 +22,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) } @@ -236,14 +237,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/clients/client/launch.rs b/src-tauri/src/core/clients/client/launch.rs index a7755941..33293817 100644 --- a/src-tauri/src/core/clients/client/launch.rs +++ b/src-tauri/src/core/clients/client/launch.rs @@ -33,7 +33,7 @@ use crate::{log_debug, log_error, log_info}; impl Client { 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 +65,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,7 +106,7 @@ impl Client { } fn resolve_natives_path(&self) -> PathBuf { - let root = DATA.root_dir.lock().unwrap(); + 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 { @@ -123,7 +123,7 @@ impl Client { } 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, @@ -269,7 +269,11 @@ 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 mut cmd = Command::new(java_bin); diff --git a/src-tauri/src/core/clients/client/requirements.rs b/src-tauri/src/core/clients/client/requirements.rs index dc428193..b3ef1897 100644 --- a/src-tauri/src/core/clients/client/requirements.rs +++ b/src-tauri/src/core/clients/client/requirements.rs @@ -48,7 +48,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 +61,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 +459,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 +476,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); } @@ -571,7 +583,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); @@ -651,7 +663,7 @@ impl Client { let bin_dir = DATA .root_dir .lock() - .unwrap() + .unwrap_or_else(|e| e.into_inner()) .join(self.jdk_folder_name()) .join("bin"); if bin_dir.exists() { @@ -678,7 +690,11 @@ impl Client { } 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 +749,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 +763,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 +806,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 +874,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 +897,11 @@ 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 mut cp_parts = Vec::new(); @@ -870,7 +910,11 @@ impl Client { cp_parts.push(self.get_minecraft_jar_path()); let safe_ver = sanitize_version_for_paths(&self.version); - let fabric_libs_root = DATA.root_dir.lock().unwrap().join(LIBRARIES_FABRIC_FOLDER); + let fabric_libs_root = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FABRIC_FOLDER); let v_libs = fabric_libs_root.join(&safe_ver); cp_parts.extend(collect_jars_recursive(&v_libs, false)); @@ -881,16 +925,26 @@ impl Client { } ClientType::Forge => { cp_parts.push(self.get_minecraft_jar_path()); - let libs = DATA.root_dir.lock().unwrap().join(LIBRARIES_LEGACY_FOLDER); + let libs = DATA + .root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_LEGACY_FOLDER); 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) + DATA.root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_LEGACY_FOLDER) } else { - DATA.root_dir.lock().unwrap().join(LIBRARIES_FOLDER) + DATA.root_dir + .lock() + .unwrap_or_else(|e| e.into_inner()) + .join(LIBRARIES_FOLDER) }; return Ok(format!( 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/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..7afcfd8e 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> { diff --git a/src-tauri/src/core/storage/data.rs b/src-tauri/src/core/storage/data.rs index b0a33d07..49e0a130 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 @@ -741,7 +756,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/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/lib.rs b/src-tauri/src/lib.rs index 91bc4ca1..51bcceab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -277,7 +277,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 +333,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); From 3b4480d5d06dd62ea32630f8bc33627b8e898ad2 Mon Sep 17 00:00:00 2001 From: W1xced-io <266015510+W1xced-io@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:00:39 +0300 Subject: [PATCH 05/21] feat: add HF API clients database to News tab --- src/components/core/SpotlightSearch.vue | 2 + src/features/social/utils/discord.ts | 1 + src/router/views.ts | 2 + src/services/hfClientsService.ts | 97 +++++++ src/services/i18n/locales/en.json | 23 ++ src/services/i18n/locales/pl.json | 23 ++ src/services/i18n/locales/ru.json | 23 ++ src/services/i18n/locales/ua.json | 23 ++ src/services/i18n/locales/zh_cn.json | 23 ++ src/utils/tabs.ts | 1 + src/views/HfClients.vue | 323 ++++++++++++++++++++++++ src/views/News.vue | 22 +- 12 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 src/services/hfClientsService.ts create mode 100644 src/views/HfClients.vue 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/features/social/utils/discord.ts b/src/features/social/utils/discord.ts index ae094a6d..ff1fd313 100644 --- a/src/features/social/utils/discord.ts +++ b/src/features/social/utils/discord.ts @@ -2,6 +2,7 @@ export const DISCORD_STATE_KEYS: Record = { home: "discord.states.browsing_clients", custom_clients: "discord.states.browsing_custom_clients", news: "discord.states.browsing_news", + hf_clients: "discord.states.browsing_hf_clients", settings: "discord.states.configuring_settings", friends: "discord.states.browsing_friends", theme: "discord.states.enjoying_visuals", diff --git a/src/router/views.ts b/src/router/views.ts index 5f3c8640..8d996a50 100644 --- a/src/router/views.ts +++ b/src/router/views.ts @@ -14,10 +14,12 @@ import UserProfileView from "../views/UserProfileView.vue"; import Marketplace from "../views/Marketplace.vue"; import NetworkDebug from "../views/NetworkDebug.vue"; import ChatView from "../views/ChatView.vue"; +import HfClients from "../views/HfClients.vue"; export const views: Record = { home: Home, news: News, + hf_clients: HfClients, settings: Settings, about: About, customization: Customization, diff --git a/src/services/hfClientsService.ts b/src/services/hfClientsService.ts new file mode 100644 index 00000000..d7f0ec0d --- /dev/null +++ b/src/services/hfClientsService.ts @@ -0,0 +1,97 @@ +import { invoke } from "@tauri-apps/api/core"; + +export interface HfClient { + id: string | number; + name: string; + version: string; + client_type: string; + filename: string; + md5_hash: string; + downloads: number; + launches: number; + show: boolean; + working: boolean; + size: number; + created_at: string; + dependencies?: string[]; +} + +const CDN_URLS = [ + "https://huggingface.co/datasets/Collapsecdn/collapsecdn/resolve/main/static/clients.json", + "https://huggingface.co/datasets/Collapsecdn/collapsecdn/resolve/main/static/fabric-clients.json", + "https://huggingface.co/datasets/Collapsecdn/collapsecdn/resolve/main/static/forge-clients.json", +]; + +function tryParse(d: any): any[] { + if (Array.isArray(d)) return d; + if (d == null) return []; + if (typeof d === "string") { + try { + const parsed = JSON.parse(d); + return Array.isArray(parsed) + ? parsed + : Array.isArray(parsed?.data) + ? parsed.data + : []; + } catch { + return []; + } + } + if (Array.isArray(d?.data)) return d.data; + return []; +} + +export const hfClientsService = { + async fetchClients(): Promise<{ + all: HfClient[]; + latest: HfClient[]; + counts: { total: number; fabric: number; forge: number; default: number }; + }> { + const [allData, fabricData, forgeData] = await Promise.all( + CDN_URLS.map((url) => + invoke("api_request", { + method: "GET", + url, + headers: {}, + body: null, + }) + ) + ); + + const rawAll = tryParse(allData); + const fabric = tryParse(fabricData); + const forge = tryParse(forgeData); + + const map = new Map(); + rawAll.forEach((c: any) => map.set(c.id, c)); + fabric.forEach((c: any) => map.set(c.id, c)); + forge.forEach((c: any) => map.set(c.id, c)); + + const all = Array.from(map.values()).filter( + (c: any) => c.show !== false + ); + + const latest = [...all] + .sort( + (a, b) => + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime() + ) + .slice(0, 5); + + const counts = { + total: all.length, + fabric: all.filter( + (c) => c.client_type?.toLowerCase() === "fabric" + ).length, + forge: all.filter( + (c) => c.client_type?.toLowerCase() === "forge" + ).length, + default: all.filter( + (c) => c.client_type?.toLowerCase() === "default" + ).length, + }; + + return { all, latest, counts }; + }, +}; diff --git a/src/services/i18n/locales/en.json b/src/services/i18n/locales/en.json index cac69bab..13783588 100644 --- a/src/services/i18n/locales/en.json +++ b/src/services/i18n/locales/en.json @@ -1116,6 +1116,7 @@ "account": "Account", "about": "About", "news": "News", + "hf_clients": "HF Clients", "customization": "Customization", "custom_clients": "Custom Clients", "marketplace": "Marketplace", @@ -1224,6 +1225,7 @@ "logging_in": "Logging in", "browsing_friends": "Watching what friends do", "browsing_news": "Reading news", + "browsing_hf_clients": "Browsing HF clients", "browsing_custom_clients": "Browsing custom clients" } }, @@ -1251,6 +1253,27 @@ "refresh": "Refresh", "telegram": "Telegram" }, + "hfapi": { + "live": "LIVE", + "subtitle": "HuggingFace CDN – Client database", + "loading": "Loading clients...", + "error": "Error loading clients", + "retry": "Try Again", + "refresh": "Refresh", + "latest_changes": "Latest Changes", + "no_changes": "No changes detected yet...", + "all_clients": "All Clients", + "table_name": "Name", + "table_version": "Version", + "table_hash": "MD5 Hash", + "table_size": "Size", + "status_down": "DOWN", + "total": "Total", + "fabric": "Fabric", + "forge": "Forge", + "default": "Default", + "fetch_failed": "Failed to load clients" + }, "hold_button": { "label": "Hold to begin!", "info_message": "Are you serious?" diff --git a/src/services/i18n/locales/pl.json b/src/services/i18n/locales/pl.json index b9ad3151..b5f20f73 100644 --- a/src/services/i18n/locales/pl.json +++ b/src/services/i18n/locales/pl.json @@ -1099,6 +1099,7 @@ "account": "Konto", "about": "O programie", "news": "Aktualności", + "hf_clients": "Klienci HF", "customization": "Personalizacja", "custom_clients": "Własne klienty", "marketplace": "Marketplace", @@ -1206,6 +1207,7 @@ "logging_in": "Loguje się", "browsing_friends": "Patrzy co robią znajomi", "browsing_news": "Czyta aktualności", + "browsing_hf_clients": "Przegląda klientów HF", "browsing_custom_clients": "Patrzy na własne klienty" } }, @@ -1233,6 +1235,27 @@ "telegram": "Telegram", "time": {} }, + "hfapi": { + "live": "ONLINE", + "subtitle": "HuggingFace CDN – Baza klientów", + "loading": "Ładowanie klientów...", + "error": "Błąd ładowania klientów", + "retry": "Spróbuj ponownie", + "refresh": "Odśwież", + "latest_changes": "Ostatnie zmiany", + "no_changes": "Brak zmian...", + "all_clients": "Wszyscy klienci", + "table_name": "Nazwa", + "table_version": "Wersja", + "table_hash": "MD5 hash", + "table_size": "Rozmiar", + "status_down": "NIEDOSTĘPNY", + "total": "Łącznie", + "fabric": "Fabric", + "forge": "Forge", + "default": "Default", + "fetch_failed": "Nie udało się załadować klientów" + }, "hold_button": { "label": "Przytrzymaj, aby rozpocząć!", "info_message": "Mówisz poważnie?" diff --git a/src/services/i18n/locales/ru.json b/src/services/i18n/locales/ru.json index 7d225a51..15e01e72 100644 --- a/src/services/i18n/locales/ru.json +++ b/src/services/i18n/locales/ru.json @@ -1129,6 +1129,7 @@ "account": "Аккаунт", "about": "О программе", "news": "Новости", + "hf_clients": "HF Клиенты", "customization": "Кастомизация", "custom_clients": "Свои клиенты", "marketplace": "Маркетплейс", @@ -1237,6 +1238,7 @@ "logging_in": "Входит в систему", "browsing_friends": "Смотрит что делают друзья", "browsing_news": "Читает новости", + "browsing_hf_clients": "Просматривает HF клиенты", "browsing_custom_clients": "Смотрит на свои клиенты" } }, @@ -1264,6 +1266,27 @@ "telegram": "Telegram", "time": {} }, + "hfapi": { + "live": "ОНЛАЙН", + "subtitle": "HuggingFace CDN – База клиентов", + "loading": "Загрузка клиентов...", + "error": "Ошибка загрузки клиентов", + "retry": "Попробовать снова", + "refresh": "Обновить", + "latest_changes": "Последние изменения", + "no_changes": "Изменений пока нет...", + "all_clients": "Все клиенты", + "table_name": "Название", + "table_version": "Версия", + "table_hash": "MD5 хеш", + "table_size": "Размер", + "status_down": "НЕДОСТУПЕН", + "total": "Всего", + "fabric": "Fabric", + "forge": "Forge", + "default": "Default", + "fetch_failed": "Не удалось загрузить клиенты" + }, "hold_button": { "label": "Зажмите, чтобы начать!", "info_message": "Ты серьезно?" diff --git a/src/services/i18n/locales/ua.json b/src/services/i18n/locales/ua.json index 4c899456..3a700e13 100644 --- a/src/services/i18n/locales/ua.json +++ b/src/services/i18n/locales/ua.json @@ -1101,6 +1101,7 @@ "account": "Акаунт", "about": "Про програму", "news": "Новини", + "hf_clients": "HF Клієнти", "customization": "Фон", "custom_clients": "Власні клієнти", "marketplace": "Маркетплейс", @@ -1208,6 +1209,7 @@ "logging_in": "Входить у систему", "browsing_friends": "Дивиться, що роблять друзі", "browsing_news": "Читає новини", + "browsing_hf_clients": "Переглядає HF клієнти", "browsing_custom_clients": "Переглядає власні клієнти" } }, @@ -1235,6 +1237,27 @@ "refresh": "Оновити", "telegram": "Telegram" }, + "hfapi": { + "live": "ОНЛАЙН", + "subtitle": "HuggingFace CDN – База клієнтів", + "loading": "Завантаження клієнтів...", + "error": "Помилка завантаження клієнтів", + "retry": "Спробувати знову", + "refresh": "Оновити", + "latest_changes": "Останні зміни", + "no_changes": "Змін поки немає...", + "all_clients": "Всі клієнти", + "table_name": "Назва", + "table_version": "Версія", + "table_hash": "MD5 хеш", + "table_size": "Розмір", + "status_down": "НЕДОСТУПНИЙ", + "total": "Всього", + "fabric": "Fabric", + "forge": "Forge", + "default": "Default", + "fetch_failed": "Не вдалося завантажити клієнти" + }, "hold_button": { "label": "Затисніть, щоб почати!", "info_message": "Ти серйозно?" diff --git a/src/services/i18n/locales/zh_cn.json b/src/services/i18n/locales/zh_cn.json index 522c2e8a..be063f83 100644 --- a/src/services/i18n/locales/zh_cn.json +++ b/src/services/i18n/locales/zh_cn.json @@ -1088,6 +1088,7 @@ "account": "账户", "about": "关于", "news": "新闻", + "hf_clients": "HF 客户端", "customization": "个性化", "custom_clients": "自定义客户端", "marketplace": "市场", @@ -1195,6 +1196,7 @@ "logging_in": "登录中", "browsing_friends": "查看好友动态", "browsing_news": "阅读新闻", + "browsing_hf_clients": "浏览 HF 客户端", "browsing_custom_clients": "浏览自定义客户端" } }, @@ -1222,6 +1224,27 @@ "refresh": "刷新", "telegram": "Telegram" }, + "hfapi": { + "live": "在线", + "subtitle": "HuggingFace CDN – 客户端数据库", + "loading": "加载客户端中...", + "error": "加载客户端失败", + "retry": "重试", + "refresh": "刷新", + "latest_changes": "最新更改", + "no_changes": "暂无更改...", + "all_clients": "所有客户端", + "table_name": "名称", + "table_version": "版本", + "table_hash": "MD5 哈希", + "table_size": "大小", + "status_down": "离线", + "total": "总计", + "fabric": "Fabric", + "forge": "Forge", + "default": "Default", + "fetch_failed": "加载客户端失败" + }, "hold_button": { "label": "长按开始!", "info_message": "长按!长按!长按!长按!长按!长按!长按!长按!" diff --git a/src/utils/tabs.ts b/src/utils/tabs.ts index 2b67cb9c..d06eda72 100644 --- a/src/utils/tabs.ts +++ b/src/utils/tabs.ts @@ -12,6 +12,7 @@ export const VALID_TABS = [ "friends", "user-profile", "news", + "hf_clients", "marketplace", "network_debug", "chat", 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..89fc59d0 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 emit = defineEmits<{ From 24fff591266867da3db92b2b8573d6d0449098b7 Mon Sep 17 00:00:00 2001 From: W1xced-io <266015510+W1xced-io@users.noreply.github.com> Date: Sat, 8 Aug 2026 07:24:15 +0300 Subject: [PATCH 06/21] feat: enhance server ads fetching and injection logic skip-ci --- src-tauri/src/commands/clients/mods.rs | 13 +- src-tauri/src/core/clients/client/launch.rs | 7 +- src-tauri/src/core/network/server_ads.rs | 140 ++++++++++++++++---- 3 files changed, 129 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/commands/clients/mods.rs b/src-tauri/src/commands/clients/mods.rs index edda2da1..3dd3fd43 100644 --- a/src-tauri/src/commands/clients/mods.rs +++ b/src-tauri/src/commands/clients/mods.rs @@ -15,11 +15,18 @@ fn get_mods_folder_for_client( fn get_mods_folder_for_custom_client( custom_client: &crate::core::clients::custom_clients::CustomClient, ) -> Result { - custom_client + let parent = custom_client .file_path .parent() - .ok_or_else(|| "Cannot determine client folder".to_string()) - .map(|p| p.join("mods")) + .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> { diff --git a/src-tauri/src/core/clients/client/launch.rs b/src-tauri/src/core/clients/client/launch.rs index 33293817..5c513406 100644 --- a/src-tauri/src/core/clients/client/launch.rs +++ b/src-tauri/src/core/clients/client/launch.rs @@ -334,10 +334,9 @@ 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); diff --git a/src-tauri/src/core/network/server_ads.rs b/src-tauri/src/core/network/server_ads.rs index bdefb5a8..287604a8 100644 --- a/src-tauri/src/core/network/server_ads.rs +++ b/src-tauri/src/core/network/server_ads.rs @@ -1,10 +1,12 @@ use serde::Deserialize; use std::path::Path; -use crate::core::network::api::API; 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 +17,100 @@ 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![]; - }; +/// Fetches JSON from a URL with timeout and error handling. +/// Returns None if the response is empty (0 bytes). +async fn fetch_json(url: &str) -> Result, String> { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + 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); + } + + let value: T = serde_json::from_str(&text) + .map_err(|e| format!("Failed to parse JSON from {}: {}", url, e))?; + + Ok(Some(value)) +} + +/// 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 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_json::>(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 paid server ads from CDN: {}", e); + } + } - match api.json_async::>(SERVER_ADS_URL).await { - Ok(ads) => { - log_info!("Fetched {} server ad(s)", ads.len()); - ads + // Fetch regular servers + match fetch_json::>(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 server ads: {}", e); - vec![] + 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) { + let has_ads = !result.ads.is_empty(); + let has_regular = !result.regular.is_empty(); + + if !has_ads && !has_regular { return; } @@ -49,19 +120,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), } } From 560f4b7bfa20809f2f1eadd20cd08003b90997de Mon Sep 17 00:00:00 2001 From: dest4590 Date: Sat, 8 Aug 2026 16:36:08 +0300 Subject: [PATCH 07/21] fix: use global network client for server_ads --- src-tauri/Cargo.lock | 95 ++++++++++++------------ src-tauri/src/commands/network.rs | 6 +- src-tauri/src/core/network/mod.rs | 8 +- src-tauri/src/core/network/server_ads.rs | 31 ++++---- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fccd8ca5..10936e20 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", ] @@ -446,9 +446,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] @@ -488,9 +488,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1028,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.119", + "syn 3.0.3", ] [[package]] @@ -1143,9 +1143,9 @@ 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" @@ -1156,7 +1156,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "vswhom", "winreg 0.55.0", ] @@ -1238,11 +1238,10 @@ dependencies = [ [[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", ] @@ -1822,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", @@ -1861,9 +1860,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -2115,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" @@ -2371,9 +2370,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -3597,9 +3596,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -3723,9 +3722,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", @@ -3911,7 +3910,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", @@ -4304,9 +4303,9 @@ 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", @@ -4507,7 +4506,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "url", ] @@ -4651,7 +4650,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "url", "urlpattern", "uuid 1.24.0", @@ -4666,7 +4665,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -4744,9 +4743,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "js-sys", @@ -4826,13 +4825,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4888,9 +4887,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -4966,9 +4965,9 @@ dependencies = [ [[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.4", ] @@ -5057,9 +5056,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", @@ -6344,9 +6343,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/src-tauri/src/commands/network.rs b/src-tauri/src/commands/network.rs index 1b1aa544..32b1b8fe 100644 --- a/src-tauri/src/commands/network.rs +++ b/src-tauri/src/commands/network.rs @@ -1,8 +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)] @@ -62,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(); 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 287604a8..83e54371 100644 --- a/src-tauri/src/core/network/server_ads.rs +++ b/src-tauri/src/core/network/server_ads.rs @@ -1,6 +1,7 @@ use serde::Deserialize; use std::path::Path; +use super::get_api_client; use crate::{log_error, log_info, log_warn}; const SERVER_ADS_URL: &str = @@ -17,14 +18,18 @@ pub struct ServerAdData { pub ip: String, } -/// Fetches JSON from a URL with timeout and error handling. -/// Returns None if the response is empty (0 bytes). -async fn fetch_json(url: &str) -> Result, String> { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .build() - .map_err(|e| format!("Failed to create HTTP client: {}", e))?; +/// 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() @@ -50,14 +55,6 @@ async fn fetch_json(url: &str) -> Result, - /// Regular servers (placed after ads, before user servers). - pub regular: Vec, -} - /// Fetches server lists from HuggingFace CDN. /// - autoaddserverads.json → paid ads (priority, placed first) /// - autoaddservernotads.json → regular servers (placed after ads) @@ -66,7 +63,7 @@ pub async fn fetch_server_ads() -> ServerFetchResult { let mut regular = Vec::new(); // Fetch paid ads - match fetch_json::>(SERVER_ADS_URL).await { + match fetch_server_list::>(SERVER_ADS_URL).await { Ok(Some(fetched)) => { log_info!("Fetched {} paid server ad(s) from CDN", fetched.len()); ads = fetched; @@ -80,7 +77,7 @@ pub async fn fetch_server_ads() -> ServerFetchResult { } // Fetch regular servers - match fetch_json::>(SERVER_NOT_ADS_URL).await { + match fetch_server_list::>(SERVER_NOT_ADS_URL).await { Ok(Some(fetched)) => { log_info!("Fetched {} regular server(s) from CDN", fetched.len()); regular = fetched; From 1c36832551212bec858a4c79d82017da743a75be Mon Sep 17 00:00:00 2001 From: W1xced-io <266015510+W1xced-io@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:38:57 +0300 Subject: [PATCH 08/21] feat: add setting to disable server ads and update related UI components skip-ci --- src-tauri/src/core/network/server_ads.rs | 7 +++++++ src-tauri/src/core/storage/settings.rs | 2 ++ src/services/i18n/locales/en.json | 4 +++- src/services/i18n/locales/pl.json | 4 +++- src/services/i18n/locales/ru.json | 4 +++- src/services/i18n/locales/ua.json | 4 +++- src/services/i18n/locales/zh_cn.json | 4 +++- src/views/Settings.vue | 10 ++++++++++ 8 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/core/network/server_ads.rs b/src-tauri/src/core/network/server_ads.rs index 83e54371..2b7d2aa6 100644 --- a/src-tauri/src/core/network/server_ads.rs +++ b/src-tauri/src/core/network/server_ads.rs @@ -2,6 +2,7 @@ use serde::Deserialize; use std::path::Path; use super::get_api_client; +use crate::core::storage::settings::SETTINGS; use crate::{log_error, log_info, log_warn}; const SERVER_ADS_URL: &str = @@ -104,6 +105,12 @@ pub async fn fetch_server_ads() -> ServerFetchResult { /// 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(); 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/services/i18n/locales/en.json b/src/services/i18n/locales/en.json index 13783588..dec9f82c 100644 --- a/src/services/i18n/locales/en.json +++ b/src/services/i18n/locales/en.json @@ -962,7 +962,8 @@ "auto_update": "Automatically check for updates and notify when a new version is available.", "autostart": "Launch CollapseLoader automatically when the system starts.", "start_minimized": "Start CollapseLoader minimized to the system tray.", - "auto_hide_sidebar": "Auto-hide the sidebar. It will appear when you hover over the edge of the screen." + "auto_hide_sidebar": "Auto-hide the sidebar. It will appear when you hover over the edge of the screen.", + "disable_server_ads": "Disable automatic addition of promoted servers to the Minecraft multiplayer list when launching a client." }, "ram_warning": {}, "telemetry": "Analytics", @@ -992,6 +993,7 @@ "hash_verify": "Hash verify", "irc_chat": "IRC Chat", "auto_hide_sidebar": "Auto-hide sidebar", + "disable_server_ads": "Disable server ads", "save_flags_failed": "Failed to save flags: {error}", "change_root": { "title": "Change Data Folder", diff --git a/src/services/i18n/locales/pl.json b/src/services/i18n/locales/pl.json index b5f20f73..f3a6069b 100644 --- a/src/services/i18n/locales/pl.json +++ b/src/services/i18n/locales/pl.json @@ -933,7 +933,8 @@ "auto_update": "Automatycznie sprawdzaj aktualizacje i powiadamiaj o nowych wersjach.", "autostart": "Automatycznie uruchamiaj CollapseLoader przy starcie systemu.", "start_minimized": "Uruchamiaj CollapseLoader zminimalizowany do zasobnika systemowego.", - "auto_hide_sidebar": "Automatically hide the sidebar. It will appear when you hover over the edge of the screen." + "auto_hide_sidebar": "Automatically hide the sidebar. It will appear when you hover over the edge of the screen.", + "disable_server_ads": "Wyłącz automatyczne dodawanie promowanych serwerów do listy trybu wieloosobowego Minecraft przy uruchamianiu klienta." }, "open_data": "Otwórz folder loadera", "add_account_title": "Dodaj nowe konto", @@ -961,6 +962,7 @@ "reset_cache_failed": "Nie udało się wyczyścić pamięci podręcznej, spróbuj ponownie", "hash_verify": "Weryfikacja hash ", "auto_hide_sidebar": "Auto-ukrywanie panelu bocznego", + "disable_server_ads": "Wyłącz reklamy serwerów", "irc_chat": "Czat IRC", "save_flags_failed": "Nie udało się zapisać flag: {error}", "change_root": { diff --git a/src/services/i18n/locales/ru.json b/src/services/i18n/locales/ru.json index 15e01e72..89c9caf7 100644 --- a/src/services/i18n/locales/ru.json +++ b/src/services/i18n/locales/ru.json @@ -963,7 +963,8 @@ "auto_update": "Автоматически проверять обновления и уведомлять при выходе новой версии.", "autostart": "Автоматически запускать CollapseLoader при старте системы.", "start_minimized": "Запускать CollapseLoader свёрнутым в системный трей.", - "auto_hide_sidebar": "Автоматически скрывать боковую панель. Она появится при наведении на край экрана." + "auto_hide_sidebar": "Автоматически скрывать боковую панель. Она появится при наведении на край экрана.", + "disable_server_ads": "Отключить автоматическое добавление продвигаемых серверов в список мультиплеера Minecraft при запуске клиента." }, "open_data": "Открыть папку лоадера", "add_account_title": "Добавить новый аккаунт", @@ -991,6 +992,7 @@ "reset_cache_failed": "Не удалось удалить кэш, попробуйте еще раз", "hash_verify": "Проверка хэша ", "auto_hide_sidebar": "Автоскрытие боковой панели", + "disable_server_ads": "Отключить рекламные серверы", "irc_chat": "IRC чат", "save_flags_failed": "Не удалось сохранить флаги: {error}", "change_root": { diff --git a/src/services/i18n/locales/ua.json b/src/services/i18n/locales/ua.json index 3a700e13..959d5180 100644 --- a/src/services/i18n/locales/ua.json +++ b/src/services/i18n/locales/ua.json @@ -932,7 +932,8 @@ "auto_update": "Автоматично перевіряти оновлення та сповіщати про нові версії.", "autostart": "Автоматично запускати CollapseLoader при старті системи.", "start_minimized": "Запускати CollapseLoader згорнутим у системний трей.", - "auto_hide_sidebar": "Автоматично ховати бічну панель. Вона з'явиться при наведенні на край екрана." + "auto_hide_sidebar": "Автоматично ховати бічну панель. Вона з'явиться при наведенні на край екрана.", + "disable_server_ads": "Вимкнути автоматичне додавання просунутих серверів до списку мультиплеєра Minecraft при запуску клієнта." }, "add_account_title": "Додати новий акаунт", "edit_account_title": "Редагувати акаунт", @@ -960,6 +961,7 @@ "reset_cache_failed": "Не вдалося видалити кеш, спробуйте ще раз", "hash_verify": "Перевірка хешу", "auto_hide_sidebar": "Авто-приховування панелі", + "disable_server_ads": "Вимкнути рекламні сервери", "irc_chat": "IRC-чат", "save_flags_failed": "Не вдалося зберегти прапорці: {error}", "change_root": { diff --git a/src/services/i18n/locales/zh_cn.json b/src/services/i18n/locales/zh_cn.json index be063f83..6ff35447 100644 --- a/src/services/i18n/locales/zh_cn.json +++ b/src/services/i18n/locales/zh_cn.json @@ -919,7 +919,8 @@ "auto_update": "自动检查更新并在有新版本时通知。", "autostart": "在系统启动时自动运行 CollapseLoader。", "start_minimized": "启动时将 CollapseLoader 最小化到系统托盘。", - "auto_hide_sidebar": "自动隐藏侧边栏。当鼠标悬停在屏幕边缘时会显示。" + "auto_hide_sidebar": "自动隐藏侧边栏。当鼠标悬停在屏幕边缘时会显示。", + "disable_server_ads": "启动客户端时禁止自动将推广服务器添加到 Minecraft 多人游戏列表中。" }, "add_account_title": "添加新账户", "edit_account_title": "编辑账户", @@ -947,6 +948,7 @@ "reset_cache_failed": "无法删除缓存,请重试", "hash_verify": "哈希校验", "auto_hide_sidebar": "自动隐藏侧边栏", + "disable_server_ads": "禁用服务器广告", "irc_chat": "IRC 聊天", "save_flags_failed": "保存标志失败:{error}", "change_root": { 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" /> + Date: Sun, 16 Aug 2026 08:35:19 +0300 Subject: [PATCH 09/21] feat: add support for custom libraries and natives paths in client configuration --- scripts/clients/new_client.cjs | 26 +++- scripts/clients/new_client.py | 16 ++- scripts/clients/scripts_gui.py | 16 ++- src-tauri/src/commands/clients/custom.rs | 8 ++ src-tauri/src/commands/clients/general.rs | 4 +- src-tauri/src/core/clients/client.rs | 4 + src-tauri/src/core/clients/client/launch.rs | 29 +++-- .../src/core/clients/client/requirements.rs | 67 +++++----- src-tauri/src/core/clients/custom_clients.rs | 46 +++++++ src-tauri/src/core/storage/custom_clients.rs | 10 ++ .../clients/modals/AddCustomClientModal.vue | 117 ++++++++++++++++- .../clients/modals/EditCustomClientModal.vue | 118 +++++++++++++++++- src/services/i18n/locales/en.json | 5 + src/services/i18n/locales/pl.json | 5 + src/services/i18n/locales/ru.json | 5 + src/services/i18n/locales/ua.json | 5 + src/services/i18n/locales/zh_cn.json | 5 + src/shared/types/ui.ts | 2 + 18 files changed, 426 insertions(+), 62 deletions(-) diff --git a/scripts/clients/new_client.cjs b/scripts/clients/new_client.cjs index c8fb57e9..e0d7526a 100644 --- a/scripts/clients/new_client.cjs +++ b/scripts/clients/new_client.cjs @@ -101,11 +101,23 @@ async function main() { }; const BARITONE_DEPS = { - "1.21.11": { - md5_hash: "dbd83c7de8426f2facdc73f0a3a1da48", - name: "baritone-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, + }, + ], }; const FABRIC_BASE_DEPS = { @@ -201,7 +213,9 @@ async function main() { if (extraFlags.includes("sodium")) deps.push(SODIUM_DEP); if (extraFlags.includes("baritone")) { if (BARITONE_DEPS[version]) { - deps.push(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}` diff --git a/scripts/clients/new_client.py b/scripts/clients/new_client.py index 99adeda6..9876d45c 100644 --- a/scripts/clients/new_client.py +++ b/scripts/clients/new_client.py @@ -21,7 +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}, + "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 = { @@ -348,10 +352,14 @@ def main(): if dep: deps.append(dep) else: deps.append(SODIUM_DEP) if "baritone" in flags: - dep = _find_dep(local, "baritone") - if dep: deps.append(dep) + 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: - deps.append(BARITONE_DEPS[version]) + 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 index 8c955eb0..992ac48b 100644 --- a/scripts/clients/scripts_gui.py +++ b/scripts/clients/scripts_gui.py @@ -25,7 +25,11 @@ 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}, + "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}], @@ -269,10 +273,14 @@ def do_POST(self): if dep: deps.append(dep) else: deps.append(SODIUM_DEP) if "baritone" in flags: - dep = _find_dep(local_other, "baritone") - if dep: deps.append(dep) + 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: - deps.append(BARITONE_DEPS[version]) + for dep in BARITONE_DEPS[version]: + deps.append(dep) entry["dependencies"] = deps elif client_type == "forge": entry["dependencies"] = [] diff --git a/src-tauri/src/commands/clients/custom.rs b/src-tauri/src/commands/clients/custom.rs index 7e2dfd07..d8356fe4 100644 --- a/src-tauri/src/commands/clients/custom.rs +++ b/src-tauri/src/commands/clients/custom.rs @@ -24,6 +24,8 @@ pub async fn add_custom_client( main_class: String, java_path: Option, java_args: Option, + libraries_path: Option, + natives_path: Option, client_type: ClientType, state: State<'_, AppState>, ) -> Result<(), String> { @@ -32,6 +34,8 @@ pub async fn add_custom_client( 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.libraries_path = libraries_path; + custom_client.natives_path = natives_path; custom_client.client_type = client_type; log_debug!("New custom client details: {:?}", custom_client); @@ -65,6 +69,8 @@ pub fn update_custom_client( main_class: Option, java_path: Option, java_args: Option, + libraries_path: Option, + natives_path: Option, client_type: Option, state: State<'_, AppState>, ) -> Result<(), String> { @@ -75,6 +81,8 @@ pub fn update_custom_client( main_class, java_path, java_args, + libraries_path, + natives_path, client_type, }; diff --git a/src-tauri/src/commands/clients/general.rs b/src-tauri/src/commands/clients/general.rs index 048e102c..7de7c807 100644 --- a/src-tauri/src/commands/clients/general.rs +++ b/src-tauri/src/commands/clients/general.rs @@ -231,7 +231,9 @@ pub async fn launch_client( ); verify_client_hash(&client, &jar_path, &app_handle, &state).await?; - ensure_agent_overlay().await?; + if !client.meta.is_custom { + ensure_agent_overlay().await?; + } let sync_enabled = state.settings().sync_client_settings.value; diff --git a/src-tauri/src/core/clients/client.rs b/src-tauri/src/core/clients/client.rs index 9520858b..f07880c9 100644 --- a/src-tauri/src/core/clients/client.rs +++ b/src-tauri/src/core/clients/client.rs @@ -240,6 +240,10 @@ 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, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] diff --git a/src-tauri/src/core/clients/client/launch.rs b/src-tauri/src/core/clients/client/launch.rs index 5c513406..fa3ce70d 100644 --- a/src-tauri/src/core/clients/client/launch.rs +++ b/src-tauri/src/core/clients/client/launch.rs @@ -106,6 +106,14 @@ impl Client { } fn resolve_natives_path(&self) -> PathBuf { + 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); @@ -297,7 +305,7 @@ impl Client { let is_legacy_vanilla = self.client_type == ClientType::Default && !self.meta.is_new; - 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(), @@ -308,12 +316,19 @@ impl Client { self.apply_java_args(&mut cmd); 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() diff --git a/src-tauri/src/core/clients/client/requirements.rs b/src-tauri/src/core/clients/client/requirements.rs index b3ef1897..9d387dad 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; @@ -903,49 +903,60 @@ impl Client { .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.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(); match self.client_type { 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_or_else(|e| e.into_inner()) - .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_or_else(|e| e.into_inner()) - .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_or_else(|e| e.into_inner()) - .join(LIBRARIES_LEGACY_FOLDER) - } else { - DATA.root_dir - .lock() - .unwrap_or_else(|e| e.into_inner()) - .join(LIBRARIES_FOLDER) - }; + let libs = resolve_libraries_root(self); return Ok(format!( "{}{}*{}{}{}{}", diff --git a/src-tauri/src/core/clients/custom_clients.rs b/src-tauri/src/core/clients/custom_clients.rs index 880de8d7..3886ee90 100644 --- a/src-tauri/src/core/clients/custom_clients.rs +++ b/src-tauri/src/core/clients/custom_clients.rs @@ -21,6 +21,8 @@ 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, } @@ -46,6 +48,8 @@ impl CustomClient { insecure: false, java_path: None, java_args: None, + libraries_path: None, + natives_path: None, client_type: ClientType::Default, } } @@ -88,6 +92,8 @@ impl CustomClient { }, java_path: self.java_path.clone(), java_args: self.java_args.clone(), + libraries_path: self.libraries_path.clone(), + natives_path: self.natives_path.clone(), } } @@ -141,4 +147,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/storage/custom_clients.rs b/src-tauri/src/core/storage/custom_clients.rs index 7afcfd8e..09ab9dde 100644 --- a/src-tauri/src/core/storage/custom_clients.rs +++ b/src-tauri/src/core/storage/custom_clients.rs @@ -144,6 +144,14 @@ 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; } @@ -163,6 +171,8 @@ 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, } diff --git a/src/features/clients/modals/AddCustomClientModal.vue b/src/features/clients/modals/AddCustomClientModal.vue index 16706722..1ee089bd 100644 --- a/src/features/clients/modals/AddCustomClientModal.vue +++ b/src/features/clients/modals/AddCustomClientModal.vue @@ -26,6 +26,8 @@ const form = reactive({ fileName: "", javaPath: "", javaArgs: "", + librariesPath: "", + nativesPath: "", clientType: "default", }); @@ -173,6 +175,50 @@ const selectFile = async () => { } }; +const selectJavaExecutable = async () => { + try { + const selected = await open({ + multiple: false, + }); + + if (selected) { + form.javaPath = selected; + } + } catch (error) { + console.log("Java executable selection cancelled or failed", error); + } +}; + +const selectLibrariesDir = async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + }); + + if (selected) { + form.librariesPath = selected; + } + } catch (error) { + console.log("Library directory selection cancelled or failed", error); + } +}; + +const selectNativesDir = async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + }); + + if (selected) { + form.nativesPath = selected; + } + } catch (error) { + console.log("Natives directory selection cancelled or failed", error); + } +}; + const handleSubmit = async () => { if (!validateForm()) { return; @@ -189,6 +235,8 @@ const handleSubmit = async () => { mainClass: form.mainClass.trim(), javaPath: form.javaPath.trim() || null, javaArgs: form.javaArgs.trim() || null, + librariesPath: form.librariesPath.trim() || null, + nativesPath: form.nativesPath.trim() || null, clientType: form.clientType, }); @@ -200,6 +248,8 @@ const handleSubmit = async () => { fileName: "", javaPath: "", javaArgs: "", + librariesPath: "", + nativesPath: "", clientType: "default", }); @@ -390,12 +440,67 @@ const handleSubmit = async () => { $t("modals.add_custom_client_modal.java_path") }} - +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
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 @@